feat: Add architect along with llmValidator

This commit is contained in:
2026-07-06 12:31:04 +05:30
parent 0c3a2043b3
commit 0a227d8046
7 changed files with 262 additions and 2 deletions

View File

@@ -0,0 +1,23 @@
import { WorldState } from "@omnia/core";
import { LLMValidator, ValidationResult } from "./llmValidator.js";
import { ILLMProvider } from "@omnia/llm";
export class Architect {
private validator: LLMValidator;
constructor(llmProvider: ILLMProvider) {
this.validator = new LLMValidator(llmProvider);
}
/**
* Processes and validates a proposed intent action in the world.
* If valid, return success. If invalid, returns denial with reasons.
*/
async validateIntent(
worldState: WorldState,
actorId: string,
actionIntent: string,
): Promise<ValidationResult> {
return this.validator.validate(worldState, actorId, actionIntent);
}
}

View File

View File

@@ -1 +1,2 @@
export {};
export * from "./llmValidator.js";
export * from "./architect.js";

View File

@@ -0,0 +1,112 @@
import { z } from "zod";
import { WorldState } from "@omnia/core";
import { ILLMProvider } from "@omnia/llm";
export const ValidationResultSchema = z.object({
isValid: z.boolean(),
reason: z.string(),
});
export type ValidationResult = z.infer<typeof ValidationResultSchema>;
export class LLMValidator {
constructor(private llmProvider: ILLMProvider) {}
/**
* Validates an action intent against the objective world state.
*/
async validate(
worldState: WorldState,
actorId: string,
actionIntent: string,
): Promise<ValidationResult> {
const actor = worldState.getEntity(actorId);
if (!actor) {
return {
isValid: false,
reason: `Actor entity with ID "${actorId}" does not exist in the world state.`,
};
}
// 1. Serialize the objective world state for the LLM
const serializedWorld = this.serializeWorldState(worldState);
// 2. Build the prompts
const systemPrompt = `
You are the World Architect, a deterministic and objective judge of reality, physics, and narration for a simulation game.
Your task is to judge whether a proposed action (Intent) by an actor is physically and logically possible given the current objective state of the world.
Exempt dialogue or speech actions from validation (consider them always valid).
Enforce logical boundaries such as:
- Spatial boundaries (an actor cannot grab an object in another location unless they are there).
- Physical boundaries (an actor cannot open a locked drawer without a key or breaking it).
- State/Attribute constraints.
You must respond with a JSON object containing:
- "isValid": boolean indicating if the action is possible/allowed.
- "reason": a concise explanation of why the action is allowed or denied.
`.trim();
const userContext = `
=== CURRENT WORLD STATE ===
Current Time: ${worldState.clock.get().toISOString()}
Entities & Attributes:
${serializedWorld}
=== PROPOSED ACTION ===
Actor ID: ${actorId}
Proposed Action: "${actionIntent}"
Decide if the proposed action is logically valid and physically possible.
`.trim();
// structured call via the LLM provider
const response = await this.llmProvider.generateStructuredResponse({
systemPrompt,
userContext,
schema: ValidationResultSchema,
});
if (!response.success || !response.data) {
return {
isValid: false,
reason: `LLM validation failed: ${response.error || "Unknown LLM error"}`,
};
}
return response.data;
}
private serializeWorldState(worldState: WorldState): string {
const lines: string[] = [];
// Serialize world attributes
if (worldState.attributes.size > 0) {
lines.push("World Attributes:");
for (const [name, attr] of worldState.attributes.entries()) {
lines.push(
` - ${name}: ${attr.getValue()} (Visibility: ${attr.getVisibility()})`,
);
}
}
// Serialize entities and their attributes
lines.push("Entities:");
for (const entity of worldState.entities.values()) {
lines.push(` - Entity [ID: ${entity.id}]:`);
if (entity.attributes.size > 0) {
for (const [name, attr] of entity.attributes.entries()) {
const aclList = Array.from(attr.getAllowedEntities());
const aclStr =
aclList.length > 0 ? ` (Visible to: ${aclList.join(", ")})` : "";
lines.push(
` * ${name}: ${attr.getValue()} (Visibility: ${attr.getVisibility()})${aclStr}`,
);
}
} else {
lines.push(" * (No attributes)");
}
}
return lines.join("\n");
}
}

View File

@@ -0,0 +1,73 @@
import { describe, test, expect } from "vitest";
import { WorldState } from "@omnia/core";
import { Entity } from "@omnia/core";
import { MockLLMProvider } from "@omnia/llm";
import { Architect } from "@omnia/architect";
describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
test("returns valid response when LLM validates intent as successful", async () => {
const world = new WorldState("world-1");
const alice = new Entity("alice");
world.addEntity(alice);
const mockResponse = {
isValid: true,
reason: "Alice is in the room and the chest is unlocked.",
};
const llmProvider = new MockLLMProvider([mockResponse]);
const architect = new Architect(llmProvider);
const result = await architect.validateIntent(
world,
"alice",
"open the chest and read the scroll",
);
expect(result.isValid).toBe(true);
expect(result.reason).toBe(
"Alice is in the room and the chest is unlocked.",
);
});
test("returns invalid response when LLM denies the intent", async () => {
const world = new WorldState("world-1");
const bob = new Entity("bob");
world.addEntity(bob);
// Setup mock LLM response
const mockResponse = {
isValid: false,
reason: "Bob does not have the key to the iron gate.",
};
const llmProvider = new MockLLMProvider([mockResponse]);
const architect = new Architect(llmProvider);
const result = await architect.validateIntent(
world,
"bob",
"unlock the gate and escape",
);
expect(result.isValid).toBe(false);
expect(result.reason).toBe("Bob does not have the key to the iron gate.");
});
test("returns invalid response immediately if actor does not exist", async () => {
const world = new WorldState("world-1");
// No entities added
const llmProvider = new MockLLMProvider([]); // No mock responses because it shouldn't be called
const architect = new Architect(llmProvider);
const result = await architect.validateIntent(
world,
"ghost",
"haunt the mansion",
);
expect(result.isValid).toBe(false);
expect(result.reason).toContain(
'Actor entity with ID "ghost" does not exist',
);
});
});

View File

@@ -5,5 +5,8 @@
"outDir": "dist"
},
"include": ["src"],
"references": []
"references": [
{ "path": "../core" },
{ "path": "../llm" }
]
}