diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index d352cb68cee..c04e13bca4b 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -8,9 +8,10 @@ import { Bus } from "../bus.js" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { llmClient } from "../effect/app-node-platform.js" import { SessionEvent } from "./event.js" +import type { SessionContext } from "./context.js" import type { SessionMessage } from "./message.js" -import { SessionModelRequest } from "./model-request.js" -import { SessionRunnerModel } from "./runner/model.js" +import type { SessionModelRequest } from "./model-request.js" +import type { SessionRunnerModel } from "./runner/model.js" import { SessionSchema } from "./schema.js" import { toSessionError } from "./to-session-error.js" import { Token } from "../util/token.js" @@ -69,14 +70,13 @@ type Dependencies = { readonly llm: { readonly stream: (request: LLMRequest, options?: StreamOptions) => Stream.Stream } - readonly models: SessionRunnerModel.Interface - readonly modelRequests: SessionModelRequest.Interface } export type AutoInput = { readonly session: SessionSchema.Info readonly messages: readonly SessionMessage.Info[] readonly resolved: SessionRunnerModel.Resolved + readonly prepare: SessionModelRequest.Interface["prepare"] } type RequiredInput = Pick @@ -86,6 +86,9 @@ export type ManualInput = { readonly messages: readonly SessionMessage.Info[] readonly inputID: SessionMessage.ID readonly started?: boolean + /** Invoked after content planning, not when the caller captures the operation. */ + readonly resolveModel: SessionContext.Interface["resolveModel"] + readonly prepare: SessionModelRequest.Interface["prepare"] } type Plan = { @@ -96,6 +99,7 @@ type Plan = { readonly recent: string readonly inputID?: SessionMessage.ID readonly started?: boolean + readonly prepare: SessionModelRequest.Interface["prepare"] } export type Outcome = @@ -278,7 +282,7 @@ const make = (dependencies: Dependencies) => { }) : Effect.void, ) - const prepared = yield* dependencies.modelRequests.prepare({ + const prepared = yield* plan.prepare({ scope: { session: plan.session, agentID: Agent.ID.make("compaction"), model: plan.resolved }, transcript: { system: [], messages: [Message.user(plan.prompt)] }, contextHooks: false, @@ -348,6 +352,7 @@ const make = (dependencies: Dependencies) => { return yield* execute({ session: input.session, resolved: input.resolved, + prepare: input.prepare, reason: "auto", ...content, }) @@ -387,7 +392,7 @@ const make = (dependencies: Dependencies) => { error: { type: "compaction.unavailable", message: "Nothing to compact yet" }, inputID: input.inputID, }) - const resolved = yield* dependencies.models.resolve(input.session).pipe( + const resolved = yield* input.resolveModel(input.session).pipe( Effect.catch((cause) => failed({ sessionID: input.session.id, @@ -401,6 +406,7 @@ const make = (dependencies: Dependencies) => { return yield* execute({ session: input.session, resolved, + prepare: input.prepare, reason: "manual", inputID: input.inputID, started: input.started, @@ -422,14 +428,12 @@ export const layer = Layer.effect( Effect.gen(function* () { const bus = yield* Bus.Service const llm = yield* LLMClient.Service - const models = yield* SessionRunnerModel.Service - const modelRequests = yield* SessionModelRequest.Service - return make({ bus, llm, models, modelRequests }) + return make({ bus, llm }) }), ) export const node = makeLocationNode({ service: Service, layer, - deps: [Bus.node, llmClient, SessionRunnerModel.node, SessionModelRequest.node], + deps: [Bus.node, llmClient], }) diff --git a/packages/core/src/session/context.ts b/packages/core/src/session/context.ts index f48bb567aaf..828c133b5e5 100644 --- a/packages/core/src/session/context.ts +++ b/packages/core/src/session/context.ts @@ -2,6 +2,7 @@ export * as SessionContext from "./context.js" import { Context, Effect, Layer } from "effect" import { Agent } from "../agent.js" +import { Catalog } from "../catalog.js" import { CodeModeInstructions } from "../codemode/instructions.js" import { Database } from "../database/database.js" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" @@ -11,6 +12,7 @@ import { InstructionBuiltIns } from "../instructions/builtins.js" import { Location } from "../location.js" import { McpInstructions } from "../mcp/instructions.js" import { McpTool } from "../tool/mcp.js" +import { Model } from "../model.js" import { PluginSupervisor } from "../plugin/supervisor.js" import { ReferenceInstructions } from "../reference/instructions.js" import { SkillInstructions } from "../skill/instructions.js" @@ -19,6 +21,7 @@ import { AgentNotFoundError } from "./error.js" import { SessionHistory } from "./history.js" import { InstructionEntry } from "./instruction-entry.js" import { SessionMessage } from "./message.js" +import { SessionModelRequest } from "./model-request.js" import { SessionRunnerModel } from "./runner/model.js" import { SessionSchema } from "./schema.js" import { SessionStore } from "./store.js" @@ -42,14 +45,27 @@ export interface Loaded { /** * Resolves model-request state in two phases: `select` fixes the Session, * 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. + * active history for that selection. Auxiliary operations resolve only the + * capabilities they need; request preparation stays separate from selection. */ export interface Interface { /** 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 + readonly resolveModel: ( + session: SessionSchema.Info, + ) => Effect.Effect + /** Selects auxiliary title capabilities without instruction or tool preflight. */ + readonly selectTitle: (session: SessionSchema.Info) => Effect.Effect< + | { + readonly agent: Agent.Info + readonly primary: SessionRunnerModel.Resolved | undefined + readonly selected: SessionRunnerModel.Resolved + } + | undefined + > + readonly prepare: SessionModelRequest.Interface["prepare"] } /** Location-scoped model-context loader for durable Session Steps. */ @@ -60,6 +76,7 @@ const layer = Layer.effect( Effect.gen(function* () { const agents = yield* Agent.Service const builtins = yield* InstructionBuiltIns.Service + const catalog = yield* Catalog.Service const db = (yield* Database.Service).db const discovery = yield* InstructionDiscovery.Service const entries = yield* InstructionEntry.Service @@ -67,12 +84,41 @@ const layer = Layer.effect( const mcpInstructions = yield* McpInstructions.Service const mcpTools = yield* McpTool.Service const models = yield* SessionRunnerModel.Service + const modelRequests = yield* SessionModelRequest.Service const plugins = yield* PluginSupervisor.Service const referenceInstructions = yield* ReferenceInstructions.Service const skillInstructions = yield* SkillInstructions.Service const store = yield* SessionStore.Service const registry = yield* Tool.Service + const resolveModel = (session: SessionSchema.Info) => models.resolve(session, catalog.model.available) + + const selectTitle = Effect.fn("SessionContext.selectTitle")(function* (session: SessionSchema.Info) { + const agent = yield* agents.get(Agent.ID.make("title")) + if (!agent) return + const primary = yield* resolveModel(session).pipe(Effect.orElseSucceed(() => undefined)) + const info = yield* Effect.gen(function* () { + if (agent.model) return yield* catalog.model.get(agent.model.providerID, agent.model.id) + if (!primary) return + return yield* catalog.model.small(primary.ref.providerID) + }) + const variant = + agent.model?.variant ?? MINIMAL_REASONING_VARIANTS.find((id) => info?.variants.some((item) => item.id === id)) + const preferred = + info && + (yield* resolveModel({ + ...session, + model: Model.Ref.make({ + providerID: info.providerID, + id: info.id, + ...(variant ? { variant } : {}), + }), + }).pipe(Effect.orElseSucceed(() => undefined))) + const selected = preferred ?? primary + if (!selected) return + return { agent, primary, selected } + }) + const select = Effect.fn("SessionContext.select")(function* (sessionID: SessionSchema.ID) { const session = yield* store.get(sessionID) if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`)) @@ -112,7 +158,7 @@ const layer = Layer.effect( }) const load = Effect.fn("SessionContext.load")(function* (selection: Selection) { - const model = yield* models.resolve(selection.session) + const model = yield* resolveModel(selection.session) const history = yield* SessionHistory.entriesForRunner(db, selection.session.id, selection.instructions) return { session: selection.session, @@ -124,15 +170,19 @@ const layer = Layer.effect( } }) - return Service.of({ select, load }) + return Service.of({ select, load, resolveModel, selectTitle, prepare: modelRequests.prepare }) }), ) +/** Variant IDs that minimize reasoning output, in preference order. */ +const MINIMAL_REASONING_VARIANTS = ["none", "minimal", "low"].map((id) => Model.VariantID.make(id)) + export const node = makeLocationNode({ service: Service, layer, deps: [ Agent.node, + Catalog.node, Database.node, InstructionBuiltIns.node, InstructionDiscovery.node, @@ -143,6 +193,7 @@ export const node = makeLocationNode({ PluginSupervisor.node, ReferenceInstructions.node, SessionRunnerModel.node, + SessionModelRequest.node, SessionStore.node, SkillInstructions.node, Tool.node, diff --git a/packages/core/src/session/generate-node.ts b/packages/core/src/session/generate-node.ts index 5b6e289dc7c..850b94b24c7 100644 --- a/packages/core/src/session/generate-node.ts +++ b/packages/core/src/session/generate-node.ts @@ -9,7 +9,6 @@ import { SessionContext } from "./context.js" import { SessionGenerate } from "./generate.js" import { SessionHistory } from "./history.js" import { SessionModelRequest } from "./model-request.js" -import { SessionRunnerModel } from "./runner/model.js" export const layer = Layer.effect( SessionGenerate.Service, @@ -17,13 +16,11 @@ export const layer = Layer.effect( const context = yield* SessionContext.Service const database = yield* Database.Service const llm = yield* LLMClient.Service - const models = yield* SessionRunnerModel.Service - const modelRequests = yield* SessionModelRequest.Service return SessionGenerate.Service.of({ generate: Effect.fn("SessionGenerate.generate")(function* (input) { const selection = yield* context.select(input.sessionID) - const model = yield* models.resolve(selection.session) + const model = yield* context.resolveModel(selection.session) const history = yield* SessionHistory.preview(database.db, selection.session.id, selection.instructions) const transcript = SessionModelRequest.baseTranscript({ agent: selection.agent.info, @@ -32,7 +29,7 @@ export const layer = Layer.effect( initial: history.initial, messages: history.messages, }) - const prepared = yield* modelRequests.prepare({ + const prepared = yield* context.prepare({ scope: { session: selection.session, agentID: selection.agent.id, model, tools: selection.tools }, transcript: { system: transcript.system, @@ -59,5 +56,5 @@ export const layer = Layer.effect( export const node = makeLocationNode({ service: SessionGenerate.Service, layer, - deps: [SessionContext.node, Database.node, SessionModelRequest.node, SessionRunnerModel.node, llmClient], + deps: [SessionContext.node, Database.node, llmClient], }) diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index cd0ea5913e1..d797f5e941b 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -36,7 +36,6 @@ const layer = Layer.effect( const bus = yield* Bus.Service const store = yield* SessionStore.Service const context = yield* SessionContext.Service - const modelRequests = yield* SessionModelRequest.Service const modelTransport = yield* SessionModelTransport.Service const db = (yield* Database.Service).db const compaction = yield* SessionCompaction.Service @@ -142,6 +141,8 @@ const layer = Layer.effect( Effect.gen(function* () { return yield* compaction.compactManual({ session, + resolveModel: context.resolveModel, + prepare: context.prepare, messages: yield* store.context(sessionID), inputID: pending.id, started: true, @@ -215,7 +216,12 @@ const layer = Layer.effect( // Reuse boundary preparation once; retries refresh context without delivering more input. const loaded = initial ?? (yield* prepareContext(sessionID).pipe(Effect.flatMap(context.load))) initial = undefined - const compactionInput = { session: loaded.session, messages: loaded.messages, resolved: loaded.model } + const compactionInput = { + session: loaded.session, + messages: loaded.messages, + resolved: loaded.model, + prepare: context.prepare, + } if (compaction.required(compactionInput)) { const compacted = yield* compaction.compact(compactionInput) if (compacted.status !== "completed") return yield* new StepFailedError({ error: compacted.error }) @@ -230,7 +236,7 @@ const layer = Layer.effect( initial: loaded.initial, messages: loaded.messages, }) - const prepared = yield* modelRequests.prepare({ + const prepared = yield* context.prepare({ scope: { session: loaded.session, agentID: loaded.agent.id, model: loaded.model, tools: loaded.tools }, transcript: { system: transcript.system, @@ -319,7 +325,6 @@ export const node = makeLocationNode({ Bus.node, llmClient, SessionContext.node, - SessionModelRequest.node, SessionModelTransport.node, SessionStore.node, SessionCompaction.node, diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index 091880452a3..8d251633ee8 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -3,7 +3,6 @@ export * as SessionRunnerModel from "./model.js" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { LanguageModel } from "@opencode-ai/ai" import { Context, Effect, Layer, Schema } from "effect" -import { Catalog } from "../../catalog.js" import { ModelResolver } from "../../model-resolver.js" import { Capabilities, ID, Info, Ref, VariantID } from "../../model.js" import { Provider } from "../../provider.js" @@ -41,7 +40,11 @@ export type Error = ModelNotSelectedError | ModelUnavailableError | ModelResolve export type Resolved = ModelResolver.Resolved export interface Interface { - readonly resolve: (session: SessionSchema.Info) => Effect.Effect + /** Availability is sampled lazily for each explicitly selected model resolution. */ + readonly resolve: ( + session: SessionSchema.Info, + available: () => Effect.Effect>, + ) => Effect.Effect } export class Service extends Context.Service()("@opencode/SessionRunnerModel") {} @@ -70,17 +73,16 @@ export const resolved = ( const layer = Layer.effect( Service, Effect.gen(function* () { - const catalog = yield* Catalog.Service const resolver = yield* ModelResolver.Service return Service.of({ - resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) { + resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session, available) { // Location plugins populate and filter the catalog asynchronously during layer startup. 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( + const selected = (yield* available()).find( (model) => model.providerID === session.model?.providerID && model.id === session.model.id, ) if (!selected) @@ -94,4 +96,4 @@ const layer = Layer.effect( }), ) -export const node = makeLocationNode({ service: Service, layer, deps: [Catalog.node, ModelResolver.node] }) +export const node = makeLocationNode({ service: Service, layer, deps: [ModelResolver.node] }) diff --git a/packages/core/src/session/title.ts b/packages/core/src/session/title.ts index 5dcec6ed201..4117fe249f4 100644 --- a/packages/core/src/session/title.ts +++ b/packages/core/src/session/title.ts @@ -4,18 +4,16 @@ import { isDeepStrictEqual } from "node:util" import { LLMClient, AIError, LLMEvent, Message, SystemPart, type LLMRequest } from "@opencode-ai/ai" import type { StreamOptions } from "@opencode-ai/ai/route" import { Context, DateTime, Effect, Layer, Stream } from "effect" -import { Agent } from "../agent.js" -import { Catalog } from "../catalog.js" +import type { Agent } from "../agent.js" import { Database } from "../database/database.js" import { Bus } from "../bus.js" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { isExactRootFallback } from "@opencode-ai/util/session-title-fallback" import { llmClient } from "../effect/app-node-platform.js" -import { Model } from "../model.js" +import { SessionContext } from "./context.js" import { SessionEvent } from "./event.js" import { SessionHistory } from "./history.js" -import { SessionModelRequest } from "./model-request.js" -import { SessionRunnerModel } from "./runner/model.js" +import type { SessionRunnerModel } from "./runner/model.js" import { SessionSchema } from "./schema.js" import { SessionUsage } from "./usage.js" import { SessionStore } from "./store.js" @@ -30,10 +28,7 @@ type Dependencies = { readonly llm: { readonly stream: (request: LLMRequest, options?: StreamOptions) => Stream.Stream } - readonly agents: Agent.Interface - readonly catalog: Catalog.Interface - readonly models: SessionRunnerModel.Interface - readonly modelRequests: SessionModelRequest.Interface + readonly context: SessionContext.Interface readonly store: SessionStore.Interface } @@ -72,7 +67,7 @@ const attempt = Effect.fn("SessionTitle.attempt")(function* ( }) : Effect.void, ) - const prepared = yield* dependencies.modelRequests.prepare({ + const prepared = yield* dependencies.context.prepare({ scope: { session: input.session, agentID: input.agent.id, model: input.model }, transcript: { system: input.agent.system ? [SystemPart.make(input.agent.system)] : [], @@ -106,9 +101,6 @@ const attempt = Effect.fn("SessionTitle.attempt")(function* ( .find((line) => line.length > 0) }) -/** Variant IDs that minimize reasoning output, in preference order. */ -const MINIMAL_REASONING_VARIANTS = ["none", "minimal", "low"].map((id) => Model.VariantID.make(id)) - const make = (dependencies: Dependencies) => { const generate = Effect.fn("SessionTitle.generate")(function* ( db: Database.Interface["db"], @@ -140,34 +132,12 @@ const make = (dependencies: Dependencies) => { Effect.orElseSucceed(() => firstUser.text), ) : firstUser.text - const agent = yield* dependencies.agents.get(Agent.ID.make("title")) - if (!agent) return - const primary = yield* dependencies.models.resolve(session).pipe(Effect.orElseSucceed(() => undefined)) - const info = yield* Effect.gen(function* () { - if (agent.model) return yield* dependencies.catalog.model.get(agent.model.providerID, agent.model.id) - if (!primary) return - return yield* dependencies.catalog.model.small(primary.ref.providerID) - }) - const variant = - agent.model?.variant ?? MINIMAL_REASONING_VARIANTS.find((id) => info?.variants.some((item) => item.id === id)) - const preferred = - info && - (yield* dependencies.models - .resolve({ - ...session, - model: Model.Ref.make({ - providerID: info.providerID, - id: info.id, - ...(variant ? { variant } : {}), - }), - }) - .pipe(Effect.orElseSucceed(() => undefined))) - const selected = preferred ?? primary - if (!selected) return + const selection = yield* dependencies.context.selectTitle(session) + if (!selection) return const title = - (yield* attempt(dependencies, { session, agent, text, model: selected })) ?? - (primary && !isDeepStrictEqual(selected.ref, primary.ref) - ? yield* attempt(dependencies, { session, agent, text, model: primary }) + (yield* attempt(dependencies, { session, agent: selection.agent, text, model: selection.selected })) ?? + (selection.primary && !isDeepStrictEqual(selection.selected.ref, selection.primary.ref) + ? yield* attempt(dependencies, { session, agent: selection.agent, text, model: selection.primary }) : undefined) if (!title) return const expectedSequence = (yield* Bus.latestSequence(db, sessionID)) + 1 @@ -192,13 +162,10 @@ export const layer = Layer.effect( Effect.gen(function* () { const bus = yield* Bus.Service const llm = yield* LLMClient.Service - const agents = yield* Agent.Service - const catalog = yield* Catalog.Service - const models = yield* SessionRunnerModel.Service - const modelRequests = yield* SessionModelRequest.Service + const context = yield* SessionContext.Service const store = yield* SessionStore.Service const database = yield* Database.Service - const title = make({ bus, llm, agents, catalog, models, modelRequests, store }) + const title = make({ bus, llm, context, store }) return Service.of({ generate: (sessionID) => title.generate(database.db, sessionID), }) @@ -208,14 +175,5 @@ export const layer = Layer.effect( export const node = makeLocationNode({ service: Service, layer, - deps: [ - Bus.node, - llmClient, - Agent.node, - Catalog.node, - SessionRunnerModel.node, - SessionModelRequest.node, - SessionStore.node, - Database.node, - ], + deps: [Bus.node, llmClient, SessionContext.node, SessionStore.node, Database.node], }) diff --git a/packages/core/test/config/compaction.test.ts b/packages/core/test/config/compaction.test.ts index eed605633e5..938ee7fe1a3 100644 --- a/packages/core/test/config/compaction.test.ts +++ b/packages/core/test/config/compaction.test.ts @@ -9,6 +9,7 @@ import { llmClient } from "@opencode-ai/core/effect/app-node-platform" import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" +import { SessionModelRequest } from "@opencode-ai/core/session/model-request" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { Session } from "@opencode-ai/core/session" import { Agent } from "@opencode-ai/core/agent" @@ -38,19 +39,13 @@ const config = Config.testLayer() const it = testEffect( Layer.merge( config, - AppNodeBuilder.build(LayerNode.group([SessionCompaction.node, Config.node, Bus.node]), [ + AppNodeBuilder.build(LayerNode.group([SessionCompaction.node, SessionModelRequest.node, Config.node, Bus.node]), [ [ llmClient, Layer.mock(LLMClient.Service)({ stream: () => Stream.make(LLMEvent.textDelta({ id: "summary", text: "summary" })), }), ], - [ - SessionRunnerModel.node, - Layer.mock(SessionRunnerModel.Service)({ - resolve: () => Effect.succeed(resolved), - }), - ], [Config.node, config], ]), ), @@ -59,6 +54,7 @@ describe("ConfigCompactionPlugin.Plugin", () => { it.live("merges settings and reloads changed config", () => Effect.gen(function* () { const compaction = yield* SessionCompaction.Service + const modelRequests = yield* SessionModelRequest.Service const config = yield* Config.Test const bus = yield* Bus.Service yield* config.setEntries([ @@ -85,6 +81,8 @@ describe("ConfigCompactionPlugin.Plugin", () => { expect( yield* compaction.compactManual({ session, + resolveModel: () => Effect.succeed(resolved), + prepare: modelRequests.prepare, messages: [ { id: SessionMessage.ID.create(), diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index fdaadb05426..54ffd1a2290 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -797,8 +797,10 @@ describe("LocationServiceMap", () => { }), ), ) - const failure = yield* SessionRunnerModel.Service.use((models) => - models.resolve( + const failure = yield* Effect.gen(function* () { + const catalog = yield* Catalog.Service + const models = yield* SessionRunnerModel.Service + return yield* models.resolve( Session.Info.make({ id: Session.ID.make("ses_unavailable_model"), projectID: Project.ID.global, @@ -812,8 +814,9 @@ describe("LocationServiceMap", () => { time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, location, }), - ), - ).pipe(Effect.provide(LocationServiceMap.Service.get(location)), Effect.flip) + catalog.model.available, + ) + }).pipe(Effect.provide(LocationServiceMap.Service.get(location)), Effect.flip) expect(failure).toMatchObject({ _tag: "SessionRunnerModel.ModelUnavailableError", @@ -837,8 +840,10 @@ describe("LocationServiceMap", () => { ["azure-cognitive-services", "azure"], ["google-vertex-anthropic", "google-vertex"], ] as const) { - const failure = yield* SessionRunnerModel.Service.use((models) => - models.resolve( + const failure = yield* Effect.gen(function* () { + const catalog = yield* Catalog.Service + const models = yield* SessionRunnerModel.Service + return yield* models.resolve( Session.Info.make({ id: Session.ID.make(`ses_removed_${providerID}`), projectID: Project.ID.global, @@ -852,8 +857,9 @@ describe("LocationServiceMap", () => { time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, location, }), - ), - ).pipe(Effect.provide(LocationServiceMap.Service.get(location)), Effect.flip) + catalog.model.available, + ) + }).pipe(Effect.provide(LocationServiceMap.Service.get(location)), Effect.flip) expect(failure).toMatchObject({ _tag: "SessionRunnerModel.ModelUnavailableError", @@ -905,6 +911,7 @@ describe("LocationServiceMap", () => { time: { created: DateTime.makeUnsafe(0), updated: DateTime.makeUnsafe(0) }, location, }), + catalog.model.available, ) }).pipe(Effect.provide(LocationServiceMap.Service.get(location))) diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index 1821d3bf28f..c2167485f31 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -10,6 +10,7 @@ import { EventTable } from "@opencode-ai/core/event/sql" import { SessionCompaction } from "@opencode-ai/core/session/compaction" import { SessionEvent } from "@opencode-ai/core/session/event" import { SessionMessage } from "@opencode-ai/core/session/message" +import { SessionModelRequest } from "@opencode-ai/core/session/model-request" 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" @@ -73,9 +74,6 @@ const resolved = SessionRunnerModel.resolved(model, { cost, limit: { context: 200_000, output: 32_000 }, }) -const models = Layer.mock(SessionRunnerModel.Service)({ - resolve: () => Effect.succeed(resolved), -}) const it = testEffect( AppNodeBuilder.build( LayerNode.group([ @@ -85,11 +83,11 @@ const it = testEffect( SessionStore.node, PluginHooks.node, SessionCompaction.node, + SessionModelRequest.node, ]), [ [Bus.node, Bus.configured({ persist: true })], [llmClient, client], - [SessionRunnerModel.node, models], ], ), ) @@ -242,6 +240,7 @@ it.effect("manual compaction summarizes short context instead of no-op", () => time: { created: DateTime.makeUnsafe(0) }, } const session = yield* insertSession(sessionID, { parent_id: parentID }) + const modelRequests = yield* SessionModelRequest.Service const delta = yield* bus .subscribe(SessionEvent.Compaction.Delta) @@ -250,6 +249,8 @@ it.effect("manual compaction summarizes short context instead of no-op", () => expect( yield* compaction.compactManual({ session, + resolveModel: () => Effect.succeed(resolved), + prepare: modelRequests.prepare, messages: [userMessage], inputID: SessionMessage.ID.make("msg_manual_compaction"), }), @@ -303,9 +304,12 @@ it.effect("forked session compaction reuses the fork root prompt cache key", () fork_session_id: rootID, fork_boundary: { type: "before", messageID: SessionMessage.ID.create() }, }) + const modelRequests = yield* SessionModelRequest.Service expect( yield* compaction.compactManual({ session, + resolveModel: () => Effect.succeed(resolved), + prepare: modelRequests.prepare, messages: [ { id: SessionMessage.ID.create(), @@ -336,9 +340,12 @@ it.effect("keeps session context hooks away from compaction requests", () => }), ) const session = yield* insertSession(Session.ID.make("ses_hook_compaction")) + const modelRequests = yield* SessionModelRequest.Service expect( yield* compaction.compactManual({ session, + resolveModel: () => Effect.succeed(resolved), + prepare: modelRequests.prepare, messages: [ { id: SessionMessage.ID.create(), diff --git a/packages/core/test/session-title.test.ts b/packages/core/test/session-title.test.ts index 075f3155170..3519dd50dd4 100644 --- a/packages/core/test/session-title.test.ts +++ b/packages/core/test/session-title.test.ts @@ -24,6 +24,8 @@ 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 { PluginHooks } from "@opencode-ai/core/plugin/hooks" +import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor" +import { Location } from "@opencode-ai/core/location" import { Session } from "@opencode-ai/core/session" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" @@ -127,6 +129,8 @@ const it = testEffect( [llmClient, client], [Catalog.node, catalog], [SessionRunnerModel.node, models], + [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], + [PluginSupervisor.node, Layer.mock(PluginSupervisor.Service, { flush: Effect.void })], ], ), )