diff --git a/packages/core/src/public/opencode.ts b/packages/core/src/public/opencode.ts index 4e85d5fe1144..7e58f2989792 100644 --- a/packages/core/src/public/opencode.ts +++ b/packages/core/src/public/opencode.ts @@ -51,6 +51,7 @@ export const layer = Layer.effect( }), get: sessions.get, list: sessions.list, + switchModel: sessions.switchModel, interrupt: sessions.interrupt, prompt: (input) => sessions.prompt({ diff --git a/packages/core/src/public/session.ts b/packages/core/src/public/session.ts index f4b5c79620e9..6827be0a035b 100644 --- a/packages/core/src/public/session.ts +++ b/packages/core/src/public/session.ts @@ -59,6 +59,11 @@ export interface PromptInput { readonly delivery?: Delivery } +export interface SwitchModelInput { + readonly sessionID: ID + readonly model: Model.Ref +} + export interface MessagesInput { readonly sessionID: ID readonly limit?: number @@ -84,6 +89,7 @@ export interface Interface { readonly get: (sessionID: ID) => Effect.Effect readonly list: (input?: ListInput) => Effect.Effect readonly prompt: (input: PromptInput) => Effect.Effect + readonly switchModel: (input: SwitchModelInput) => Effect.Effect /** Interrupt the active V2 execution chain for one Session on this process. Interrupting an idle or missing Session is a no-op. */ readonly interrupt: (sessionID: ID) => Effect.Effect readonly messages: (input: MessagesInput) => Effect.Effect diff --git a/packages/core/test/public-opencode.test.ts b/packages/core/test/public-opencode.test.ts index c86a3c28bb66..54d1bc7bfb7b 100644 --- a/packages/core/test/public-opencode.test.ts +++ b/packages/core/test/public-opencode.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Schema } from "effect" -import { OpenCode, Session, Tool } from "@opencode-ai/core/public" +import { AbsolutePath, Location, Model, OpenCode, Session, Tool } from "@opencode-ai/core/public" import { testEffect } from "./lib/effect" const it = testEffect(OpenCode.layer) @@ -22,6 +22,7 @@ describe("public native OpenCode API", () => { "message", "messages", "prompt", + "switchModel", ]) expect(Session.ID.create()).toStartWith("ses_") expect(Session.MessageID.create()).toStartWith("msg_") @@ -36,4 +37,41 @@ describe("public native OpenCode API", () => { }) }), ) + + it.effect("switches the exact Session to the exact model through the durable facade", () => + Effect.gen(function* () { + const opencode = yield* OpenCode.Service + const targetID = Session.ID.make("ses_public_switch_target") + const otherID = Session.ID.make("ses_public_switch_other") + const model = Schema.decodeUnknownSync(Model.Ref)({ + id: "claude-sonnet-4-5", + providerID: "anthropic", + variant: "high", + }) + const location = Location.Ref.make({ directory: AbsolutePath.make("/public-session-switch-model") }) + yield* opencode.sessions.create({ id: targetID, location }) + yield* opencode.sessions.create({ id: otherID, location }) + + yield* opencode.sessions.switchModel({ sessionID: targetID, model }) + + expect((yield* opencode.sessions.get(targetID)).model).toEqual(model) + expect((yield* opencode.sessions.get(otherID)).model).toBeUndefined() + }), + ) + + it.effect("preserves the typed not-found error for a missing Session", () => + Effect.gen(function* () { + const opencode = yield* OpenCode.Service + const sessionID = Session.ID.make("ses_public_switch_missing") + const error = yield* opencode.sessions + .switchModel({ + sessionID, + model: Schema.decodeUnknownSync(Model.Ref)({ id: "claude-sonnet-4-5", providerID: "anthropic" }), + }) + .pipe(Effect.flip) + + expect(error).toBeInstanceOf(Session.NotFoundError) + expect(error.sessionID).toBe(sessionID) + }), + ) })