mirror of
https://github.com/sortedcord/omnia.git
synced 2026-09-05 05:43:47 +05:30
refactor!(runtime): Moved simulation orchestration and DB management out of GUI
This commit is contained in:
18
packages/runtime/package.json
Normal file
18
packages/runtime/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@omnia/runtime",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@omnia/actor": "workspace:*",
|
||||
"@omnia/architect": "workspace:*",
|
||||
"@omnia/core": "workspace:*",
|
||||
"@omnia/llm": "workspace:*",
|
||||
"@omnia/memory": "workspace:*",
|
||||
"@omnia/scenario": "workspace:*",
|
||||
"@omnia/voice": "workspace:*",
|
||||
"better-sqlite3": "^12.11.1"
|
||||
}
|
||||
}
|
||||
93
packages/runtime/src/alias-handoff.ts
Normal file
93
packages/runtime/src/alias-handoff.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { HandoffEngine, checkHandoffTrigger } from "@omnia/memory";
|
||||
import type { HandoffResult } from "./snapshot.js";
|
||||
import type { RuntimeSession } from "./session.js";
|
||||
|
||||
function isHandoffResult(value: unknown): value is HandoffResult {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
return Array.isArray((value as { chunks?: unknown }).chunks);
|
||||
}
|
||||
|
||||
export async function runHandoffResolution(
|
||||
session: RuntimeSession,
|
||||
): Promise<void> {
|
||||
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
|
||||
if (!worldState) throw new Error("World state lost");
|
||||
|
||||
const handoffEngine = new HandoffEngine(
|
||||
session.handoffProvider,
|
||||
session.embeddingProvider,
|
||||
session.bufferRepo,
|
||||
session.ledgerRepo,
|
||||
);
|
||||
|
||||
for (const entity of worldState.entities.values()) {
|
||||
if (!entity.isAgent) continue;
|
||||
const bufferEntries = session.bufferRepo.listForOwner(entity.id);
|
||||
const trigger = checkHandoffTrigger(
|
||||
entity,
|
||||
bufferEntries,
|
||||
worldState.clock.get(),
|
||||
session.handoffProvider.maxContext ?? 32768,
|
||||
);
|
||||
if (trigger === "none") continue;
|
||||
|
||||
const ran = await handoffEngine.runHandoff(
|
||||
entity,
|
||||
bufferEntries,
|
||||
worldState.clock.get(),
|
||||
);
|
||||
if (!ran) continue;
|
||||
|
||||
const lastResult = handoffEngine.lastResult;
|
||||
const lastCall = session.handoffProvider.lastCalls?.at(-1);
|
||||
const entityName =
|
||||
session.entities.find((item) => item.id === entity.id)?.name ?? entity.id;
|
||||
session.log.push({
|
||||
turn: session.turn,
|
||||
entityId: entity.id,
|
||||
entityName,
|
||||
narrativeProse: `Handoff triggered for ${entityName}: memories were transferred from Cognitive Buffer to Memory Ledger`,
|
||||
intents: [],
|
||||
timestamp: worldState.clock.get().toISOString(),
|
||||
isHandoff: true,
|
||||
rawPrompt: lastResult
|
||||
? {
|
||||
systemPrompt: lastResult.systemPrompt || "",
|
||||
userContext: lastResult.userContext || "",
|
||||
components: lastResult.promptComponents,
|
||||
}
|
||||
: undefined,
|
||||
usage: lastCall?.usage,
|
||||
handoffResult: isHandoffResult(lastResult?.response)
|
||||
? lastResult.response
|
||||
: isHandoffResult(lastCall?.response)
|
||||
? lastCall.response
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function runAliasResolution(
|
||||
session: RuntimeSession,
|
||||
): Promise<void> {
|
||||
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
|
||||
if (!worldState) throw new Error("World state lost");
|
||||
|
||||
const entities = Array.from(worldState.entities.values());
|
||||
for (const viewer of entities) {
|
||||
if (!viewer.isAgent || !viewer.locationId) continue;
|
||||
for (const target of entities) {
|
||||
if (
|
||||
viewer.id !== target.id &&
|
||||
target.locationId === viewer.locationId &&
|
||||
!viewer.aliases.has(target.id)
|
||||
) {
|
||||
viewer.aliases.set(
|
||||
target.id,
|
||||
await session.aliasGenerator.generate(viewer, target),
|
||||
);
|
||||
session.coreRepo.saveEntity(viewer, worldState.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/runtime/src/commands.ts
Normal file
16
packages/runtime/src/commands.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface CreateRuntimeCommand {
|
||||
scenarioPath: string;
|
||||
playEntityName?: string;
|
||||
providerInstanceId?: string;
|
||||
customName?: string;
|
||||
}
|
||||
|
||||
export interface SubmitPlayerActionCommand {
|
||||
sessionId: string;
|
||||
prose: string;
|
||||
}
|
||||
|
||||
export interface RenameRuntimeCommand {
|
||||
sessionId: string;
|
||||
name: string;
|
||||
}
|
||||
24
packages/runtime/src/errors.ts
Normal file
24
packages/runtime/src/errors.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export class RuntimeError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly code: string,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(message, options);
|
||||
this.name = "RuntimeError";
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionNotFoundError extends RuntimeError {
|
||||
constructor(sessionId: string) {
|
||||
super(`Runtime session not found: ${sessionId}`, "SESSION_NOT_FOUND");
|
||||
this.name = "SessionNotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export class ProviderUnavailableError extends RuntimeError {
|
||||
constructor(message: string) {
|
||||
super(message, "PROVIDER_UNAVAILABLE");
|
||||
this.name = "ProviderUnavailableError";
|
||||
}
|
||||
}
|
||||
15
packages/runtime/src/index.ts
Normal file
15
packages/runtime/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export * from "./commands.js";
|
||||
export * from "./errors.js";
|
||||
export * from "./providers.js";
|
||||
export * from "./runtime-service.js";
|
||||
export * from "./session.js";
|
||||
export * from "./snapshot.js";
|
||||
export * from "./persistence/types.js";
|
||||
export * from "./persistence/sqlite-session-store.js";
|
||||
export * from "./testing/runtime-fixtures.js";
|
||||
export {
|
||||
executePlayerAction,
|
||||
preparePlayerTurn,
|
||||
processNpcTurn,
|
||||
} from "./turn-executor.js";
|
||||
export { runAliasResolution, runHandoffResolution } from "./alias-handoff.js";
|
||||
163
packages/runtime/src/persistence/sqlite-session-store.ts
Normal file
163
packages/runtime/src/persistence/sqlite-session-store.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import Database from "better-sqlite3";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import type { RuntimeSession, SavedSessionState } from "../session.js";
|
||||
import type { RuntimeSnapshot } from "../snapshot.js";
|
||||
import type { SessionStore } from "./types.js";
|
||||
|
||||
const RUNTIME_META = "runtime_meta";
|
||||
const LEGACY_META = "gui_meta";
|
||||
|
||||
export class SQLiteSessionStore implements SessionStore {
|
||||
constructor(
|
||||
readonly dataDir: string = path.resolve(process.cwd(), "data"),
|
||||
) { }
|
||||
|
||||
loadState(db: Database.Database, id: string): SavedSessionState | null {
|
||||
try {
|
||||
this.ensureRuntimeTable(db);
|
||||
let row = this.readRow(db, RUNTIME_META, id);
|
||||
if (!row && this.tableExists(db, LEGACY_META)) {
|
||||
row = this.readRow(db, LEGACY_META, id);
|
||||
if (row) this.writeStateJson(db, id, row.state_json);
|
||||
}
|
||||
return row ? (JSON.parse(row.state_json) as SavedSessionState) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
save(session: RuntimeSession): void {
|
||||
const state: SavedSessionState = {
|
||||
scenarioName: session.scenarioName,
|
||||
scenarioDescription: session.scenarioDescription,
|
||||
turn: session.turn,
|
||||
maxTurns: session.maxTurns,
|
||||
entities: session.entities,
|
||||
playerEntityId: session.playerEntityId,
|
||||
entityIndex: session.entityIndex,
|
||||
status: session.status,
|
||||
error: session.error,
|
||||
waitingEntity: session.waitingEntity,
|
||||
aliasDoneForTurn: session.aliasDoneForTurn,
|
||||
log: session.log,
|
||||
providerMappings: session.providerMappings,
|
||||
};
|
||||
this.ensureRuntimeTable(session.db);
|
||||
this.writeStateJson(
|
||||
session.db,
|
||||
session.worldInstanceId,
|
||||
JSON.stringify(state),
|
||||
);
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
const dbPath = this.pathFor(id);
|
||||
if (!fs.existsSync(dbPath)) return;
|
||||
try {
|
||||
fs.unlinkSync(dbPath);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete session file ${dbPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
list(
|
||||
activeSessions: ReadonlyMap<string, RuntimeSession>,
|
||||
snapshot: (session: RuntimeSession) => RuntimeSnapshot,
|
||||
): RuntimeSnapshot[] {
|
||||
if (!fs.existsSync(this.dataDir)) return [];
|
||||
const snapshots: RuntimeSnapshot[] = [];
|
||||
const files = fs
|
||||
.readdirSync(this.dataDir)
|
||||
.filter((file) => file.startsWith("sim-") && file.endsWith(".db"));
|
||||
|
||||
for (const file of files) {
|
||||
const id = file.slice(0, -3);
|
||||
const active = activeSessions.get(id);
|
||||
if (active) {
|
||||
snapshots.push(snapshot(active));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const db = new Database(this.pathFor(id));
|
||||
const state = this.loadState(db, id);
|
||||
db.close();
|
||||
if (state) {
|
||||
snapshots.push({
|
||||
id,
|
||||
status: state.status,
|
||||
turn: state.turn,
|
||||
maxTurns: state.maxTurns,
|
||||
scenarioName: state.scenarioName,
|
||||
scenarioDescription: state.scenarioDescription,
|
||||
entities: state.entities || [],
|
||||
log: state.log || [],
|
||||
entityIndex: state.entityIndex,
|
||||
waitingEntity: state.waitingEntity,
|
||||
error: state.error,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Skip corrupt or locked session files.
|
||||
}
|
||||
}
|
||||
return snapshots.sort(
|
||||
(a, b) =>
|
||||
(Number.parseInt(b.id.replace("sim-", ""), 10) || 0) -
|
||||
(Number.parseInt(a.id.replace("sim-", ""), 10) || 0),
|
||||
);
|
||||
}
|
||||
|
||||
pathFor(id: string): string {
|
||||
return path.join(this.dataDir, `${id}.db`);
|
||||
}
|
||||
|
||||
private ensureRuntimeTable(db: Database.Database): void {
|
||||
db.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS runtime_meta (
|
||||
id TEXT PRIMARY KEY,
|
||||
state_json TEXT
|
||||
)`,
|
||||
).run();
|
||||
}
|
||||
|
||||
private tableExists(db: Database.Database, table: string): boolean {
|
||||
return Boolean(
|
||||
db
|
||||
.prepare(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
|
||||
)
|
||||
.get(table),
|
||||
);
|
||||
}
|
||||
|
||||
private readRow(
|
||||
db: Database.Database,
|
||||
table: typeof RUNTIME_META | typeof LEGACY_META,
|
||||
id: string,
|
||||
): { state_json: string } | undefined {
|
||||
return db
|
||||
.prepare(`SELECT state_json FROM ${table} WHERE id = ?`)
|
||||
.get(id) as { state_json: string } | undefined;
|
||||
}
|
||||
|
||||
private writeStateJson(
|
||||
db: Database.Database,
|
||||
id: string,
|
||||
stateJson: string,
|
||||
): void {
|
||||
db.prepare(
|
||||
`INSERT INTO runtime_meta (id, state_json)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET state_json = excluded.state_json`,
|
||||
).run(id, stateJson);
|
||||
}
|
||||
}
|
||||
|
||||
export const DATA_DIR = path.resolve(process.cwd(), "data");
|
||||
const defaultStore = new SQLiteSessionStore(DATA_DIR);
|
||||
|
||||
export const loadSessionState = defaultStore.loadState.bind(defaultStore);
|
||||
export const saveSession = defaultStore.save.bind(defaultStore);
|
||||
export const deleteSessionFile = defaultStore.delete.bind(defaultStore);
|
||||
export const listSavedSessions = defaultStore.list.bind(defaultStore);
|
||||
14
packages/runtime/src/persistence/types.ts
Normal file
14
packages/runtime/src/persistence/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type Database from "better-sqlite3";
|
||||
import type { RuntimeSession, SavedSessionState } from "../session.js";
|
||||
import type { RuntimeSnapshot } from "../snapshot.js";
|
||||
|
||||
export interface SessionStore {
|
||||
readonly dataDir: string;
|
||||
loadState(db: Database.Database, id: string): SavedSessionState | null;
|
||||
save(session: RuntimeSession): void;
|
||||
delete(id: string): void;
|
||||
list(
|
||||
activeSessions: ReadonlyMap<string, RuntimeSession>,
|
||||
snapshot: (session: RuntimeSession) => RuntimeSnapshot,
|
||||
): RuntimeSnapshot[];
|
||||
}
|
||||
101
packages/runtime/src/providers.ts
Normal file
101
packages/runtime/src/providers.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
MockLLMProvider,
|
||||
MockEmbeddingProvider,
|
||||
ProviderManager,
|
||||
buildLLMProvider,
|
||||
buildEmbeddingProvider,
|
||||
} from "@omnia/llm";
|
||||
import type {
|
||||
ILLMProvider,
|
||||
IEmbeddingProvider,
|
||||
ModelProviderInstance,
|
||||
} from "@omnia/llm";
|
||||
|
||||
export interface ResolvedProviders {
|
||||
actorProvider: ILLMProvider;
|
||||
validatorProvider: ILLMProvider;
|
||||
decoderProvider: ILLMProvider;
|
||||
timedeltaProvider: ILLMProvider;
|
||||
handoffProvider: ILLMProvider;
|
||||
embeddingProvider: IEmbeddingProvider;
|
||||
}
|
||||
|
||||
export interface ProviderResolverOptions {
|
||||
fallbackInstance?: ModelProviderInstance | null;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export function resolveProviders(
|
||||
mappings: Record<string, string>,
|
||||
options: ProviderResolverOptions = {},
|
||||
): ResolvedProviders {
|
||||
const { fallbackInstance = null, required = false } = options;
|
||||
const list = ProviderManager.list();
|
||||
const activeGenerative =
|
||||
ProviderManager.getActive("generative") ?? fallbackInstance ?? null;
|
||||
|
||||
const resolveGenerative = (task: string): ILLMProvider => {
|
||||
const mappedId = mappings[task];
|
||||
let inst: ModelProviderInstance | null = mappedId
|
||||
? (list.find((provider) => provider.id === mappedId) ?? null)
|
||||
: null;
|
||||
|
||||
if (!inst || inst.type !== "generative") inst = activeGenerative;
|
||||
if (!inst && process.env.GOOGLE_API_KEY) {
|
||||
inst = ProviderManager.create(
|
||||
"Default (Env)",
|
||||
"google-genai",
|
||||
process.env.GOOGLE_API_KEY,
|
||||
undefined,
|
||||
"generative",
|
||||
);
|
||||
}
|
||||
if (!inst) {
|
||||
if (required) {
|
||||
throw new Error(
|
||||
`No active LLM Provider Instance found for task "${task}". Please configure a key in Settings first.`,
|
||||
);
|
||||
}
|
||||
return new MockLLMProvider([]);
|
||||
}
|
||||
return buildLLMProvider(inst);
|
||||
};
|
||||
|
||||
const resolveEmbedding = (): IEmbeddingProvider => {
|
||||
const mappedId = mappings.embeddings;
|
||||
let inst: ModelProviderInstance | null = mappedId
|
||||
? (list.find((provider) => provider.id === mappedId) ?? null)
|
||||
: null;
|
||||
|
||||
if (!inst || inst.type !== "embedding") {
|
||||
inst = ProviderManager.getActive("embedding");
|
||||
}
|
||||
if (!inst && process.env.GOOGLE_API_KEY) {
|
||||
inst = ProviderManager.create(
|
||||
"Default Embed (Env)",
|
||||
"google-genai",
|
||||
process.env.GOOGLE_API_KEY,
|
||||
"gemini-embedding-001",
|
||||
"embedding",
|
||||
);
|
||||
}
|
||||
if (!inst) {
|
||||
if (required) {
|
||||
throw new Error(
|
||||
"No active Embedding Provider Instance found. Please configure an embedding key in Settings first.",
|
||||
);
|
||||
}
|
||||
return new MockEmbeddingProvider(undefined);
|
||||
}
|
||||
return buildEmbeddingProvider(inst);
|
||||
};
|
||||
|
||||
return {
|
||||
actorProvider: resolveGenerative("actor-prose"),
|
||||
validatorProvider: resolveGenerative("llm-validator"),
|
||||
decoderProvider: resolveGenerative("intent-decoder"),
|
||||
timedeltaProvider: resolveGenerative("timedelta"),
|
||||
handoffProvider: resolveGenerative("handoff"),
|
||||
embeddingProvider: resolveEmbedding(),
|
||||
};
|
||||
}
|
||||
446
packages/runtime/src/runtime-service.ts
Normal file
446
packages/runtime/src/runtime-service.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import Database from "better-sqlite3";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import { SQLiteRepository } from "@omnia/core";
|
||||
import { BufferRepository, LedgerRepository } from "@omnia/memory";
|
||||
import { Architect, AliasDeltaGenerator } from "@omnia/architect";
|
||||
import { ProviderManager, buildEmbeddingProvider } from "@omnia/llm";
|
||||
import type { ModelProviderInstance, IEmbeddingProvider } from "@omnia/llm";
|
||||
import { ScenarioLoader } from "@omnia/scenario";
|
||||
import type { RuntimeSession } from "./session.js";
|
||||
import type { EntityInfo, RuntimeSnapshot } from "./snapshot.js";
|
||||
import { resolveProviders } from "./providers.js";
|
||||
import { SQLiteSessionStore } from "./persistence/sqlite-session-store.js";
|
||||
import type { SessionStore } from "./persistence/types.js";
|
||||
import {
|
||||
preparePlayerTurn,
|
||||
processNpcTurn,
|
||||
executePlayerAction,
|
||||
} from "./turn-executor.js";
|
||||
import { runAliasResolution, runHandoffResolution } from "./alias-handoff.js";
|
||||
|
||||
export interface RuntimeServiceOptions {
|
||||
dataDir?: string;
|
||||
store?: SessionStore;
|
||||
idFactory?: () => string;
|
||||
}
|
||||
|
||||
export class RuntimeService {
|
||||
private readonly sessions = new Map<string, RuntimeSession>();
|
||||
private readonly pending = new Map<string, Promise<unknown>>();
|
||||
private readonly store: SessionStore;
|
||||
private readonly idFactory: () => string;
|
||||
private lastTimestamp = 0;
|
||||
|
||||
constructor(options: RuntimeServiceOptions = {}) {
|
||||
this.store =
|
||||
options.store ??
|
||||
new SQLiteSessionStore(
|
||||
options.dataDir ?? path.resolve(process.cwd(), "data"),
|
||||
);
|
||||
this.idFactory =
|
||||
options.idFactory ??
|
||||
(() => {
|
||||
this.lastTimestamp = Math.max(Date.now(), this.lastTimestamp + 1);
|
||||
return `sim-${this.lastTimestamp}`;
|
||||
});
|
||||
}
|
||||
|
||||
async create(
|
||||
scenarioPath: string,
|
||||
playEntityName?: string,
|
||||
providerInstanceId?: string,
|
||||
customName?: string,
|
||||
): Promise<RuntimeSnapshot> {
|
||||
let activeInstance: ModelProviderInstance | null = providerInstanceId
|
||||
? (ProviderManager.list().find((item) => item.id === providerInstanceId) ??
|
||||
null)
|
||||
: ProviderManager.getActive("generative");
|
||||
if (!activeInstance && process.env.GOOGLE_API_KEY) {
|
||||
activeInstance = ProviderManager.create(
|
||||
"Default (Env)",
|
||||
"google-genai",
|
||||
process.env.GOOGLE_API_KEY,
|
||||
undefined,
|
||||
"generative",
|
||||
);
|
||||
}
|
||||
if (!activeInstance) return this.providerErrorSnapshot();
|
||||
|
||||
const scenarioJson = JSON.parse(fs.readFileSync(scenarioPath, "utf-8"));
|
||||
const id = this.idFactory();
|
||||
fs.mkdirSync(this.store.dataDir, { recursive: true });
|
||||
const dbPath = path.join(this.store.dataDir, `${id}.db`);
|
||||
const db = new Database(dbPath);
|
||||
const coreRepo = new SQLiteRepository(db);
|
||||
const bufferRepo = new BufferRepository(db);
|
||||
const ledgerRepo = new LedgerRepository(db);
|
||||
await new ScenarioLoader(coreRepo, bufferRepo).initializeWorld(
|
||||
scenarioJson,
|
||||
id,
|
||||
);
|
||||
|
||||
const worldState = coreRepo.loadWorldState(id);
|
||||
if (!worldState) {
|
||||
db.close();
|
||||
return this.errorSnapshot("Failed to load world state after initialization.");
|
||||
}
|
||||
const rawEntities = Array.from(worldState.entities.values());
|
||||
const entities: EntityInfo[] = rawEntities.map((entity) => ({
|
||||
id: entity.id,
|
||||
name:
|
||||
(entity.attributes.get("name")?.getValue() as string | undefined) ??
|
||||
entity.id,
|
||||
isPlayer: false,
|
||||
isAgent: entity.isAgent,
|
||||
}));
|
||||
const playerEntityId = this.resolvePlayerEntity(
|
||||
rawEntities,
|
||||
entities,
|
||||
playEntityName,
|
||||
);
|
||||
const mappings = ProviderManager.getMappings();
|
||||
const providers = resolveProviders(mappings, {
|
||||
fallbackInstance: activeInstance,
|
||||
});
|
||||
const session: RuntimeSession = {
|
||||
db,
|
||||
dbPath,
|
||||
coreRepo,
|
||||
bufferRepo,
|
||||
ledgerRepo,
|
||||
worldInstanceId: id,
|
||||
scenarioName: customName || scenarioJson.name,
|
||||
scenarioDescription: scenarioJson.description || "",
|
||||
turn: 1,
|
||||
maxTurns: 20,
|
||||
entities,
|
||||
playerEntityId,
|
||||
entityIndex: 0,
|
||||
...providers,
|
||||
architect: new Architect(
|
||||
{
|
||||
validator: providers.validatorProvider,
|
||||
timedelta: providers.timedeltaProvider,
|
||||
},
|
||||
coreRepo,
|
||||
),
|
||||
aliasGenerator: new AliasDeltaGenerator(providers.actorProvider),
|
||||
log: [],
|
||||
status: "running",
|
||||
aliasDoneForTurn: false,
|
||||
providerMappings: mappings,
|
||||
};
|
||||
this.sessions.set(id, session);
|
||||
this.store.save(session);
|
||||
return this.snapshot(session);
|
||||
}
|
||||
|
||||
async load(id: string): Promise<RuntimeSnapshot | null> {
|
||||
return this.exclusive(id, async () => {
|
||||
const active = this.sessions.get(id);
|
||||
if (active) return this.snapshot(active);
|
||||
const dbPath = path.join(this.store.dataDir, `${id}.db`);
|
||||
if (!fs.existsSync(dbPath)) return null;
|
||||
|
||||
let db: Database.Database | undefined;
|
||||
try {
|
||||
db = new Database(dbPath);
|
||||
const state = this.store.loadState(db, id);
|
||||
if (!state) {
|
||||
db.close();
|
||||
return null;
|
||||
}
|
||||
const providers = resolveProviders(state.providerMappings || {}, {
|
||||
required: true,
|
||||
});
|
||||
const coreRepo = new SQLiteRepository(db);
|
||||
const bufferRepo = new BufferRepository(db);
|
||||
const ledgerRepo = new LedgerRepository(db);
|
||||
const session: RuntimeSession = {
|
||||
...state,
|
||||
db,
|
||||
dbPath,
|
||||
coreRepo,
|
||||
bufferRepo,
|
||||
ledgerRepo,
|
||||
worldInstanceId: id,
|
||||
...providers,
|
||||
architect: new Architect(
|
||||
{
|
||||
validator: providers.validatorProvider,
|
||||
timedelta: providers.timedeltaProvider,
|
||||
},
|
||||
coreRepo,
|
||||
),
|
||||
aliasGenerator: new AliasDeltaGenerator(providers.actorProvider),
|
||||
entities: state.entities || [],
|
||||
log: state.log || [],
|
||||
aliasDoneForTurn: state.aliasDoneForTurn || false,
|
||||
providerMappings: state.providerMappings || {},
|
||||
};
|
||||
this.sessions.set(id, session);
|
||||
return this.snapshot(session);
|
||||
} catch (error) {
|
||||
if (db?.open) db.close();
|
||||
console.error(`Failed to load session ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close(id: string): void {
|
||||
const session = this.sessions.get(id);
|
||||
if (session) session.db.close();
|
||||
this.sessions.delete(id);
|
||||
}
|
||||
|
||||
deleteSession(id: string): void {
|
||||
this.close(id);
|
||||
this.store.delete(id);
|
||||
}
|
||||
|
||||
listSavedSessions(): RuntimeSnapshot[] {
|
||||
return this.store.list(this.sessions, (session) => this.snapshot(session));
|
||||
}
|
||||
|
||||
getSnapshot(id: string): RuntimeSnapshot | null {
|
||||
const session = this.sessions.get(id);
|
||||
return session ? this.snapshot(session) : null;
|
||||
}
|
||||
|
||||
async rename(id: string, newName: string): Promise<RuntimeSnapshot | null> {
|
||||
if (!this.sessions.has(id)) await this.load(id);
|
||||
return this.exclusive(id, async () => {
|
||||
const session = this.sessions.get(id);
|
||||
if (!session) return null;
|
||||
session.scenarioName = newName;
|
||||
this.store.save(session);
|
||||
return this.snapshot(session);
|
||||
});
|
||||
}
|
||||
|
||||
async step(id: string): Promise<RuntimeSnapshot | null> {
|
||||
return this.exclusive(id, async () => {
|
||||
const session = this.sessions.get(id);
|
||||
if (!session) return null;
|
||||
if (session.status !== "running") return this.snapshot(session);
|
||||
try {
|
||||
if (session.turn > session.maxTurns) {
|
||||
session.status = "done";
|
||||
} else if (!session.aliasDoneForTurn && session.entityIndex === 0) {
|
||||
await runAliasResolution(session);
|
||||
await runHandoffResolution(session);
|
||||
session.aliasDoneForTurn = true;
|
||||
} else if (session.entityIndex >= session.entities.length) {
|
||||
session.turn++;
|
||||
session.entityIndex = 0;
|
||||
session.aliasDoneForTurn = false;
|
||||
} else {
|
||||
const info = session.entities[session.entityIndex];
|
||||
if (!info.isAgent) session.entityIndex++;
|
||||
else if (info.isPlayer) await preparePlayerTurn(session, info);
|
||||
else {
|
||||
await processNpcTurn(session, info);
|
||||
session.entityIndex++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
session.status = "error";
|
||||
session.error = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
this.store.save(session);
|
||||
return this.snapshot(session);
|
||||
});
|
||||
}
|
||||
|
||||
async submitPlayerAction(
|
||||
id: string,
|
||||
prose: string,
|
||||
): Promise<RuntimeSnapshot | null> {
|
||||
return this.exclusive(id, async () => {
|
||||
const session = this.sessions.get(id);
|
||||
if (!session) return null;
|
||||
if (session.status !== "waiting_player" || !session.waitingEntity) {
|
||||
return this.snapshot(session);
|
||||
}
|
||||
const context = session.waitingEntity;
|
||||
session.waitingEntity = undefined;
|
||||
session.status = "running";
|
||||
try {
|
||||
await executePlayerAction(session, context, prose);
|
||||
session.entityIndex++;
|
||||
} catch (error) {
|
||||
session.status = "error";
|
||||
session.error = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
this.store.save(session);
|
||||
return this.snapshot(session);
|
||||
});
|
||||
}
|
||||
|
||||
async regenerateAllEmbeddings(
|
||||
newProviderInstanceId?: string,
|
||||
): Promise<void> {
|
||||
if (!fs.existsSync(this.store.dataDir)) return;
|
||||
let instance = newProviderInstanceId
|
||||
? (ProviderManager.list().find((item) => item.id === newProviderInstanceId) ??
|
||||
null)
|
||||
: null;
|
||||
if (!instance || instance.type !== "embedding") {
|
||||
instance = ProviderManager.getActive("embedding");
|
||||
}
|
||||
if (!instance) {
|
||||
instance = process.env.GOOGLE_API_KEY
|
||||
? {
|
||||
id: "regen-env-fallback",
|
||||
name: "Gemini Embed (Env)",
|
||||
providerName: "google-genai",
|
||||
apiKey: process.env.GOOGLE_API_KEY,
|
||||
isActive: true,
|
||||
modelName: "gemini-embedding-001",
|
||||
type: "embedding",
|
||||
maxContext: 0,
|
||||
}
|
||||
: {
|
||||
id: "regen-mock-fallback",
|
||||
name: "Mock Embed (Fallback)",
|
||||
providerName: "mock",
|
||||
apiKey: "",
|
||||
isActive: true,
|
||||
modelName: undefined,
|
||||
type: "embedding",
|
||||
maxContext: 0,
|
||||
};
|
||||
}
|
||||
const embeddingProvider: IEmbeddingProvider = buildEmbeddingProvider(instance);
|
||||
const files = fs
|
||||
.readdirSync(this.store.dataDir)
|
||||
.filter((file) => file.startsWith("sim-") && file.endsWith(".db"));
|
||||
for (const file of files) {
|
||||
const id = file.slice(0, -3);
|
||||
const active = this.sessions.get(id);
|
||||
const db = active?.db ?? new Database(path.join(this.store.dataDir, file));
|
||||
try {
|
||||
const rows = db
|
||||
.prepare("SELECT id, content FROM ledger_entries")
|
||||
.all() as { id: string; content: string }[];
|
||||
for (const row of rows) {
|
||||
const vector = await embeddingProvider.embed(row.content);
|
||||
db.prepare("UPDATE ledger_entries SET embedding = ? WHERE id = ?").run(
|
||||
Buffer.from(new Float32Array(vector).buffer),
|
||||
row.id,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to regenerate embeddings for ${file}:`, error);
|
||||
} finally {
|
||||
if (!active) db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private snapshot(session: RuntimeSession): RuntimeSnapshot {
|
||||
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
|
||||
const entities = session.entities.map((entity) => {
|
||||
const actual = worldState?.getEntity(entity.id);
|
||||
return {
|
||||
...entity,
|
||||
aliases: actual ? Object.fromEntries(actual.aliases) : {},
|
||||
};
|
||||
});
|
||||
let currentLocation: string | undefined;
|
||||
if (
|
||||
worldState &&
|
||||
session.entityIndex >= 0 &&
|
||||
session.entityIndex < session.entities.length
|
||||
) {
|
||||
const actual = worldState.getEntity(
|
||||
session.entities[session.entityIndex].id,
|
||||
);
|
||||
currentLocation = actual?.locationId
|
||||
? worldState.getLocation(actual.locationId)?.id
|
||||
: undefined;
|
||||
}
|
||||
return {
|
||||
id: session.worldInstanceId,
|
||||
status: session.status,
|
||||
turn: session.turn,
|
||||
maxTurns: session.maxTurns,
|
||||
scenarioName: session.scenarioName,
|
||||
scenarioDescription: session.scenarioDescription,
|
||||
entities,
|
||||
log: session.log,
|
||||
entityIndex: session.entityIndex,
|
||||
waitingEntity: session.waitingEntity,
|
||||
error: session.error,
|
||||
worldTime: worldState?.clock.get().toISOString(),
|
||||
currentLocation,
|
||||
};
|
||||
}
|
||||
|
||||
private resolvePlayerEntity(
|
||||
rawEntities: Array<{
|
||||
id: string;
|
||||
attributes: Map<string, { getValue(): unknown }>;
|
||||
}>,
|
||||
entities: EntityInfo[],
|
||||
name?: string,
|
||||
): string | undefined {
|
||||
if (!name) return undefined;
|
||||
const query = name.toLowerCase();
|
||||
const matched =
|
||||
rawEntities.find((entity) => entity.id === name) ??
|
||||
rawEntities.find(
|
||||
(entity) =>
|
||||
String(entity.attributes.get("name")?.getValue()).toLowerCase() ===
|
||||
query,
|
||||
) ??
|
||||
rawEntities.find((entity) => {
|
||||
const entityName = String(
|
||||
entity.attributes.get("name")?.getValue() ?? "",
|
||||
).toLowerCase();
|
||||
return entityName.includes(query) || entity.id.toLowerCase().includes(query);
|
||||
});
|
||||
if (!matched) return undefined;
|
||||
const info = entities.find((entity) => entity.id === matched.id);
|
||||
if (info) info.isPlayer = true;
|
||||
return matched.id;
|
||||
}
|
||||
|
||||
private exclusive<T>(id: string, operation: () => Promise<T>): Promise<T> {
|
||||
const previous = this.pending.get(id) ?? Promise.resolve();
|
||||
const current = previous.catch(() => undefined).then(operation);
|
||||
this.pending.set(id, current);
|
||||
void current.then(() => {
|
||||
if (this.pending.get(id) === current) this.pending.delete(id);
|
||||
}, () => {
|
||||
if (this.pending.get(id) === current) this.pending.delete(id);
|
||||
});
|
||||
return current;
|
||||
}
|
||||
|
||||
private providerErrorSnapshot(): RuntimeSnapshot {
|
||||
return this.errorSnapshot(
|
||||
"No active LLM Provider Instance found. Please configure a key in Settings first.",
|
||||
);
|
||||
}
|
||||
|
||||
private errorSnapshot(error: string): RuntimeSnapshot {
|
||||
return {
|
||||
id: "",
|
||||
status: "error",
|
||||
turn: 0,
|
||||
maxTurns: 20,
|
||||
scenarioName: "",
|
||||
scenarioDescription: "",
|
||||
entities: [],
|
||||
log: [],
|
||||
entityIndex: 0,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use RuntimeService. */
|
||||
export class SimulationManager extends RuntimeService { }
|
||||
49
packages/runtime/src/session.ts
Normal file
49
packages/runtime/src/session.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type Database from "better-sqlite3";
|
||||
import type { SQLiteRepository } from "@omnia/core";
|
||||
import type { BufferRepository, LedgerRepository } from "@omnia/memory";
|
||||
import type { Architect, AliasDeltaGenerator } from "@omnia/architect";
|
||||
import type { ILLMProvider, IEmbeddingProvider } from "@omnia/llm";
|
||||
import type {
|
||||
EntityInfo,
|
||||
LogEntry,
|
||||
RuntimeStatus,
|
||||
WaitingContext,
|
||||
} from "./snapshot.js";
|
||||
|
||||
export interface SavedSessionState {
|
||||
scenarioName: string;
|
||||
scenarioDescription: string;
|
||||
turn: number;
|
||||
maxTurns: number;
|
||||
entities: EntityInfo[];
|
||||
playerEntityId: string | undefined;
|
||||
entityIndex: number;
|
||||
status: RuntimeStatus;
|
||||
error?: string;
|
||||
waitingEntity?: WaitingContext;
|
||||
aliasDoneForTurn: boolean;
|
||||
log: LogEntry[];
|
||||
providerMappings: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface RuntimeSession extends SavedSessionState {
|
||||
db: Database.Database;
|
||||
dbPath: string;
|
||||
coreRepo: SQLiteRepository;
|
||||
bufferRepo: BufferRepository;
|
||||
ledgerRepo: LedgerRepository;
|
||||
worldInstanceId: string;
|
||||
actorProvider: ILLMProvider;
|
||||
validatorProvider: ILLMProvider;
|
||||
decoderProvider: ILLMProvider;
|
||||
timedeltaProvider: ILLMProvider;
|
||||
handoffProvider: ILLMProvider;
|
||||
embeddingProvider: IEmbeddingProvider;
|
||||
architect: Architect;
|
||||
aliasGenerator: AliasDeltaGenerator;
|
||||
}
|
||||
|
||||
/** @deprecated Use RuntimeSession. */
|
||||
export type SimSession = RuntimeSession;
|
||||
/** @deprecated Use SavedSessionState. */
|
||||
export type SavedState = SavedSessionState;
|
||||
101
packages/runtime/src/snapshot.ts
Normal file
101
packages/runtime/src/snapshot.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export interface IntentInfo {
|
||||
type: string;
|
||||
content: string;
|
||||
modifiers: string[];
|
||||
targetIds: string[];
|
||||
isValid?: boolean;
|
||||
reason?: string;
|
||||
minutesToAdvance?: number;
|
||||
}
|
||||
|
||||
export interface PromptComponent {
|
||||
label: string;
|
||||
type: "system" | "world" | "events" | "memories" | "input" | "other";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface PromptBreakdown {
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
components?: PromptComponent[];
|
||||
}
|
||||
|
||||
export interface TokenUsage {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalTokens: number;
|
||||
modelName?: string;
|
||||
providerInstanceName?: string;
|
||||
maxContext?: number;
|
||||
}
|
||||
|
||||
export interface ValidatorCall {
|
||||
intentIndex: number;
|
||||
intentContent: string;
|
||||
prompt?: PromptBreakdown;
|
||||
response: { isValid: boolean; reason: string };
|
||||
usage?: TokenUsage;
|
||||
}
|
||||
|
||||
export interface HandoffResult {
|
||||
chunks: {
|
||||
content: string;
|
||||
importance: number;
|
||||
quotes?: string[];
|
||||
retainInBuffer?: boolean;
|
||||
involvedEntityIds?: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
turn: number;
|
||||
entityId: string;
|
||||
entityName: string;
|
||||
narrativeProse: string;
|
||||
intents: IntentInfo[];
|
||||
timestamp: string;
|
||||
isHandoff?: boolean;
|
||||
handoffResult?: HandoffResult;
|
||||
decodedIntents?: IntentInfo[];
|
||||
validatorCalls?: ValidatorCall[];
|
||||
rawPrompt?: PromptBreakdown;
|
||||
usage?: TokenUsage;
|
||||
decoderPrompt?: PromptBreakdown;
|
||||
decoderUsage?: TokenUsage;
|
||||
}
|
||||
|
||||
export interface EntityInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
isPlayer: boolean;
|
||||
isAgent: boolean;
|
||||
aliases?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface WaitingContext {
|
||||
entityId: string;
|
||||
name: string;
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
}
|
||||
|
||||
export type RuntimeStatus = "running" | "waiting_player" | "done" | "error";
|
||||
|
||||
export interface RuntimeSnapshot {
|
||||
id: string;
|
||||
status: RuntimeStatus;
|
||||
turn: number;
|
||||
maxTurns: number;
|
||||
scenarioName: string;
|
||||
scenarioDescription: string;
|
||||
entities: EntityInfo[];
|
||||
log: LogEntry[];
|
||||
entityIndex: number;
|
||||
waitingEntity?: WaitingContext;
|
||||
error?: string;
|
||||
worldTime?: string;
|
||||
currentLocation?: string;
|
||||
}
|
||||
|
||||
/** @deprecated Use RuntimeSnapshot. */
|
||||
export type SimSnapshot = RuntimeSnapshot;
|
||||
18
packages/runtime/src/testing/runtime-fixtures.ts
Normal file
18
packages/runtime/src/testing/runtime-fixtures.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { RuntimeSnapshot } from "../snapshot.js";
|
||||
|
||||
export function createRuntimeSnapshot(
|
||||
overrides: Partial<RuntimeSnapshot> = {},
|
||||
): RuntimeSnapshot {
|
||||
return {
|
||||
id: "sim-test",
|
||||
status: "running",
|
||||
turn: 1,
|
||||
maxTurns: 20,
|
||||
scenarioName: "Test scenario",
|
||||
scenarioDescription: "",
|
||||
entities: [],
|
||||
log: [],
|
||||
entityIndex: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
258
packages/runtime/src/turn-executor.ts
Normal file
258
packages/runtime/src/turn-executor.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import {
|
||||
ActorAgent,
|
||||
ActorPromptBuilder,
|
||||
buildBufferEntryForIntent,
|
||||
} from "@omnia/actor";
|
||||
import type { IActorProseGenerator } from "@omnia/actor";
|
||||
import type { RuntimeSession } from "./session.js";
|
||||
import type {
|
||||
EntityInfo,
|
||||
IntentInfo,
|
||||
LogEntry,
|
||||
WaitingContext,
|
||||
ValidatorCall,
|
||||
} from "./snapshot.js";
|
||||
|
||||
class FixedProseGenerator implements IActorProseGenerator {
|
||||
constructor(private readonly prose: string) { }
|
||||
|
||||
async generate(): Promise<string> {
|
||||
return this.prose;
|
||||
}
|
||||
}
|
||||
|
||||
type RuntimeIntent = Parameters<typeof buildBufferEntryForIntent>[0];
|
||||
|
||||
async function processIntents(
|
||||
intents: RuntimeIntent[],
|
||||
actorEntityId: string,
|
||||
entity: { locationId: string | null },
|
||||
worldState: NonNullable<ReturnType<RuntimeSession["coreRepo"]["loadWorldState"]>>,
|
||||
session: RuntimeSession,
|
||||
): Promise<{ intentInfos: IntentInfo[]; validatorCalls: ValidatorCall[] }> {
|
||||
const intentInfos: IntentInfo[] = [];
|
||||
const validatorCalls: ValidatorCall[] = [];
|
||||
|
||||
for (const [intentIndex, intent] of intents.entries()) {
|
||||
const outcome = await session.architect.processIntent(worldState, intent);
|
||||
const timestamp = worldState.clock.get().toISOString();
|
||||
intentInfos.push({
|
||||
type: intent.type,
|
||||
content: intent.content,
|
||||
modifiers: intent.modifiers || [],
|
||||
targetIds: intent.targetIds,
|
||||
isValid: outcome.isValid,
|
||||
reason: outcome.reason,
|
||||
minutesToAdvance: outcome.timeDelta?.minutesToAdvance,
|
||||
});
|
||||
|
||||
if (intent.type === "action" && session.architect.validator.lastResult) {
|
||||
const result = session.architect.validator.lastResult;
|
||||
validatorCalls.push({
|
||||
intentIndex,
|
||||
intentContent: intent.content,
|
||||
prompt: {
|
||||
systemPrompt: result.systemPrompt || "",
|
||||
userContext: result.userContext || "",
|
||||
components: result.components,
|
||||
},
|
||||
response: { isValid: outcome.isValid, reason: outcome.reason },
|
||||
usage: session.validatorProvider.lastCalls?.at(-1)?.usage,
|
||||
});
|
||||
} else {
|
||||
const reason =
|
||||
intent.type === "dialogue"
|
||||
? "Dialogue intents represent verbal/communication actions and are automatically valid."
|
||||
: "Monologue/thought intents represent internal reflections and bypass validation.";
|
||||
validatorCalls.push({
|
||||
intentIndex,
|
||||
intentContent: intent.content,
|
||||
response: {
|
||||
isValid: true,
|
||||
reason: outcome.reason || reason,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const actorEntry = buildBufferEntryForIntent(
|
||||
intent,
|
||||
timestamp,
|
||||
entity.locationId,
|
||||
);
|
||||
if (intent.type === "action") {
|
||||
actorEntry.outcome = { isValid: outcome.isValid, reason: outcome.reason };
|
||||
}
|
||||
session.bufferRepo.save(actorEntry);
|
||||
|
||||
if (
|
||||
entity.locationId &&
|
||||
(intent.type === "dialogue" || intent.type === "action")
|
||||
) {
|
||||
for (const other of worldState.entities.values()) {
|
||||
if (
|
||||
other.id === actorEntityId ||
|
||||
other.locationId !== entity.locationId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const observerEntry = buildBufferEntryForIntent(
|
||||
intent,
|
||||
timestamp,
|
||||
entity.locationId,
|
||||
);
|
||||
if (intent.type === "action") {
|
||||
observerEntry.outcome = {
|
||||
isValid: outcome.isValid,
|
||||
reason: outcome.reason,
|
||||
};
|
||||
}
|
||||
session.bufferRepo.save({ ...observerEntry, ownerId: other.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
return { intentInfos, validatorCalls };
|
||||
}
|
||||
|
||||
function attachDecoderDetails(
|
||||
session: RuntimeSession,
|
||||
entry: LogEntry,
|
||||
): void {
|
||||
const call = session.decoderProvider.lastCalls?.at(-1);
|
||||
if (!call) return;
|
||||
const proseHeader = "=== NARRATIVE PROSE ===";
|
||||
const index = call.userContext.indexOf(proseHeader);
|
||||
const context =
|
||||
index === -1 ? call.userContext : call.userContext.substring(0, index).trim();
|
||||
const prose = index === -1 ? "" : call.userContext.substring(index).trim();
|
||||
entry.decoderPrompt = {
|
||||
systemPrompt: call.systemPrompt,
|
||||
userContext: call.userContext,
|
||||
components: [
|
||||
{ label: "System Prompt", type: "system", content: call.systemPrompt },
|
||||
{ label: "Decoder Context", type: "world", content: context },
|
||||
{ label: "Narrative Prose", type: "input", content: prose },
|
||||
],
|
||||
};
|
||||
entry.decoderUsage = call.usage;
|
||||
}
|
||||
|
||||
export async function preparePlayerTurn(
|
||||
session: RuntimeSession,
|
||||
info: EntityInfo,
|
||||
): Promise<void> {
|
||||
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
|
||||
if (!worldState) throw new Error("World state lost");
|
||||
const entity = worldState.getEntity(info.id);
|
||||
if (!entity) throw new Error(`Entity "${info.id}" not found`);
|
||||
const prompt = new ActorPromptBuilder(
|
||||
session.bufferRepo,
|
||||
session.ledgerRepo,
|
||||
20,
|
||||
).build(worldState, entity);
|
||||
session.waitingEntity = {
|
||||
entityId: info.id,
|
||||
name: info.name,
|
||||
systemPrompt: prompt.systemPrompt,
|
||||
userContext: prompt.userContext,
|
||||
};
|
||||
session.status = "waiting_player";
|
||||
}
|
||||
|
||||
export async function processNpcTurn(
|
||||
session: RuntimeSession,
|
||||
info: EntityInfo,
|
||||
): Promise<void> {
|
||||
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
|
||||
if (!worldState) throw new Error("World state lost");
|
||||
const entity = worldState.getEntity(info.id);
|
||||
if (!entity) throw new Error(`Entity "${info.id}" not found`);
|
||||
|
||||
const result = await new ActorAgent(
|
||||
{ actor: session.actorProvider, decoder: session.decoderProvider },
|
||||
session.bufferRepo,
|
||||
session.ledgerRepo,
|
||||
20,
|
||||
).act(worldState, entity);
|
||||
const entry: LogEntry = {
|
||||
turn: session.turn,
|
||||
entityId: info.id,
|
||||
entityName: info.name,
|
||||
narrativeProse: result.narrativeProse,
|
||||
intents: [],
|
||||
timestamp: worldState.clock.get().toISOString(),
|
||||
rawPrompt: {
|
||||
systemPrompt: result.systemPrompt || "",
|
||||
userContext: result.userContext || "",
|
||||
components: result.promptComponents,
|
||||
},
|
||||
usage: session.actorProvider.lastCalls?.at(-1)?.usage,
|
||||
};
|
||||
attachDecoderDetails(session, entry);
|
||||
const processed = await processIntents(
|
||||
result.intents.intents,
|
||||
info.id,
|
||||
entity,
|
||||
worldState,
|
||||
session,
|
||||
);
|
||||
entry.intents = processed.intentInfos;
|
||||
entry.validatorCalls = processed.validatorCalls;
|
||||
entry.decodedIntents = result.intents.intents.map((intent) => ({
|
||||
type: intent.type,
|
||||
content: intent.content,
|
||||
modifiers: intent.modifiers || [],
|
||||
targetIds: intent.targetIds,
|
||||
}));
|
||||
session.log.push(entry);
|
||||
session.coreRepo.saveWorldState(worldState);
|
||||
}
|
||||
|
||||
export async function executePlayerAction(
|
||||
session: RuntimeSession,
|
||||
context: WaitingContext,
|
||||
prose: string,
|
||||
): Promise<void> {
|
||||
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
|
||||
if (!worldState) throw new Error("World state lost");
|
||||
const entity = worldState.getEntity(context.entityId);
|
||||
if (!entity) throw new Error(`Player entity "${context.entityId}" not found`);
|
||||
|
||||
const result = await new ActorAgent(
|
||||
{ actor: session.actorProvider, decoder: session.decoderProvider },
|
||||
session.bufferRepo,
|
||||
session.ledgerRepo,
|
||||
20,
|
||||
new FixedProseGenerator(prose),
|
||||
).act(worldState, entity);
|
||||
const entry: LogEntry = {
|
||||
turn: session.turn,
|
||||
entityId: context.entityId,
|
||||
entityName: context.name,
|
||||
narrativeProse: result.narrativeProse,
|
||||
intents: [],
|
||||
timestamp: worldState.clock.get().toISOString(),
|
||||
rawPrompt: {
|
||||
systemPrompt: result.systemPrompt || context.systemPrompt,
|
||||
userContext: result.userContext || context.userContext,
|
||||
components: result.promptComponents,
|
||||
},
|
||||
};
|
||||
attachDecoderDetails(session, entry);
|
||||
const processed = await processIntents(
|
||||
result.intents.intents,
|
||||
context.entityId,
|
||||
entity,
|
||||
worldState,
|
||||
session,
|
||||
);
|
||||
entry.intents = processed.intentInfos;
|
||||
entry.validatorCalls = processed.validatorCalls;
|
||||
entry.decodedIntents = result.intents.intents.map((intent) => ({
|
||||
type: intent.type,
|
||||
content: intent.content,
|
||||
modifiers: intent.modifiers || [],
|
||||
targetIds: intent.targetIds,
|
||||
}));
|
||||
session.log.push(entry);
|
||||
session.coreRepo.saveWorldState(worldState);
|
||||
}
|
||||
33
packages/runtime/tsconfig.json
Normal file
33
packages/runtime/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../actor"
|
||||
},
|
||||
{
|
||||
"path": "../architect"
|
||||
},
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../llm"
|
||||
},
|
||||
{
|
||||
"path": "../memory"
|
||||
},
|
||||
{
|
||||
"path": "../scenario"
|
||||
},
|
||||
{
|
||||
"path": "../voice"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user