diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index e7103627a49f..b4d3e040389e 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -199,6 +199,8 @@ export const layer = Layer.effect( } }), }) + const available = (model: ModelV2.Info) => + state.get().providers.get(model.providerID)?.provider.enabled !== false && model.enabled yield* events.subscribe(PluginV2.Event.Added).pipe( // Plugin registries are location scoped even though the event bus is process scoped. @@ -250,17 +252,17 @@ export const layer = Layer.effect( }), available: Effect.fn("CatalogV2.model.available")(function* () { - return (yield* result.model.all()).filter((model) => { - const record = state.get().providers.get(model.providerID) - return record?.provider.enabled !== false && model.enabled - }) + return (yield* result.model.all()).filter(available) }), default: Effect.fn("CatalogV2.model.default")(function* () { const defaultModel = state.get().defaultModel if (defaultModel) { - const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option) - if (Option.isSome(model) && model.value.enabled) return model + const provider = state.get().providers.get(defaultModel.providerID)?.provider + if (provider?.enabled !== false) { + const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option) + if (Option.isSome(model) && available(model.value)) return model + } } return pipe( diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 9d3c76618f90..8ea45933041b 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -88,7 +88,7 @@ export class NotFoundError extends Schema.TaggedErrorClass()("Ses export class OperationUnavailableError extends Schema.TaggedErrorClass()( "Session.OperationUnavailableError", { - operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "switchModel", "compact", "wait"]), + operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact", "wait"]), }, ) {} @@ -386,13 +386,7 @@ export const layer = Layer.effect( return yield* new OperationUnavailableError({ operation: "switchAgent" }) }), switchModel: Effect.fn("V2Session.switchModel")(function* (input) { - const session = yield* result.get(input.sessionID) - if ( - session.model?.providerID === input.model.providerID && - session.model.id === input.model.id && - (session.model.variant ?? "default") === (input.model.variant ?? "default") - ) - return + yield* result.get(input.sessionID) yield* events.publish(SessionEvent.ModelSwitched, { sessionID: input.sessionID, messageID: SessionMessage.ID.create(), diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 14811d67ce0f..ed2bd42c4072 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -281,6 +281,34 @@ 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 transform = yield* catalog.transform() + + yield* transform((catalog) => { + catalog.provider.update(disabledProvider, (provider) => { + provider.enabled = false + }) + catalog.model.update(disabledProvider, disabledModel, () => {}) + catalog.provider.update(enabledProvider, (provider) => { + provider.enabled = { via: "custom", data: {} } + }) + catalog.model.update(enabledProvider, fallbackModel, () => {}) + catalog.model.default.set(disabledProvider, disabledModel) + }) + + expect(Option.getOrUndefined(yield* catalog.model.default())).toMatchObject({ + providerID: enabledProvider, + id: fallbackModel, + }) + }), + ) + it.effect("small model prefers small keyword candidates before cost scoring", () => Effect.gen(function* () { const catalog = yield* Catalog.Service diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index da4ccac73528..3551ec52f3cb 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -356,12 +356,23 @@ describe("SessionV2.create", () => { expect( Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(1), Stream.runCollect)), ).toMatchObject([{ event: { type: "session.next.model.switched", data: { model } } }]) + }), + ) + it.effect("persists repeated switches as distinct durable Session events", () => + Effect.gen(function* () { + const session = yield* SessionV2.Service + const created = yield* session.create({ location }) + const model = ModelV2.Ref.make({ id: ModelV2.ID.make("sonnet"), providerID: ProviderV2.ID.anthropic }) + + yield* session.switchModel({ sessionID: created.id, model }) yield* session.switchModel({ sessionID: created.id, model }) + const { db } = yield* Database.Service expect( yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie), - ).toHaveLength(2) + ).toHaveLength(3) + expect(yield* session.get(created.id)).toMatchObject({ model }) }), )