diff --git a/packages/provider-opencode/src/server/adapter.test.ts b/packages/provider-opencode/src/server/adapter.test.ts index 759545fff0d19..55faec513c7d9 100644 --- a/packages/provider-opencode/src/server/adapter.test.ts +++ b/packages/provider-opencode/src/server/adapter.test.ts @@ -15,6 +15,7 @@ import { ThreadId, type OrchestrationV2ProviderThread, type OrchestrationV2ProviderTurn, + type ModelSelection, } from "@t3tools/contracts"; import { OpenCodeSettings } from "../settings.ts"; import * as Effect from "effect/Effect"; @@ -145,15 +146,22 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct nativeSessionId: string, client: object, host: Partial = {}, + selection: { + readonly options?: ModelSelection["options"]; + readonly interactionMode?: ProviderAdapter.ProviderAdapterV2RuntimePolicy["interactionMode"]; + } = {}, ) { const instanceId = ProviderInstanceId.make(`opencode-${suffix}`); const threadId = ThreadId.make(`thread-opencode-${suffix}`); const modelSelection = { instanceId, model: "anthropic/claude-sonnet", - options: [], + options: selection.options ?? [], }; - const policy = runtimePolicy("full-access", { cwd: "/workspace" }); + const policy = runtimePolicy("full-access", { + cwd: "/workspace", + interactionMode: selection.interactionMode ?? "default", + }); const adapter = yield* makeOpenCodeAdapterV2({ instanceId, settings: OPEN_CODE_TEST_SETTINGS, @@ -194,7 +202,7 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct providerInstanceId: modelSelection.instanceId, modelSelection, runtimeMode: "full-access", - interactionMode: "default", + interactionMode: policy.interactionMode, branch: null, worktreePath: null, activeProviderThreadId: providerThread.id, @@ -240,6 +248,73 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct }); describe("OpenCodeAdapterV2", () => { + it.effect.each(["custom", "plan", "default"] as const)( + "keeps %s agent and reasoning options when steering", + (scenario) => + Effect.gen(function* () { + const nativeEvents = asyncEventStream(); + const prompts: Array[0]> = []; + const harness = yield* makeOpenCodeRuntimeHarness( + `steer-options-${scenario}`, + "root", + { + event: { subscribe: async () => ({ stream: nativeEvents.stream }) }, + session: { + create: async () => ({ data: { id: "root", time: { created: 1, updated: 1 } } }), + get: async () => ({ data: { id: "root", time: { created: 1, updated: 1 } } }), + messages: async () => ({ data: [] }), + promptAsync: async (input: (typeof prompts)[number]) => { + prompts.push(input); + return { data: true }; + }, + abort: async () => ({ data: true }), + children: async () => ({ data: [] }), + }, + }, + {}, + { + interactionMode: scenario === "default" ? "default" : "plan", + options: + scenario === "custom" + ? [ + { id: "agent", value: "agent-b" }, + { id: "variant", value: "high" }, + ] + : [], + }, + ); + yield* harness.startTurn(); + const snapshot = yield* harness.runtime.readThreadSnapshot({ + providerThread: harness.providerThread, + }); + yield* harness.runtime.steerTurn({ + threadId: harness.threadId, + runId: harness.runId, + providerThread: harness.providerThread, + providerTurnId: snapshot.providerTurns.at(-1)!.id, + message: { + messageId: MessageId.make("message-steer-options"), + text: "Continue with the same agent", + attachments: [], + createdBy: "user", + creationSource: "web", + }, + }); + assert.lengthOf(prompts, 2); + assert.equal( + prompts[1]?.agent, + scenario === "custom" ? "agent-b" : scenario === "plan" ? "plan" : undefined, + ); + assert.equal(prompts[1]?.variant, scenario === "custom" ? "high" : undefined); + assert.isNotEmpty(prompts[0]?.system); + assert.equal(prompts[1]?.system, prompts[0]?.system); + assert.notEqual(prompts[1]?.messageID, prompts[0]?.messageID); + }).pipe( + Effect.provide(Layer.merge(IdAllocator.layer, McpProviderSessions.layer)), + Effect.scoped, + ), + ); + it.effect("applies a changed runtime policy to the session on resume and turn start", () => Effect.gen(function* () { const updates: Array<{ sessionID: string; permission: unknown }> = []; diff --git a/packages/provider-opencode/src/server/adapter.ts b/packages/provider-opencode/src/server/adapter.ts index bad3e8cd7b89a..a4fe90c2ef06d 100644 --- a/packages/provider-opencode/src/server/adapter.ts +++ b/packages/provider-opencode/src/server/adapter.ts @@ -2927,6 +2927,23 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function }; }); + const promptOptions = (turn: ActiveOpenCodeTurn) => { + const agent = + getModelSelectionStringOptionValue(turn.modelSelection, "agent") ?? + (turn.runtimePolicy.interactionMode === "plan" ? "plan" : undefined); + const variant = getModelSelectionStringOptionValue(turn.modelSelection, "variant"); + return { + ...(agent === undefined ? {} : { agent }), + ...(variant === undefined ? {} : { variant }), + system: [ + orchestrationSystemPrompt, + buildRuntimeInstructions({ harness: "OpenCode", model: turn.modelSelection.model }), + ] + .filter(Boolean) + .join("\n\n"), + }; + }; + // Native commands await generation; user-message receipts admit their background requests. const submitPrompt = Effect.fn("OpenCode.submitPrompt")(function* ( state: OpenCodeThreadState, @@ -3249,22 +3266,6 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function ); return; } - const systemPrompt = [ - orchestrationSystemPrompt, - buildRuntimeInstructions({ - harness: "OpenCode", - model: turnInput.modelSelection.model, - }), - ] - .filter(Boolean) - .join("\n\n"); - const agent = - getModelSelectionStringOptionValue(turnInput.modelSelection, "agent") ?? - (turnInput.runtimePolicy.interactionMode === "plan" ? "plan" : undefined); - const variant = getModelSelectionStringOptionValue( - turnInput.modelSelection, - "variant", - ); yield* submitPrompt( state, turn, @@ -3272,9 +3273,7 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function sessionID: sessionId, messageID: turn.admissionMessageId!, model: parsedModel, - ...(agent === undefined ? {} : { agent }), - ...(variant === undefined ? {} : { variant }), - system: systemPrompt, + ...promptOptions(turn), parts, }, admissionAbortController!, @@ -3385,6 +3384,7 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function sessionID: sessionId, messageID: turn.admissionMessageId, model: parsedModel, + ...promptOptions(turn), parts, }, admissionAbortController!,