feat(api): Created openapi based pubic API contracts

- made DTOs
- created an api-client
This commit is contained in:
2026-08-03 11:06:02 +00:00
parent 8204e9c18e
commit 81152dc8de
25 changed files with 1171 additions and 112 deletions

View 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"
}
}

View 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,
};

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "../api-contracts" }]
}

View 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"
}
}

View File

@@ -0,0 +1 @@
export * from "./v1/index.js";

View File

@@ -0,0 +1,6 @@
export * from "./schemas.js";
export {
getOpenApiDocumentV1,
openApiDocumentV1,
} from "./openapi.js";
export type { OpenApiDocumentV1 } from "./openapi.js";

View 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;
}

View 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>;

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"]
}

View File

@@ -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"
}
}
}

View File

@@ -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";

View 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";

View File

@@ -0,0 +1 @@
export { createRuntimeSnapshot } from "./runtime-fixtures.js";