fix(core): harden model selection edges (#30987)
This commit is contained in:
@@ -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(
|
yield* events.subscribe(PluginV2.Event.Added).pipe(
|
||||||
// Plugin registries are location scoped even though the event bus is process scoped.
|
// 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* () {
|
available: Effect.fn("CatalogV2.model.available")(function* () {
|
||||||
return (yield* result.model.all()).filter((model) => {
|
return (yield* result.model.all()).filter(available)
|
||||||
const record = state.get().providers.get(model.providerID)
|
|
||||||
return record?.provider.enabled !== false && model.enabled
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
default: Effect.fn("CatalogV2.model.default")(function* () {
|
default: Effect.fn("CatalogV2.model.default")(function* () {
|
||||||
const defaultModel = state.get().defaultModel
|
const defaultModel = state.get().defaultModel
|
||||||
if (defaultModel) {
|
if (defaultModel) {
|
||||||
const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
|
const provider = state.get().providers.get(defaultModel.providerID)?.provider
|
||||||
if (Option.isSome(model) && model.value.enabled) return model
|
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(
|
return pipe(
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Ses
|
|||||||
export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
|
export class OperationUnavailableError extends Schema.TaggedErrorClass<OperationUnavailableError>()(
|
||||||
"Session.OperationUnavailableError",
|
"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" })
|
return yield* new OperationUnavailableError({ operation: "switchAgent" })
|
||||||
}),
|
}),
|
||||||
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
|
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
|
||||||
const session = yield* result.get(input.sessionID)
|
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* events.publish(SessionEvent.ModelSwitched, {
|
yield* events.publish(SessionEvent.ModelSwitched, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
messageID: SessionMessage.ID.create(),
|
messageID: SessionMessage.ID.create(),
|
||||||
|
|||||||
@@ -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", () =>
|
it.effect("small model prefers small keyword candidates before cost scoring", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
|
|||||||
@@ -356,12 +356,23 @@ describe("SessionV2.create", () => {
|
|||||||
expect(
|
expect(
|
||||||
Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(1), Stream.runCollect)),
|
Array.from(yield* session.events({ sessionID: created.id }).pipe(Stream.take(1), Stream.runCollect)),
|
||||||
).toMatchObject([{ event: { type: "session.next.model.switched", data: { model } } }])
|
).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 })
|
||||||
|
yield* session.switchModel({ sessionID: created.id, model })
|
||||||
|
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
expect(
|
expect(
|
||||||
yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, created.id)).all().pipe(Effect.orDie),
|
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 })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user