File size: 5,467 Bytes
68d7816 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | import { BugIndicatingError } from '#/_base/errors/errors';
import { onUnexpectedError } from '#/_base/errors/unexpectedError';
import type { StateKey } from '#/_base/state/stateRegistry';
import type { Event2 } from '#/app/event/event2';
import {
type AgentModel,
type AgentModelBridge,
type AgentModelDefinition,
} from '#/state/agentModel';
import type { AgentContext } from './agentContext';
export type AgentModelInstanceOf<D> = D extends AgentModelDefinition<any, infer M> ? M : never;
export interface AgentSpace {
use<D extends AgentModelDefinition<any, any>, R>(
definition: D,
run: (model: AgentModelInstanceOf<D>) => R,
): R;
}
export interface AgentSpaceHost {
isActiveModelDefinition(definition: AgentModelDefinition<any, any>): boolean;
registerModel(definition: AgentModelDefinition<any, any>, model: AgentModel<any>): void;
dispatchModelEvent(event: Event2<any>): Promise<void>;
readLegacyState(key: StateKey<any>): unknown;
}
interface ModelEntry {
readonly definition: AgentModelDefinition<any, any>;
readonly model: AgentModel<any>;
leases: number;
retired: boolean;
disposed: boolean;
}
export class AgentSpaceImpl implements AgentSpace {
private readonly instances = new Map<AgentModelDefinition<any, any>, ModelEntry>();
private host: AgentSpaceHost | undefined;
private context: AgentContext | undefined;
private dead = false;
constructor(private readonly agentId: string) {}
_bindContext(context: AgentContext): void {
this.context = context;
}
_attachHost(host: AgentSpaceHost): void {
this.host = host;
}
_detachHost(host: AgentSpaceHost): void {
if (this.host === host) this.host = undefined;
}
use<D extends AgentModelDefinition<any, any>, R>(
definition: D,
run: (model: AgentModelInstanceOf<D>) => R,
): R {
const entry = this.ensureModel(definition);
entry.leases += 1;
let result: R;
try {
result = run(entry.model as AgentModelInstanceOf<D>);
} catch (error) {
this.release(entry);
throw error;
}
if (result instanceof Promise) {
return result.finally(() => {
this.release(entry);
}) as R;
}
this.release(entry);
return result;
}
ensureModel(definition: AgentModelDefinition<any, any>): ModelEntry {
const existing = this.instances.get(definition);
if (existing !== undefined) return existing;
if (this.dead) {
throw new Error(`Agent ${this.agentId} space is disposed`);
}
const host = this.host;
if (host === undefined) {
throw new BugIndicatingError(`Agent ${this.agentId} space has no model host`);
}
if (!host.isActiveModelDefinition(definition)) {
throw new Error(`Model definition '${definition.id}' is unavailable`);
}
const context = this.context;
if (context === undefined) {
throw new BugIndicatingError(`Agent ${this.agentId} space is not bound to a context`);
}
const bridge: AgentModelBridge = {
dispatch: (event) => host.dispatchModelEvent(event),
readLegacy: (key) => host.readLegacyState(key),
initialState: () => Object.freeze(definition.state.initial()),
};
const model = new definition.model({ agent: context, bridge });
model._seal();
validateApplierCoverage(definition, model);
const entry: ModelEntry = { definition, model, leases: 0, retired: false, disposed: false };
this.instances.set(definition, entry);
host.registerModel(definition, model);
return entry;
}
retireModel(definition: AgentModelDefinition<any, any>): void {
const entry = this.instances.get(definition);
if (entry === undefined) return;
this.instances.delete(definition);
entry.retired = true;
if (entry.leases === 0) this.disposeEntry(entry);
}
_kill(): void {
if (this.dead) return;
this.dead = true;
const entries = [...this.instances.values()];
this.instances.clear();
for (const entry of entries) {
entry.retired = true;
if (entry.leases === 0) this.disposeEntry(entry);
}
}
private release(entry: ModelEntry): void {
entry.leases -= 1;
if (entry.leases === 0 && entry.retired) this.disposeEntry(entry);
}
private disposeEntry(entry: ModelEntry): void {
if (entry.disposed) return;
entry.disposed = true;
try {
const result = entry.model.dispose();
if (result instanceof Promise) {
result.catch((error: unknown) => onUnexpectedError(error));
}
} catch (error) {
onUnexpectedError(error);
}
}
}
function validateApplierCoverage(
definition: AgentModelDefinition<any, any>,
model: AgentModel<any>,
): void {
const registered = model._appliersTable();
for (const cls of definition.events) {
if (!registered.has(cls)) {
throw new BugIndicatingError(
`Agent model '${definition.id}' does not apply declared event '${cls.type}'`,
);
}
}
for (const cls of registered.keys()) {
if (!definition.events.includes(cls)) {
throw new BugIndicatingError(
`Agent model '${definition.id}' applies undeclared event '${cls.type}'`,
);
}
}
}
export function agentSpaceOf(agent: AgentContext): AgentSpace {
const space = (agent as { readonly space?: AgentSpace }).space;
if (space === undefined) {
throw new Error(
`Agent ${agent.agentId}:${String(agent.generation)} is not a lifecycle-issued context`,
);
}
return space;
}
|