import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import type { HarnessSelection } from '@mosaicstack/types'; import type { ActorTenantScope } from '../auth/session-scope.js'; import { HarnessAdapterUnavailableError, HarnessRegistry, operationError, } from './harness.registry.js'; import { HARNESS_REGISTRY } from './harness.tokens.js'; import { readContextFromScope } from './harness.dto.js'; import { HarnessSelectionRepository } from './harness-selection.repository.js'; /** * Selection logic for the Slice-Zero chat-preferences surface. It validates the * requested harness/provider/model tuple against the live catalog with NO * fallback substitution, then persists it owner-scoped. The stored selection is * only ever mutated when the tuple is valid AND available. */ @Injectable() export class HarnessSelectionService { constructor( @Inject(HARNESS_REGISTRY) private readonly registry: HarnessRegistry, private readonly repository: HarnessSelectionRepository, ) {} getSelection(scope: ActorTenantScope): HarnessSelection | null { return this.repository.get(scope); } async setSelection( scope: ActorTenantScope, selection: HarnessSelection, ): Promise { // Throws HarnessOperationError (selection_invalid / model_unavailable) with the // requested tuple echoed back unchanged. The store is untouched on any throw. await this.assertSelectionAvailable(scope, selection); return this.repository.set(scope, selection); } private async assertSelectionAvailable( scope: ActorTenantScope, selection: HarnessSelection, ): Promise { const correlationId = randomUUID(); let adapter; try { adapter = this.registry.get(selection.harnessId); } catch (error) { if (error instanceof HarnessAdapterUnavailableError) { // An unknown harness makes the whole tuple invalid — no fallback adapter. throw operationError( 'selection_invalid', 'The requested harness/provider/model tuple is not in the catalog.', selection, correlationId, ); } throw error; } const catalog = await adapter.catalog(readContextFromScope(scope)); const entry = catalog.models.find( (candidate) => candidate.harnessId === selection.harnessId && candidate.providerId === selection.providerId && candidate.modelId === selection.modelId, ); if (!entry) { // No first-row / first-provider fallback: reject the requested tuple unchanged. throw operationError( 'selection_invalid', 'The requested harness/provider/model tuple is not in the catalog.', selection, correlationId, ); } if (entry.availability === 'unavailable') { throw operationError( 'model_unavailable', 'The requested model is currently unavailable.', selection, correlationId, true, ); } } }