mirror of
https://github.com/sortedcord/omnia.git
synced 2026-09-05 05:43:47 +05:30
feat(api): Created openapi based pubic API contracts
- made DTOs - created an api-client
This commit is contained in:
5
.codegraph/.gitignore
vendored
5
.codegraph/.gitignore
vendored
@@ -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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -62,3 +62,4 @@ data/
|
||||
.vercel
|
||||
|
||||
__local_notes/
|
||||
omnia.code-workspace
|
||||
|
||||
65
.rsyncignore
65
.rsyncignore
@@ -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/
|
||||
@@ -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/<package>/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.
|
||||
|
||||
63
README.md
63
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/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
13
packages/api-client/package.json
Normal file
13
packages/api-client/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
362
packages/api-client/src/index.ts
Normal file
362
packages/api-client/src/index.ts
Normal file
@@ -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<T> {
|
||||
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<ListResponse<SimulationSummaryV1>> {
|
||||
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<unknown>(
|
||||
`/api/v1/simulations${params.size ? `?${params}` : ""}`,
|
||||
);
|
||||
return parseOrThrow(
|
||||
simulationListResponseV1,
|
||||
result,
|
||||
"Invalid simulation list response",
|
||||
);
|
||||
}
|
||||
|
||||
async createSimulation(
|
||||
input: CreateSimulationRequestV1,
|
||||
options: { idempotencyKey?: string } = {},
|
||||
): Promise<SimulationSnapshotV1 | OperationV1> {
|
||||
const body = createSimulationRequestV1.parse(input);
|
||||
const result = await this.request<unknown>("/api/v1/simulations", {
|
||||
method: "POST",
|
||||
body,
|
||||
idempotencyKey: options.idempotencyKey,
|
||||
});
|
||||
return parseOrThrow(
|
||||
simulationSnapshotV1.or(operationV1),
|
||||
result,
|
||||
"Invalid create simulation response",
|
||||
);
|
||||
}
|
||||
|
||||
async getSimulation(id: string): Promise<SimulationSnapshotV1> {
|
||||
const result = await this.request<unknown>(`/api/v1/simulations/${encodeURIComponent(id)}`);
|
||||
return parseOrThrow(simulationSnapshotV1, result, "Invalid simulation response");
|
||||
}
|
||||
|
||||
async renameSimulation(
|
||||
id: string,
|
||||
input: RenameSimulationRequestV1,
|
||||
options: { etag: string; idempotencyKey?: string },
|
||||
): Promise<SimulationSnapshotV1> {
|
||||
const body = renameSimulationRequestV1.parse(input);
|
||||
const result = await this.request<unknown>(
|
||||
`/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<void> {
|
||||
await this.request<unknown>(`/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<OperationV1> {
|
||||
const body = stepSimulationRequestV1.parse(input);
|
||||
const result = await this.request<unknown>(
|
||||
`/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<OperationV1> {
|
||||
const body = playerActionRequestV1.parse(input);
|
||||
const result = await this.request<unknown>(
|
||||
`/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<LogPageV1> {
|
||||
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<unknown>(
|
||||
`/api/v1/simulations/${encodeURIComponent(id)}/logs${params.size ? `?${params}` : ""}`,
|
||||
);
|
||||
return parseOrThrow(logPageV1, result, "Invalid log page response");
|
||||
}
|
||||
|
||||
async getOperation(id: string): Promise<OperationV1> {
|
||||
const result = await this.request<unknown>(`/api/v1/operations/${encodeURIComponent(id)}`);
|
||||
return parseOrThrow(operationV1, result, "Invalid operation response");
|
||||
}
|
||||
|
||||
async cancelOperation(id: string): Promise<OperationV1> {
|
||||
const result = await this.request<unknown>(`/api/v1/operations/${encodeURIComponent(id)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
return parseOrThrow(operationV1, result, "Invalid cancellation response");
|
||||
}
|
||||
|
||||
async listScenarios(): Promise<ScenarioV1[]> {
|
||||
const result = await this.request<unknown>("/api/v1/scenarios");
|
||||
return parseOrThrow(zArray(scenarioV1), result, "Invalid scenarios response");
|
||||
}
|
||||
|
||||
async listProviders(): Promise<ProviderSummaryV1[]> {
|
||||
const result = await this.request<unknown>("/api/v1/admin/providers");
|
||||
return parseOrThrow(zArray(providerSummaryV1), result, "Invalid providers response");
|
||||
}
|
||||
|
||||
async createProvider(input: ProviderCreateRequestV1): Promise<ProviderSummaryV1> {
|
||||
const result = await this.request<unknown>("/api/v1/admin/providers", {
|
||||
method: "POST",
|
||||
body: providerCreateRequestV1.parse(input),
|
||||
});
|
||||
return parseOrThrow(providerSummaryV1, result, "Invalid provider response");
|
||||
}
|
||||
|
||||
async updateProvider(id: string, input: ProviderUpdateRequestV1): Promise<ProviderSummaryV1> {
|
||||
const result = await this.request<unknown>(`/api/v1/admin/providers/${encodeURIComponent(id)}`, {
|
||||
method: "PATCH",
|
||||
body: providerUpdateRequestV1.parse(input),
|
||||
});
|
||||
return parseOrThrow(providerSummaryV1, result, "Invalid provider response");
|
||||
}
|
||||
|
||||
async listProviderMappings(): Promise<ProviderMappingV1> {
|
||||
const result = await this.request<unknown>("/api/v1/admin/provider-mappings");
|
||||
return parseOrThrow(providerMappingV1, result, "Invalid provider mappings response");
|
||||
}
|
||||
|
||||
async listProviderCatalog(): Promise<ProviderCatalogEntryV1[]> {
|
||||
const result = await this.request<unknown>("/api/v1/admin/provider-catalog");
|
||||
return parseOrThrow(zArray(providerCatalogEntryV1), result, "Invalid provider catalog response");
|
||||
}
|
||||
|
||||
async discoverModelsForProvider(id: string): Promise<ModelListV1> {
|
||||
const result = await this.request<unknown>(`/api/v1/admin/providers/${encodeURIComponent(id)}/models`);
|
||||
return parseOrThrow(modelListV1, result, "Invalid model discovery response");
|
||||
}
|
||||
|
||||
async *events(signal?: AbortSignal): AsyncGenerator<EventEnvelopeV1> {
|
||||
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<EventEnvelopeV1> {
|
||||
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<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
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<Response> {
|
||||
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 = <T extends z.ZodType>(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<T>(schema: z.ZodType<T>, 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<T>(schema: z.ZodType<T>, input: unknown): T | null {
|
||||
const parsed = schema.safeParse(input);
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
async function* parseEventStream<T>(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
schema: z.ZodType<T>,
|
||||
signal?: AbortSignal,
|
||||
): AsyncGenerator<T> {
|
||||
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,
|
||||
};
|
||||
9
packages/api-client/tsconfig.json
Normal file
9
packages/api-client/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "../api-contracts" }]
|
||||
}
|
||||
14
packages/api-contracts/package.json
Normal file
14
packages/api-contracts/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
1
packages/api-contracts/src/index.ts
Normal file
1
packages/api-contracts/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./v1/index.js";
|
||||
6
packages/api-contracts/src/v1/index.ts
Normal file
6
packages/api-contracts/src/v1/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./schemas.js";
|
||||
export {
|
||||
getOpenApiDocumentV1,
|
||||
openApiDocumentV1,
|
||||
} from "./openapi.js";
|
||||
export type { OpenApiDocumentV1 } from "./openapi.js";
|
||||
341
packages/api-contracts/src/v1/openapi.ts
Normal file
341
packages/api-contracts/src/v1/openapi.ts
Normal file
@@ -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<string, unknown>;
|
||||
components: {
|
||||
schemas: Record<string, unknown>;
|
||||
responses: Record<string, unknown>;
|
||||
parameters: Record<string, unknown>;
|
||||
securitySchemes: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
function schema(input: unknown): unknown {
|
||||
return toJSONSchema(input as Parameters<typeof toJSONSchema>[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;
|
||||
}
|
||||
282
packages/api-contracts/src/v1/schemas.ts
Normal file
282
packages/api-contracts/src/v1/schemas.ts
Normal file
@@ -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<typeof apiVersionV1>;
|
||||
export type RuntimeStatusV1 = z.infer<typeof runtimeStatusV1>;
|
||||
export type SimulationIdV1 = z.infer<typeof simulationIdV1>;
|
||||
export type OperationIdV1 = z.infer<typeof operationIdV1>;
|
||||
export type EntityV1 = z.infer<typeof entityV1>;
|
||||
export type IntentV1 = z.infer<typeof intentV1>;
|
||||
export type LogEntryV1 = z.infer<typeof logEntryV1>;
|
||||
export type SimulationSummaryV1 = z.infer<typeof simulationSummaryV1>;
|
||||
export type SimulationSnapshotV1 = z.infer<typeof simulationSnapshotV1>;
|
||||
export type CreateSimulationRequestV1 = z.infer<typeof createSimulationRequestV1>;
|
||||
export type RenameSimulationRequestV1 = z.infer<typeof renameSimulationRequestV1>;
|
||||
export type PlayerActionRequestV1 = z.infer<typeof playerActionRequestV1>;
|
||||
export type StepSimulationRequestV1 = z.infer<typeof stepSimulationRequestV1>;
|
||||
export type ProviderSummaryV1 = z.infer<typeof providerSummaryV1>;
|
||||
export type ProviderCreateRequestV1 = z.infer<typeof providerCreateRequestV1>;
|
||||
export type ProviderUpdateRequestV1 = z.infer<typeof providerUpdateRequestV1>;
|
||||
export type ProviderMappingV1 = z.infer<typeof providerMappingV1>;
|
||||
export type ModelV1 = z.infer<typeof modelV1>;
|
||||
export type ModelListV1 = z.infer<typeof modelListV1>;
|
||||
export type ProviderCatalogEntryV1 = z.infer<typeof providerCatalogEntryV1>;
|
||||
export type ScenarioV1 = z.infer<typeof scenarioV1>;
|
||||
export type OperationV1 = z.infer<typeof operationV1>;
|
||||
export type ProblemDetailsV1 = z.infer<typeof problemDetailsV1>;
|
||||
export type EventEnvelopeV1 = z.infer<typeof eventEnvelopeV1>;
|
||||
export type ListQueryV1 = z.infer<typeof listQueryV1>;
|
||||
8
packages/api-contracts/tsconfig.json
Normal file
8
packages/api-contracts/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
10
packages/runtime/src/internal.ts
Normal file
10
packages/runtime/src/internal.ts
Normal file
@@ -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";
|
||||
1
packages/runtime/src/testing/index.ts
Normal file
1
packages/runtime/src/testing/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createRuntimeSnapshot } from "./runtime-fixtures.js";
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -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':
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user