import type { z } from 'zod'; import type { Draft } from 'immer'; import { collection } from '#/_base/di/collection'; import { BugIndicatingError } from '#/_base/errors/errors'; import type { StateKey } from '#/_base/state/stateRegistry'; import type { AgentContext } from '#/agent/agentContext/agentContext'; import { registerEvent2Class, type Event2, type Event2Class } from '#/app/event/event2'; import type { FoldContext } from './state'; export interface DomainResourceRuntime { dispose(): void | Promise; abort?(reason?: unknown): void; } export interface AgentModelBridge { dispatch(event: Event2): Promise; readLegacy(key: StateKey): unknown; initialState(): unknown; } export interface AgentModelContext { readonly agent: AgentContext; readonly bridge: AgentModelBridge; } interface ModelWindow { readonly draft: unknown; readonly ctx: FoldContext; replaced: boolean; replacement: unknown; } export abstract class AgentModel implements DomainResourceRuntime { private committedState: S; private window: ModelWindow | undefined; private readonly appliers = new Map, (event: any) => void>(); private sealed = false; readonly agent: AgentContext; private readonly bridge: AgentModelBridge; constructor(context: AgentModelContext) { this.agent = context.agent; this.bridge = context.bridge; this.committedState = context.bridge.initialState() as S; } protected get state(): Draft { const window = this.window; return (window !== undefined ? window.draft : this.committedState) as Draft; } protected set state(value: S) { if (this.window === undefined) { throw new BugIndicatingError( `Model '${this.constructor.name}' can only replace state inside an applier`, ); } this.window.replaced = true; this.window.replacement = value; } protected on>(cls: Event2Class, applier: (event: E) => void): void { if (this.sealed) { throw new BugIndicatingError( `Model '${this.constructor.name}' cannot register appliers after construction`, ); } if (this.appliers.has(cls)) { throw new BugIndicatingError( `Model '${this.constructor.name}' already applies event '${cls.type}'`, ); } this.appliers.set(cls, applier as (event: any) => void); } protected emit(event: Event2): Promise { const window = this.window; if (window !== undefined) { window.ctx.emit(event); return Promise.resolve(); } return this.bridge.dispatch(event); } protected readLegacy(key: StateKey): T { return this.bridge.readLegacy(key) as T; } onUndo?(count: number): void; dispose(): void | Promise {} _seal(): void { this.sealed = true; } _appliersTable(): ReadonlyMap, (event: any) => void> { return this.appliers; } _state(): S { return this.committedState; } _commitState(next: S): void { this.committedState = next; } _enterWindow(draft: S, ctx: FoldContext): void { this.window = { draft, ctx, replaced: false, replacement: undefined }; } _exitWindow(): { readonly replaced: boolean; readonly replacement: unknown } { const window = this.window; this.window = undefined; return { replaced: window?.replaced ?? false, replacement: window?.replacement }; } } export interface AgentModelStateSpec { readonly initial: () => S; readonly schema: z.ZodType; } export interface AgentModelDefinition = AgentModel> { readonly id: string; readonly model: new (context: AgentModelContext) => M; readonly state: AgentModelStateSpec; readonly events: readonly Event2Class[]; readonly undoable: boolean; } export interface AgentModelDefinitionInput> { readonly id: string; readonly model: new (context: AgentModelContext) => M; readonly state: AgentModelStateSpec; readonly events: readonly Event2Class[]; readonly undoable?: boolean; } const AGENT_MODEL_DEFINITIONS = new Map>(); export function defineAgentModel>( input: AgentModelDefinitionInput, ): AgentModelDefinition { if (AGENT_MODEL_DEFINITIONS.has(input.id)) { throw new BugIndicatingError(`Agent model '${input.id}' is already defined`); } for (const cls of input.events) { if (!cls.durable) { throw new BugIndicatingError( `Agent model '${input.id}' cannot apply non-durable event '${cls.type}'`, ); } registerEvent2Class(cls); } const definition: AgentModelDefinition = Object.freeze({ id: input.id, model: input.model, state: input.state, events: Object.freeze([...input.events]), undoable: input.undoable ?? false, }); AGENT_MODEL_DEFINITIONS.set(definition.id, definition); return definition; } export function agentModelDefinitions(): readonly AgentModelDefinition[] { return [...AGENT_MODEL_DEFINITIONS.values()]; } export const AgentModelContribution = collection>('agent-model', { validate: (value, existing) => { if (existing.some((definition) => definition.id === value.id)) { throw new Error(`Agent model '${value.id}' already has an active provider`); } }, }); export interface SessionModelDefinition { readonly id: string; readonly state: AgentModelStateSpec; readonly events: readonly Event2Class[]; readonly undoable: boolean; } export const SessionModelContribution = collection('session-model', { validate: (value, existing) => { if (existing.some((definition) => definition.id === value.id)) { throw new Error(`Session model '${value.id}' already has an active provider`); } }, });