diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore deleted file mode 100644 index d20c0fe..0000000 --- a/.codegraph/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# CodeGraph data files — local to each machine, not for committing. -# Ignore everything in .codegraph/ except this file itself, so transient -# files (the database, daemon.pid, sockets, logs) never show up in git. -* -!.gitignore diff --git a/.gitignore b/.gitignore index d8fef40..4bab504 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ data/ .vercel __local_notes/ +omnia.code-workspace diff --git a/.rsyncignore b/.rsyncignore deleted file mode 100644 index fdce114..0000000 --- a/.rsyncignore +++ /dev/null @@ -1,65 +0,0 @@ -# Git metadata -.git/ -.gitignore -.gitattributes - -# Dependencies and package managers -node_modules/ -.pnpm-store/ -.pnp -.pnp.* -.yarn/ -.yarn-cache/ - -# Build and generated output -dist/ -dist-ssr/ -build/ -coverage/ -.next/ -.astro/ -out/ -*.tsbuildinfo -.turbo/ -.cache/ - -# Logs and temporary files -*.log -logs/ -npm-debug.log* -yarn-debug.log* -pnpm-debug.log* -.pnpm-debug.log* -lerna-debug.log* -*.swp -*.swo -*.swn -*.tmp -*.temp - -# Environment and local config -.env -.env* -*.local - -# OS/editor files -.DS_Store -Thumbs.db -.idea/ -.vscode/ -*.suo -*.sln -*.ntvs* -*.njsproj - -# Databases and local data -*.db -*.sqlite -*.sqlite3 -*.db-journal -*.db-wal -*.db-shm -omnia.db - -# Local notes and generated artifacts -__local_notes/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19b6d48..6efa15a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,28 +26,35 @@ Omnia is organized as a monorepo managed with **pnpm** workspaces. ### Prerequisites - **Node.js** (v22.13 or newer recommended) -- **pnpm** (v11 or newer recommended) +- **pnpm** (v11.15.1) ### Local Setup 1. Fork the repository and clone your fork: ```bash - git clone https://github.com/YOUR_USERNAME/omnia-consolidated.git - cd omnia-consolidated + git clone https://github.com/YOUR_USERNAME/omnia.git + cd omnia ``` -2. Install dependencies: +2. Install dependencies and compile the workspace packages: ```bash - pnpm install + pnpm install --frozen-lockfile + pnpm build ``` 3. Run the Web GUI interface locally: ```bash pnpm dev:gui ``` + The Next.js server hosts `@omnia/runtime`; no separate backend process is + required. 4. Run the Starlight documentation site locally: ```bash pnpm dev:docs ``` +When editing packages, run `pnpm watch` and `pnpm dev:gui` in separate +terminals. The initial `pnpm build` is still required because workspace package +exports resolve to compiled files under `dist/`. + ## Development Workflow ### Branching @@ -72,6 +79,13 @@ pnpm test pnpm test:watch ``` +Before submitting a change, also verify the package and production GUI builds: + +```bash +pnpm build +pnpm build:gui +``` + ### Linting and Formatting We enforce consistent code quality and formatting rules across the repository. @@ -94,6 +108,6 @@ pnpm format ## Pull Request Guidelines 1. **Keep PRs Focused**: Keep your changes as small and focused as possible. -2. **Include Tests**: If you are introducing a new feature or fixing a bug, write corresponding tests in `tests/`. +2. **Include Tests**: Add package unit tests under `packages//tests`, or cross-package tests under `tests/integration`, as appropriate. 3. **Update Documentation**: If your changes alter public behavior or introduce new APIs, update the docs under `web/docs/src/content/docs/`. 4. **Follow Commit Conventions**: Write clear, descriptive commit messages. diff --git a/README.md b/README.md index cc33661..0e5924f 100644 --- a/README.md +++ b/README.md @@ -61,25 +61,26 @@ The general principle: **anything that must remain true is state; the model only ### Prerequisites -- [Node.js](https://nodejs.org/) (v20+ recommended) -- [pnpm](https://pnpm.io/) (v9+ recommended) +- [Node.js](https://nodejs.org/) (v22.13 or newer recommended) +- [pnpm](https://pnpm.io/) (v11.15.1) - An API key for Google Gemini (`GOOGLE_API_KEY` environment variable), or configured settings via the GUI. ### Installation 1. Clone the repository: ```bash - git clone https://github.com/sortedcord/omnia-consolidated.git - cd omnia-consolidated + git clone https://github.com/sortedcord/omnia.git + cd omnia ``` -2. Install dependencies: +2. Install dependencies and compile the workspace packages: ```bash - pnpm install + pnpm install --frozen-lockfile + pnpm build ``` ### Running the Web GUI -To launch the Next.js development server for the GUI dashboard: +After compiling the workspace packages, launch the Next.js application: ```bash pnpm dev:gui @@ -87,6 +88,47 @@ pnpm dev:gui Access the application locally at `http://localhost:3000`. +The Next.js server hosts `@omnia/runtime`, which owns simulation sessions, turn +execution, provider routing, and simulation persistence. There is no separate +backend process to start. + +When changing code under `packages/`, keep the package compiler running in a +second terminal while the GUI development server runs: + +```bash +pnpm watch +``` + +```bash +pnpm dev:gui +``` + +For a production build, compile the packages before building and starting the +GUI: + +```bash +pnpm build +pnpm build:gui +pnpm --filter @omnia/gui start +``` + +### Running with Docker + +Build and run the production application with Docker Compose: + +```bash +GOOGLE_API_KEY=your-api-key docker compose up --build +``` + +For the bind-mounted development environment, use: + +```bash +GOOGLE_API_KEY=your-api-key docker compose -f docker-compose.dev.yml up --build +``` + +In either case, open `http://localhost:3000`. The API key can be omitted when +providers will be configured through the GUI. + ## Core Architecture ### The Actor Agent @@ -158,11 +200,11 @@ The finish line for the first milestone is small on purpose. `v0` is almost on t - [x] Actor Agent with epistemically-bounded prompts (self, memory, co-located entities, subjective time). - [x] Verbatim Cognitive Buffer with per-observer subjective serialization and alias resolution. - [x] Spatial location graph (data model; perception is co-location only). -- [x] Scenario loader (JSON → SQLite) and a playable CLI loop with human or LLM actors. +- [x] Scenario loader (JSON → SQLite) and a runtime-driven simulation loop exposed through the GUI. **[The `v0` Milestone:](https://github.com/sortedcord/omnia-consolidated/milestone/1)** -- [x] Two hand-authored NPCs live in one location, playable via CLI. +- [x] Two hand-authored NPCs live in one location and are playable through the GUI. - [x] Each has Cognitive Buffer and Memory Ledger memory and recalls something said a few turns earlier. - [x] One NPC knows a fact the other does not and, provably by testing, will not leak it. - [x] The Architect processes at least one non-trivial action per exchange with a visible state change. @@ -187,8 +229,9 @@ omnia/ spatial/ location and POI graph, portal-based perception llm/ ILLMProvider interface plus Gemini and deterministic mock implementations scenario/ scenario JSON schema and loader (JSON → SQLite) + runtime/ session lifecycle, turn execution, provider routing, and simulation persistence apps/ - gui/ Next.js Web GUI dashboard and simulation runner + gui/ Next.js UI and server-action adapter; hosts RuntimeService content/ demo/ bundled scenarios (talking-room) tests/ diff --git a/apps/gui/Dockerfile b/apps/gui/Dockerfile index 34aa4fe..9577ebe 100644 --- a/apps/gui/Dockerfile +++ b/apps/gui/Dockerfile @@ -23,8 +23,10 @@ COPY packages/core/package.json packages/core/package.json COPY packages/intent/package.json packages/intent/package.json COPY packages/llm/package.json packages/llm/package.json COPY packages/memory/package.json packages/memory/package.json +COPY packages/runtime/package.json packages/runtime/package.json COPY packages/scenario/package.json packages/scenario/package.json COPY packages/spatial/package.json packages/spatial/package.json +COPY packages/voice/package.json packages/voice/package.json RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ pnpm install --frozen-lockfile diff --git a/packages/api-client/package.json b/packages/api-client/package.json new file mode 100644 index 0000000..7ed7450 --- /dev/null +++ b/packages/api-client/package.json @@ -0,0 +1,13 @@ +{ + "name": "@omnia/api-client", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./dist/index.js" + }, + "dependencies": { + "@omnia/api-contracts": "workspace:*", + "zod": "^4.4.3" + } +} diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts new file mode 100644 index 0000000..f986bc4 --- /dev/null +++ b/packages/api-client/src/index.ts @@ -0,0 +1,362 @@ +import { + createSimulationRequestV1, + eventEnvelopeV1, + logPageV1, + modelListV1, + operationV1, + playerActionRequestV1, + problemDetailsV1, + providerCatalogEntryV1, + providerCreateRequestV1, + providerMappingV1, + providerSummaryV1, + providerUpdateRequestV1, + renameSimulationRequestV1, + scenarioV1, + simulationSnapshotV1, + simulationSummaryV1, + stepSimulationRequestV1, +} from "@omnia/api-contracts"; +import type { + CreateSimulationRequestV1, + EventEnvelopeV1, + LogPageV1, + ModelListV1, + OperationV1, + PlayerActionRequestV1, + ProblemDetailsV1, + ProviderCatalogEntryV1, + ProviderCreateRequestV1, + ProviderMappingV1, + ProviderSummaryV1, + ProviderUpdateRequestV1, + RenameSimulationRequestV1, + ScenarioV1, + SimulationSnapshotV1, + SimulationSummaryV1, + StepSimulationRequestV1, +} from "@omnia/api-contracts"; + +export interface ApiClientOptions { + baseUrl: string; + token?: string; + fetch?: typeof globalThis.fetch; + requestIdFactory?: () => string; +} + +export interface ListResponse { + items: T[]; + page: { + nextCursor: string | null; + hasMore: boolean; + }; +} + +export class ApiClientError extends Error { + readonly status: number; + readonly problem: ProblemDetailsV1 | null; + + constructor(status: number, message: string, problem: ProblemDetailsV1 | null) { + super(message); + this.name = "ApiClientError"; + this.status = status; + this.problem = problem; + } +} + +export class OmniaApiClient { + private readonly baseUrl: string; + private readonly token?: string; + private readonly fetchImpl: typeof globalThis.fetch; + private readonly requestIdFactory: () => string; + + constructor(options: ApiClientOptions) { + this.baseUrl = options.baseUrl.replace(/\/$/, ""); + this.token = options.token; + this.fetchImpl = options.fetch ?? globalThis.fetch; + this.requestIdFactory = + options.requestIdFactory ?? (() => crypto.randomUUID()); + } + + async listSimulations(query: { cursor?: string; limit?: number } = {}): Promise> { + const params = new URLSearchParams(); + if (query.cursor) params.set("cursor", query.cursor); + if (query.limit !== undefined) params.set("limit", String(query.limit)); + const result = await this.request( + `/api/v1/simulations${params.size ? `?${params}` : ""}`, + ); + return parseOrThrow( + simulationListResponseV1, + result, + "Invalid simulation list response", + ); + } + + async createSimulation( + input: CreateSimulationRequestV1, + options: { idempotencyKey?: string } = {}, + ): Promise { + const body = createSimulationRequestV1.parse(input); + const result = await this.request("/api/v1/simulations", { + method: "POST", + body, + idempotencyKey: options.idempotencyKey, + }); + return parseOrThrow( + simulationSnapshotV1.or(operationV1), + result, + "Invalid create simulation response", + ); + } + + async getSimulation(id: string): Promise { + const result = await this.request(`/api/v1/simulations/${encodeURIComponent(id)}`); + return parseOrThrow(simulationSnapshotV1, result, "Invalid simulation response"); + } + + async renameSimulation( + id: string, + input: RenameSimulationRequestV1, + options: { etag: string; idempotencyKey?: string }, + ): Promise { + const body = renameSimulationRequestV1.parse(input); + const result = await this.request( + `/api/v1/simulations/${encodeURIComponent(id)}`, + { method: "PATCH", body, etag: options.etag, idempotencyKey: options.idempotencyKey }, + ); + return parseOrThrow(simulationSnapshotV1, result, "Invalid rename simulation response"); + } + + async deleteSimulation( + id: string, + options: { etag: string; idempotencyKey?: string }, + ): Promise { + await this.request(`/api/v1/simulations/${encodeURIComponent(id)}`, { + method: "DELETE", + etag: options.etag, + idempotencyKey: options.idempotencyKey, + }); + } + + async stepSimulation( + id: string, + input: StepSimulationRequestV1 = {}, + options: { etag: string; idempotencyKey?: string }, + ): Promise { + const body = stepSimulationRequestV1.parse(input); + const result = await this.request( + `/api/v1/simulations/${encodeURIComponent(id)}/steps`, + { method: "POST", body, etag: options.etag, idempotencyKey: options.idempotencyKey }, + ); + return parseOrThrow(operationV1, result, "Invalid step operation response"); + } + + async submitPlayerAction( + id: string, + input: PlayerActionRequestV1, + options: { etag: string; idempotencyKey?: string }, + ): Promise { + const body = playerActionRequestV1.parse(input); + const result = await this.request( + `/api/v1/simulations/${encodeURIComponent(id)}/player-actions`, + { method: "POST", body, etag: options.etag, idempotencyKey: options.idempotencyKey }, + ); + return parseOrThrow(operationV1, result, "Invalid player action operation response"); + } + + async listSimulationLogs( + id: string, + query: { cursor?: string; limit?: number } = {}, + ): Promise { + const params = new URLSearchParams(); + if (query.cursor) params.set("cursor", query.cursor); + if (query.limit !== undefined) params.set("limit", String(query.limit)); + const result = await this.request( + `/api/v1/simulations/${encodeURIComponent(id)}/logs${params.size ? `?${params}` : ""}`, + ); + return parseOrThrow(logPageV1, result, "Invalid log page response"); + } + + async getOperation(id: string): Promise { + const result = await this.request(`/api/v1/operations/${encodeURIComponent(id)}`); + return parseOrThrow(operationV1, result, "Invalid operation response"); + } + + async cancelOperation(id: string): Promise { + const result = await this.request(`/api/v1/operations/${encodeURIComponent(id)}`, { + method: "DELETE", + }); + return parseOrThrow(operationV1, result, "Invalid cancellation response"); + } + + async listScenarios(): Promise { + const result = await this.request("/api/v1/scenarios"); + return parseOrThrow(zArray(scenarioV1), result, "Invalid scenarios response"); + } + + async listProviders(): Promise { + const result = await this.request("/api/v1/admin/providers"); + return parseOrThrow(zArray(providerSummaryV1), result, "Invalid providers response"); + } + + async createProvider(input: ProviderCreateRequestV1): Promise { + const result = await this.request("/api/v1/admin/providers", { + method: "POST", + body: providerCreateRequestV1.parse(input), + }); + return parseOrThrow(providerSummaryV1, result, "Invalid provider response"); + } + + async updateProvider(id: string, input: ProviderUpdateRequestV1): Promise { + const result = await this.request(`/api/v1/admin/providers/${encodeURIComponent(id)}`, { + method: "PATCH", + body: providerUpdateRequestV1.parse(input), + }); + return parseOrThrow(providerSummaryV1, result, "Invalid provider response"); + } + + async listProviderMappings(): Promise { + const result = await this.request("/api/v1/admin/provider-mappings"); + return parseOrThrow(providerMappingV1, result, "Invalid provider mappings response"); + } + + async listProviderCatalog(): Promise { + const result = await this.request("/api/v1/admin/provider-catalog"); + return parseOrThrow(zArray(providerCatalogEntryV1), result, "Invalid provider catalog response"); + } + + async discoverModelsForProvider(id: string): Promise { + const result = await this.request(`/api/v1/admin/providers/${encodeURIComponent(id)}/models`); + return parseOrThrow(modelListV1, result, "Invalid model discovery response"); + } + + async *events(signal?: AbortSignal): AsyncGenerator { + const response = await this.fetchRequest("/api/v1/events", { signal }); + if (!response.body) throw new Error("API event stream has no body"); + yield* parseEventStream(response.body, eventEnvelopeV1, signal); + } + + async *simulationEvents(id: string, signal?: AbortSignal): AsyncGenerator { + const response = await this.fetchRequest( + `/api/v1/simulations/${encodeURIComponent(id)}/events`, + { signal }, + ); + if (!response.body) throw new Error("Simulation event stream has no body"); + yield* parseEventStream(response.body, eventEnvelopeV1, signal); + } + + private async request(path: string, options: RequestOptions = {}): Promise { + const response = await this.fetchRequest(path, options); + if (response.status === 204) return undefined as T; + return (await response.json()) as T; + } + + private async fetchRequest(path: string, options: RequestOptions = {}): Promise { + const headers = new Headers(options.headers); + headers.set("Accept", "application/json"); + headers.set("X-Request-Id", this.requestIdFactory()); + if (options.body !== undefined) { + headers.set("Content-Type", "application/json"); + } + if (this.token) headers.set("Authorization", `Bearer ${this.token}`); + if (options.etag) headers.set("If-Match", options.etag); + if (options.idempotencyKey) headers.set("Idempotency-Key", options.idempotencyKey); + + const response = await this.fetchImpl(`${this.baseUrl}${path}`, { + method: options.method ?? "GET", + headers, + body: options.body === undefined ? undefined : JSON.stringify(options.body), + signal: options.signal, + }); + if (!response.ok) { + const contentType = response.headers.get("content-type") ?? ""; + const problem = contentType.includes("application/problem+json") + ? parseOrNull(problemDetailsV1, await response.json()) + : null; + throw new ApiClientError( + response.status, + problem?.detail ?? `Omnia API request failed with status ${response.status}`, + problem, + ); + } + return response; + } +} + +interface RequestOptions { + method?: string; + body?: unknown; + headers?: HeadersInit; + etag?: string; + idempotencyKey?: string; + signal?: AbortSignal; +} + +const zArray = (item: T) => z.array(item); + +import { z } from "zod"; + +const simulationListResponseV1 = z.object({ + items: z.array(simulationSummaryV1), + page: z.object({ nextCursor: z.string().nullable(), hasMore: z.boolean() }), +}); + +function parseOrThrow(schema: z.ZodType, input: unknown, message: string): T { + const parsed = schema.safeParse(input); + if (!parsed.success) throw new Error(`${message}: ${parsed.error.message}`); + return parsed.data; +} + +function parseOrNull(schema: z.ZodType, input: unknown): T | null { + const parsed = schema.safeParse(input); + return parsed.success ? parsed.data : null; +} + +async function* parseEventStream( + body: ReadableStream, + schema: z.ZodType, + signal?: AbortSignal, +): AsyncGenerator { + const reader = body.pipeThrough(new TextDecoderStream()).getReader(); + let buffer = ""; + try { + while (!signal?.aborted) { + const result = await reader.read(); + if (result.done) break; + buffer += result.value; + const frames = buffer.split("\n\n"); + buffer = frames.pop() ?? ""; + for (const frame of frames) { + const data = frame + .split("\n") + .filter((line) => line.startsWith("data:")) + .map((line) => line.slice(5).trimStart()) + .join("\n"); + if (!data) continue; + yield parseOrThrow(schema, JSON.parse(data), "Invalid API event"); + } + } + } finally { + await reader.cancel(); + } +} + +export type { + CreateSimulationRequestV1, + EventEnvelopeV1, + LogPageV1, + OperationV1, + PlayerActionRequestV1, + ProblemDetailsV1, + ProviderCatalogEntryV1, + ModelListV1, + ProviderCreateRequestV1, + ProviderMappingV1, + ProviderSummaryV1, + ProviderUpdateRequestV1, + RenameSimulationRequestV1, + ScenarioV1, + SimulationSnapshotV1, + SimulationSummaryV1, + StepSimulationRequestV1, +}; diff --git a/packages/api-client/tsconfig.json b/packages/api-client/tsconfig.json new file mode 100644 index 0000000..0241a1a --- /dev/null +++ b/packages/api-client/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"], + "references": [{ "path": "../api-contracts" }] +} diff --git a/packages/api-contracts/package.json b/packages/api-contracts/package.json new file mode 100644 index 0000000..1d26a3f --- /dev/null +++ b/packages/api-contracts/package.json @@ -0,0 +1,14 @@ +{ + "name": "@omnia/api-contracts", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./dist/index.js", + "./v1": "./dist/v1/index.js", + "./openapi": "./dist/v1/openapi.js" + }, + "dependencies": { + "zod": "^4.4.3" + } +} diff --git a/packages/api-contracts/src/index.ts b/packages/api-contracts/src/index.ts new file mode 100644 index 0000000..7092767 --- /dev/null +++ b/packages/api-contracts/src/index.ts @@ -0,0 +1 @@ +export * from "./v1/index.js"; diff --git a/packages/api-contracts/src/v1/index.ts b/packages/api-contracts/src/v1/index.ts new file mode 100644 index 0000000..2a6972a --- /dev/null +++ b/packages/api-contracts/src/v1/index.ts @@ -0,0 +1,6 @@ +export * from "./schemas.js"; +export { + getOpenApiDocumentV1, + openApiDocumentV1, +} from "./openapi.js"; +export type { OpenApiDocumentV1 } from "./openapi.js"; diff --git a/packages/api-contracts/src/v1/openapi.ts b/packages/api-contracts/src/v1/openapi.ts new file mode 100644 index 0000000..3222ce1 --- /dev/null +++ b/packages/api-contracts/src/v1/openapi.ts @@ -0,0 +1,341 @@ +import { toJSONSchema, z } from "zod"; +import { + createSimulationRequestV1, + eventEnvelopeV1, + logPageV1, + modelListV1, + operationV1, + playerActionRequestV1, + problemDetailsV1, + providerCatalogEntryV1, + providerCreateRequestV1, + providerMappingV1, + providerSummaryV1, + providerUpdateRequestV1, + renameSimulationRequestV1, + scenarioV1, + simulationSnapshotV1, + simulationSummaryV1, + stepSimulationRequestV1, +} from "./schemas.js"; + +export interface OpenApiDocumentV1 { + openapi: "3.1.0"; + info: { + title: string; + version: string; + description: string; + }; + jsonSchemaDialect: string; + paths: Record; + components: { + schemas: Record; + responses: Record; + parameters: Record; + securitySchemes: Record; + }; +} + +function schema(input: unknown): unknown { + return toJSONSchema(input as Parameters[0], { + target: "draft-2020-12", + }); +} + +const jsonContent = (schemaName: string, status = "200") => ({ + [status]: { + description: "JSON response", + content: { + "application/json": { + schema: { $ref: `#/components/schemas/${schemaName}` }, + }, + }, + }, +}); + +export const openApiDocumentV1: OpenApiDocumentV1 = { + openapi: "3.1.0", + info: { + title: "Omnia API", + version: "1.0.0", + description: "Versioned REST contract for Omnia runtime and administration.", + }, + jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema", + paths: { + "/api/v1/simulations": { + get: { + operationId: "listSimulations", + security: [{ bearerAuth: [] }], + responses: jsonContent("SimulationSummaryPageV1"), + }, + post: { + operationId: "createSimulation", + security: [{ bearerAuth: [] }], + requestBody: { + required: true, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/CreateSimulationRequestV1" }, + }, + }, + }, + responses: { + ...jsonContent("SimulationSnapshotV1", "201"), + "400": { $ref: "#/components/responses/ProblemDetails" }, + }, + }, + }, + "/api/v1/simulations/{simulationId}": { + get: { + operationId: "getSimulation", + security: [{ bearerAuth: [] }], + parameters: [{ $ref: "#/components/parameters/SimulationId" }], + responses: { + ...jsonContent("SimulationSnapshotV1"), + "404": { $ref: "#/components/responses/ProblemDetails" }, + }, + }, + patch: { + operationId: "renameSimulation", + security: [{ bearerAuth: [] }], + parameters: [ + { $ref: "#/components/parameters/SimulationId" }, + { $ref: "#/components/parameters/IfMatch" }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/RenameSimulationRequestV1" }, + }, + }, + }, + responses: jsonContent("SimulationSnapshotV1"), + }, + delete: { + operationId: "deleteSimulation", + security: [{ bearerAuth: [] }], + parameters: [ + { $ref: "#/components/parameters/SimulationId" }, + { $ref: "#/components/parameters/IfMatch" }, + ], + responses: { "204": { description: "Simulation deleted" } }, + }, + }, + "/api/v1/simulations/{simulationId}/steps": { + post: { + operationId: "stepSimulation", + security: [{ bearerAuth: [] }], + parameters: [ + { $ref: "#/components/parameters/SimulationId" }, + { $ref: "#/components/parameters/IfMatch" }, + { $ref: "#/components/parameters/IdempotencyKey" }, + ], + requestBody: { + required: false, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/StepSimulationRequestV1" }, + }, + }, + }, + responses: jsonContent("OperationV1", "202"), + }, + }, + "/api/v1/simulations/{simulationId}/player-actions": { + post: { + operationId: "submitPlayerAction", + security: [{ bearerAuth: [] }], + parameters: [ + { $ref: "#/components/parameters/SimulationId" }, + { $ref: "#/components/parameters/IfMatch" }, + { $ref: "#/components/parameters/IdempotencyKey" }, + ], + requestBody: { + required: true, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/PlayerActionRequestV1" }, + }, + }, + }, + responses: jsonContent("OperationV1", "202"), + }, + }, + "/api/v1/simulations/{simulationId}/logs": { + get: { + operationId: "listSimulationLogs", + security: [{ bearerAuth: [] }], + parameters: [ + { $ref: "#/components/parameters/SimulationId" }, + { $ref: "#/components/parameters/Cursor" }, + { $ref: "#/components/parameters/Limit" }, + ], + responses: jsonContent("LogPageV1"), + }, + }, + "/api/v1/simulations/{simulationId}/events": { + get: { + operationId: "subscribeSimulationEvents", + security: [{ bearerAuth: [] }], + parameters: [{ $ref: "#/components/parameters/SimulationId" }], + responses: { + "200": { + description: "Server-sent event stream", + content: { "text/event-stream": { schema: { type: "string" } } }, + }, + }, + }, + }, + "/api/v1/operations/{operationId}": { + get: { + operationId: "getOperation", + security: [{ bearerAuth: [] }], + parameters: [{ $ref: "#/components/parameters/OperationId" }], + responses: jsonContent("OperationV1"), + }, + delete: { + operationId: "cancelOperation", + security: [{ bearerAuth: [] }], + parameters: [{ $ref: "#/components/parameters/OperationId" }], + responses: jsonContent("OperationV1"), + }, + }, + "/api/v1/scenarios": { + get: { + operationId: "listScenarios", + security: [{ bearerAuth: [] }], + responses: { + "200": { + description: "Available scenarios", + content: { + "application/json": { + schema: { type: "array", items: { $ref: "#/components/schemas/ScenarioV1" } }, + }, + }, + }, + }, + }, + }, + "/api/v1/admin/providers": { + get: { + operationId: "listProviders", + security: [{ bearerAuth: [] }], + responses: { + "200": { + description: "Redacted provider configurations", + content: { + "application/json": { + schema: { type: "array", items: { $ref: "#/components/schemas/ProviderSummaryV1" } }, + }, + }, + }, + }, + }, + post: { + operationId: "createProvider", + security: [{ bearerAuth: [] }], + requestBody: { + required: true, + content: { + "application/json": { + schema: { $ref: "#/components/schemas/ProviderCreateRequestV1" }, + }, + }, + }, + responses: jsonContent("ProviderSummaryV1", "201"), + }, + }, + "/api/v1/events": { + get: { + operationId: "subscribeEvents", + security: [{ bearerAuth: [] }], + responses: { + "200": { + description: "Server-sent event stream", + content: { "text/event-stream": { schema: { type: "string" } } }, + }, + }, + }, + }, + }, + components: { + schemas: { + CreateSimulationRequestV1: schema(createSimulationRequestV1), + RenameSimulationRequestV1: schema(renameSimulationRequestV1), + PlayerActionRequestV1: schema(playerActionRequestV1), + StepSimulationRequestV1: schema(stepSimulationRequestV1), + SimulationSummaryV1: schema(simulationSummaryV1), + SimulationSnapshotV1: schema(simulationSnapshotV1), + SimulationSummaryPageV1: schema( + z.object({ items: z.array(simulationSummaryV1), page: z.object({ nextCursor: z.string().nullable(), hasMore: z.boolean() }) }), + ), + LogPageV1: schema(logPageV1), + OperationV1: schema(operationV1), + ProviderSummaryV1: schema(providerSummaryV1), + ProviderCreateRequestV1: schema(providerCreateRequestV1), + ProviderUpdateRequestV1: schema(providerUpdateRequestV1), + ProviderMappingV1: schema(providerMappingV1), + ProviderCatalogEntryV1: schema(providerCatalogEntryV1), + ScenarioV1: schema(scenarioV1), + EventEnvelopeV1: schema(eventEnvelopeV1), + ModelListV1: schema(modelListV1), + ProblemDetailsV1: schema(problemDetailsV1), + }, + responses: { + ProblemDetails: { + description: "RFC 9457 problem details", + content: { + "application/problem+json": { + schema: { $ref: "#/components/schemas/ProblemDetailsV1" }, + }, + }, + }, + }, + parameters: { + SimulationId: { + name: "simulationId", + in: "path", + required: true, + schema: { type: "string" }, + }, + OperationId: { + name: "operationId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + }, + Cursor: { + name: "cursor", + in: "query", + required: false, + schema: { type: "string" }, + }, + Limit: { + name: "limit", + in: "query", + required: false, + schema: { type: "integer", minimum: 1, maximum: 100, default: 25 }, + }, + IfMatch: { + name: "If-Match", + in: "header", + required: true, + schema: { type: "string" }, + }, + IdempotencyKey: { + name: "Idempotency-Key", + in: "header", + required: false, + schema: { type: "string", minLength: 1, maxLength: 256 }, + }, + }, + securitySchemes: { + bearerAuth: { type: "http", scheme: "bearer" }, + }, + }, +}; + +export function getOpenApiDocumentV1(): OpenApiDocumentV1 { + return openApiDocumentV1; +} diff --git a/packages/api-contracts/src/v1/schemas.ts b/packages/api-contracts/src/v1/schemas.ts new file mode 100644 index 0000000..ea3a5a8 --- /dev/null +++ b/packages/api-contracts/src/v1/schemas.ts @@ -0,0 +1,282 @@ +import { z } from "zod"; + +export const apiVersionV1 = z.literal("1"); + +export const runtimeStatusV1 = z.enum([ + "running", + "waiting_player", + "done", + "error", +]); + +export const simulationIdV1 = z.string().regex( + /^(?:sim-[0-9]+|[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/i, + "Invalid simulation identifier", +); + +export const operationIdV1 = z.string().regex( + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + "Invalid operation identifier", +); + +export const isoTimestampV1 = z.string().datetime({ offset: true }); + +export const entityV1 = z.object({ + id: z.string().min(1), + name: z.string().min(1), + isPlayer: z.boolean(), + isAgent: z.boolean(), + aliases: z.record(z.string(), z.string()).nullable(), +}); + +export const intentV1 = z.object({ + type: z.string().min(1), + content: z.string(), + modifiers: z.array(z.string()), + targetIds: z.array(z.string()), + isValid: z.boolean().nullable(), + reason: z.string().nullable(), + minutesToAdvance: z.number().nullable(), +}); + +export const tokenUsageV1 = z.object({ + inputTokens: z.number().int().nonnegative(), + outputTokens: z.number().int().nonnegative(), + totalTokens: z.number().int().nonnegative(), + modelName: z.string().nullable(), + providerInstanceName: z.string().nullable(), + maxContext: z.number().int().nonnegative().nullable(), +}); + +export const handoffChunkV1 = z.object({ + content: z.string(), + importance: z.number(), + quotes: z.array(z.string()).nullable(), + retainInBuffer: z.boolean().nullable(), + involvedEntityIds: z.array(z.string()).nullable(), +}); + +/** Public log data intentionally excludes prompts and model diagnostic payloads. */ +export const logEntryV1 = z.object({ + turn: z.number().int().nonnegative(), + entityId: z.string().min(1), + entityName: z.string().min(1), + narrativeProse: z.string(), + intents: z.array(intentV1), + timestamp: isoTimestampV1, + isHandoff: z.boolean(), + handoffResult: z.object({ chunks: z.array(handoffChunkV1) }).nullable(), + decodedIntents: z.array(intentV1).nullable(), + usage: tokenUsageV1.nullable(), +}); + +/** Public player context deliberately contains no prompt material. */ +export const waitingPlayerV1 = z.object({ + entityId: z.string().min(1), + name: z.string().min(1), +}); + +export const simulationSummaryV1 = z.object({ + apiVersion: apiVersionV1, + id: simulationIdV1, + status: runtimeStatusV1, + turn: z.number().int().nonnegative(), + maxTurns: z.number().int().positive(), + scenarioName: z.string(), + scenarioDescription: z.string(), + entityCount: z.number().int().nonnegative(), + updatedAt: isoTimestampV1.nullable(), +}); + +export const simulationSnapshotV1 = z.object({ + apiVersion: apiVersionV1, + id: simulationIdV1, + status: runtimeStatusV1, + turn: z.number().int().nonnegative(), + maxTurns: z.number().int().positive(), + scenarioName: z.string(), + scenarioDescription: z.string(), + entities: z.array(entityV1), + entityIndex: z.number().int().nonnegative(), + waitingPlayer: waitingPlayerV1.nullable(), + error: z.string().nullable(), + worldTime: isoTimestampV1.nullable(), + currentLocation: z.string().nullable(), + revision: z.number().int().nonnegative().nullable(), + updatedAt: isoTimestampV1.nullable(), +}); + +export const pageInfoV1 = z.object({ + nextCursor: z.string().nullable(), + hasMore: z.boolean(), +}); + +export const logPageV1 = z.object({ + items: z.array(logEntryV1), + page: pageInfoV1, +}); + +export const createSimulationRequestV1 = z.object({ + scenarioId: z.string().min(1).max(128), + playEntity: z.string().min(1).max(256).nullable().optional(), + providerInstanceId: z.string().min(1).max(256).nullable().optional(), + customName: z.string().trim().min(1).max(256).nullable().optional(), +}); + +export const renameSimulationRequestV1 = z.object({ + name: z.string().trim().min(1).max(256), +}); + +export const playerActionRequestV1 = z.object({ + prose: z.string().trim().min(1).max(32_000), +}); + +export const stepSimulationRequestV1 = z.object({ + waitForCompletion: z.boolean().default(false), +}); + +export const providerTypeV1 = z.enum(["generative", "embedding"]); + +export const providerSummaryV1 = z.object({ + id: z.string().min(1), + name: z.string().min(1), + providerName: z.string().min(1), + hasCredential: z.boolean(), + isActive: z.boolean(), + modelName: z.string().nullable(), + type: providerTypeV1, + maxContext: z.number().int().nonnegative().nullable(), + endpointUrl: z.string().url().nullable(), +}); + +/** Credentials are accepted on writes only and are never part of provider reads. */ +export const providerCredentialInputV1 = z.object({ + apiKey: z.string().max(16_384).nullable().optional(), +}); + +export const providerCreateRequestV1 = providerCredentialInputV1.extend({ + name: z.string().trim().min(1).max(256), + providerName: z.string().min(1).max(128), + modelName: z.string().trim().max(256).nullable().optional(), + type: providerTypeV1.default("generative"), + maxContext: z.number().int().positive().nullable().optional(), + endpointUrl: z.string().url().nullable().optional(), +}); + +export const providerUpdateRequestV1 = providerCreateRequestV1.partial().extend({ + apiKey: z.string().max(16_384).nullable().optional(), +}); + +export const providerMappingV1 = z.record(z.string().min(1), z.string().min(1)); + +export const modelV1 = z.object({ + id: z.string().min(1), + name: z.string().min(1), + ownedBy: z.string().nullable(), +}); + +export const modelListV1 = z.array(modelV1); + +export const providerCatalogEntryV1 = z.object({ + id: z.string().min(1), + displayName: z.string().min(1), + description: z.string(), + defaultModel: z.string(), + defaultEmbeddingModel: z.string(), +}); + +export const scenarioV1 = z.object({ + id: z.string().min(1), + name: z.string().min(1), + description: z.string(), + entities: z.array(z.object({ id: z.string().min(1), name: z.string().min(1) })), +}); + +export const operationStatusV1 = z.enum([ + "queued", + "running", + "completed", + "failed", + "cancelled", +]); + +export const operationV1 = z.object({ + apiVersion: apiVersionV1, + id: operationIdV1, + kind: z.enum(["step", "run", "player_action", "embedding_regeneration"]), + status: operationStatusV1, + progress: z.number().min(0).max(1).nullable(), + resultSimulationId: simulationIdV1.nullable(), + errorCode: z.string().nullable(), + createdAt: isoTimestampV1, + updatedAt: isoTimestampV1, +}); + +export const problemDetailsV1 = z.object({ + type: z.string().url(), + title: z.string().min(1), + status: z.number().int().min(400).max(599), + detail: z.string(), + instance: z.string().nullable(), + code: z.string().min(1), + requestId: z.string().min(1), +}); + +export const eventTypeV1 = z.enum([ + "simulation.created", + "simulation.status.changed", + "simulation.turn.started", + "simulation.turn.completed", + "simulation.player_input.requested", + "simulation.player_action.accepted", + "simulation.log_entry.appended", + "simulation.completed", + "simulation.failed", + "operation.progress", + "operation.completed", + "operation.failed", + "operation.cancelled", +]); + +export const eventEnvelopeV1 = z.object({ + apiVersion: apiVersionV1, + eventId: z.string().min(1), + sequence: z.number().int().nonnegative(), + revision: z.number().int().nonnegative().nullable(), + occurredAt: isoTimestampV1, + type: eventTypeV1, + simulationId: simulationIdV1.nullable(), + operationId: operationIdV1.nullable(), + data: z.unknown(), +}); + +export const listQueryV1 = z.object({ + cursor: z.string().max(512).nullable().optional(), + limit: z.coerce.number().int().min(1).max(100).default(25), +}); + +export type ApiVersionV1 = z.infer; +export type RuntimeStatusV1 = z.infer; +export type SimulationIdV1 = z.infer; +export type OperationIdV1 = z.infer; +export type EntityV1 = z.infer; +export type IntentV1 = z.infer; +export type LogEntryV1 = z.infer; +export type SimulationSummaryV1 = z.infer; +export type SimulationSnapshotV1 = z.infer; +export type CreateSimulationRequestV1 = z.infer; +export type RenameSimulationRequestV1 = z.infer; +export type PlayerActionRequestV1 = z.infer; +export type StepSimulationRequestV1 = z.infer; +export type ProviderSummaryV1 = z.infer; +export type ProviderCreateRequestV1 = z.infer; +export type ProviderUpdateRequestV1 = z.infer; +export type ProviderMappingV1 = z.infer; +export type ModelV1 = z.infer; +export type ModelListV1 = z.infer; +export type ProviderCatalogEntryV1 = z.infer; +export type ScenarioV1 = z.infer; +export type OperationV1 = z.infer; +export type ProblemDetailsV1 = z.infer; +export type EventEnvelopeV1 = z.infer; +export type ListQueryV1 = z.infer; diff --git a/packages/api-contracts/tsconfig.json b/packages/api-contracts/tsconfig.json new file mode 100644 index 0000000..5285d28 --- /dev/null +++ b/packages/api-contracts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/runtime/package.json b/packages/runtime/package.json index dc34425..d4f3b56 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -3,7 +3,9 @@ "private": true, "type": "module", "exports": { - ".": "./dist/index.js" + ".": "./dist/index.js", + "./testing": "./dist/testing/index.js", + "./internal": "./dist/internal.js" }, "dependencies": { "@omnia/actor": "workspace:*", @@ -15,4 +17,4 @@ "@omnia/voice": "workspace:*", "better-sqlite3": "^12.11.1" } -} \ No newline at end of file +} diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 051f2df..fd2d76f 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -1,15 +1,4 @@ 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"; diff --git a/packages/runtime/src/internal.ts b/packages/runtime/src/internal.ts new file mode 100644 index 0000000..9af2299 --- /dev/null +++ b/packages/runtime/src/internal.ts @@ -0,0 +1,10 @@ +export * from "./providers.js"; +export * from "./session.js"; +export * from "./persistence/types.js"; +export * from "./persistence/sqlite-session-store.js"; +export { + executePlayerAction, + preparePlayerTurn, + processNpcTurn, +} from "./turn-executor.js"; +export { runAliasResolution, runHandoffResolution } from "./alias-handoff.js"; diff --git a/packages/runtime/src/testing/index.ts b/packages/runtime/src/testing/index.ts new file mode 100644 index 0000000..09b949f --- /dev/null +++ b/packages/runtime/src/testing/index.ts @@ -0,0 +1 @@ +export { createRuntimeSnapshot } from "./runtime-fixtures.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11baf15..0d441d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,6 +435,21 @@ importers: specifier: ^4.4.3 version: 4.4.3 + packages/api-client: + dependencies: + '@omnia/api-contracts': + specifier: workspace:* + version: link:../api-contracts + zod: + specifier: ^4.4.3 + version: 4.4.3 + + packages/api-contracts: + dependencies: + zod: + specifier: ^4.4.3 + version: 4.4.3 + packages/architect: dependencies: '@omnia/core': diff --git a/tsconfig.json b/tsconfig.json index 3e5a972..d860eca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,8 @@ { "path": "./packages/llm" }, { "path": "./packages/actor" }, { "path": "./packages/scenario" }, - { "path": "./packages/runtime" } + { "path": "./packages/runtime" }, + { "path": "./packages/api-contracts" }, + { "path": "./packages/api-client" } ] } diff --git a/vitest.config.ts b/vitest.config.ts index 8a87b76..d87eebf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,6 +17,8 @@ export default defineConfig({ "@omnia/actor": path.resolve(__dirname, "./packages/actor/src"), "@omnia/scenario": path.resolve(__dirname, "./packages/scenario/src"), "@omnia/runtime": path.resolve(__dirname, "./packages/runtime/src"), + "@omnia/api-contracts": path.resolve(__dirname, "./packages/api-contracts/src"), + "@omnia/api-client": path.resolve(__dirname, "./packages/api-client/src"), }, }, test: { diff --git a/web/docs/src/content/docs/architecture/actor.md b/web/docs/src/content/docs/architecture/actor.md index 161cc25..e5a8ef9 100644 --- a/web/docs/src/content/docs/architecture/actor.md +++ b/web/docs/src/content/docs/architecture/actor.md @@ -60,8 +60,7 @@ Monologue (`"monologue"`) is the third intent type. Its properties: │ → system prompt + user context (subjective world + memory + time) │ ├─ 2. IActorProseGenerator.generate(entityId, systemPrompt, userContext) - │ ├─ LLMActorProseGenerator: queries LLM via generateStructuredResponse - │ └─ CLIProseGenerator: prompts human player via CLI / readline interface + │ └─ LLMActorProseGenerator: queries LLM via generateStructuredResponse │ → narrativeProse: string │ ├─ 3. IntentDecoder.decode(worldState, actorId, prose) @@ -69,7 +68,7 @@ Monologue (`"monologue"`) is the third intent type. Its properties: │ └─ returns { narrativeProse, intents } -[Caller (e.g. game loop)] +[Runtime turn executor] │ ├─ for each intent in intents: │ ├─ if intent.type === "monologue": short-circuit, write to buffer @@ -79,6 +78,11 @@ Monologue (`"monologue"`) is the third intent type. Its properties: └─ world state persisted to DB ``` +Human-controlled turns bypass NPC prose generation. `@omnia/runtime` prepares +a waiting-player snapshot, the GUI collects prose, and +`RuntimeService.submitPlayerAction()` sends it through intent decoding and turn +execution. + ## Key Files | File | Role | diff --git a/web/docs/src/content/docs/architecture/llm-providers.md b/web/docs/src/content/docs/architecture/llm-providers.md index 3244e06..5c8dd59 100644 --- a/web/docs/src/content/docs/architecture/llm-providers.md +++ b/web/docs/src/content/docs/architecture/llm-providers.md @@ -58,7 +58,7 @@ Configurations are stored globally in `data/settings.db` (separated from specifi ## Task Provider Routing -During a simulation run, the engine executes four distinct LLM operations. To optimize costs, latency, or model accuracy, you can route each of these tasks to different LLM provider instances: +During a simulation run, the runtime executes five generative operations and one embedding operation. To optimize costs, latency, or model accuracy, you can route each task to a different provider instance: | Task Name | Key ID | Description | Default Model | | :------------------------- | :--------------- | :--------------------------------------------------------------------------------------- | :--------------------------------------------- | @@ -66,6 +66,8 @@ During a simulation run, the engine executes four distinct LLM operations. To op | **LLM Validator** | `llm-validator` | Arbitrates and validates proposed actions against the world state rules and constraints. | `gemini-2.5-flash` / `google/gemini-2.5-flash` | | **Intent Decoder** | `intent-decoder` | Parses and splits free-text actions/prose into structured intent sequences. | `gemini-2.5-flash` / `google/gemini-2.5-flash` | | **TimeDelta Generator** | `timedelta` | Calculates the duration of character actions to advance the game clock. | `gemini-2.5-flash` / `google/gemini-2.5-flash` | +| **Memory Handoff Engine** | `handoff` | Summarizes Cognitive Buffer entries into the Memory Ledger. | Active generative provider | +| **Text Embeddings** | `embeddings` | Generates vectors for Memory Ledger retrieval. | Active embedding provider | If no specific provider instance is mapped to a task, the task automatically routes to the globally marked **Active** provider instance. @@ -73,11 +75,12 @@ If no specific provider instance is mapped to a task, the task automatically rou ## CLI Setup & Seeding -Rather than automatically bootstrapping from environment variables at runtime, which adds runtime complexity, you can quickly seed the database using the CLI setup tool: +Provider instances can be configured in the GUI or seeded from environment variables with the CLI setup tool. The CLI requires compiled workspace output, so run `pnpm build` first. ### Seeding All Environment-Variable Providers ```bash +pnpm build pnpm setup-provider --all ``` @@ -91,7 +94,7 @@ pnpm setup-provider --provider google-genai --key YOUR_API_KEY [--name "My Gemin ### Environment Variable Fallback -If the database contains no active provider instances, the LLM providers (e.g. `GeminiProvider`, `OpenAIProvider`, etc.) will fall back directly to reading their keys from environment variables (e.g. `GOOGLE_API_KEY`, `OPENAI_API_KEY`) via `resolveCredentials`. +When `GOOGLE_API_KEY` is present and no suitable active instance exists, `@omnia/runtime` creates Google generative and embedding fallback instances in `data/settings.db`. Other provider environment variables can be seeded with `pnpm setup-provider --all`. --- diff --git a/web/docs/src/content/docs/architecture/overview.md b/web/docs/src/content/docs/architecture/overview.md index a71a1fc..60082eb 100644 --- a/web/docs/src/content/docs/architecture/overview.md +++ b/web/docs/src/content/docs/architecture/overview.md @@ -16,8 +16,9 @@ omnia/ spatial/ location and POI graph, portal-based perception llm/ ILLMProvider interface plus Gemini and deterministic mock implementations scenario/ scenario JSON schema and loader (JSON → SQLite) + runtime/ session lifecycle, turn execution, provider routing, and simulation persistence apps/ - cli/ the playable loop (human or LLM actors, --scenario / --play flags) + gui/ Next.js UI and server actions; instantiates RuntimeService content/ demo/ bundled scenarios (talking-room) tests/ @@ -30,13 +31,17 @@ omnia/ The engine core deliberately knows nothing about domain content (stats, traits, genres). Scenarios are plain JSON the loader ingests; what an attribute means is the scenario's business, not the engine's. +`@omnia/runtime` is an application library hosted by the Next.js server, not a +separately deployed backend service. Browser requests reach GUI server actions, +which delegate simulation lifecycle and turn execution to `RuntimeService`. + ## Core Data Flow -1. An **Actor Agent** receives an epistemically-bounded view of the world and produces narrative prose. -2. The **Intent Decoder** splits prose into typed intents (`dialogue`, `action`, `monologue`). -3. The **World Architect** validates action intents against objective world state and generates structured deltas. -4. Deterministic code applies deltas to the **World State** (SQLite) and persists results. -5. Memory entries are written per-character, filtered through **Subjective Aliases**. +1. A browser action reaches a Next.js server action in `apps/gui`. +2. `RuntimeService` loads the session and asks an **Actor Agent** for narrative prose when the active entity is an NPC. +3. The **Intent Decoder** splits prose into typed intents (`dialogue`, `action`, `monologue`). +4. The **World Architect** validates action intents against objective world state and generates structured deltas. +5. Deterministic code applies deltas to the **World State** (SQLite), writes per-character memory through **Subjective Aliases**, and persists the runtime session. ## A Research Instrument