mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
feat: Add architect along with llmValidator
This commit is contained in:
48
docs/architect.md
Normal file
48
docs/architect.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Architect
|
||||
|
||||
The architect is not a specialized model. Neither is it a special entity. The architect isn't a an entity that inherits `AttributableObject` either. Instead, it is a special context along with a set of tools that is given to an LLM that dictates what happens to our world state.
|
||||
|
||||
The Architect (context) is provided with the WorldState, states of entities, state of a scene, location, along with their attributes and is asked to judge weather an action (later an `Intent`) makes canonical sense or not (for example, item ownership, entity location and state tracking) and disallows intents that break the narrative flow completely.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Agent] -->|"Perform Action"| B(Action Intent)
|
||||
subgraph "Architect Layer"
|
||||
C[Architect]
|
||||
D{"Validators"}
|
||||
E{"Noise Layer 1"}
|
||||
F{"Noise Layer 2"}
|
||||
G[Delta Generators]
|
||||
end
|
||||
B --> C
|
||||
|
||||
C -->|"Tool Call"| D
|
||||
C -->|"Spontaneity Bypass"| E
|
||||
|
||||
D --> F
|
||||
E --> F
|
||||
|
||||
F -->|"Pass"| G
|
||||
F -->|"Fail"| A
|
||||
|
||||
|
||||
G --> |Deltas modify| H[State]
|
||||
H --> I[Entity]
|
||||
H --> J[Location]
|
||||
H --> K[World]
|
||||
```
|
||||
|
||||
For now, dialogue intents are exempted from validation system and are not used for state manipulation. Dialogues fall more into the domain of the [Perception Engine (Deferred)]() and [memory systems (nlavs)]().
|
||||
|
||||
v0 defers atomic validators completely. Instead an umbrella LLM based validator is used for validating the intent (action) generated based on spatial knowledge and heuristic physics.
|
||||
|
||||
## Dyamic Validators (Deferred from V0)
|
||||
|
||||
There are will a [standard set of Validators]() that dictate if an action is possible or not. The architect can, however, dynamically generate its own set of Validators which can be loaded and unloaded during runtime based on narration. These Validators can be soft validators (if they fail they're sent back to the entity for override confirmation.)
|
||||
|
||||
## RNG and Noise Layers (Deferred from v0)
|
||||
|
||||
> [!NOTE]
|
||||
> Documenting story flattening problem.
|
||||
|
||||
Since we're have such complex systems for action validation, it's possible that the LLM would naturally steer towards low stakes actions or actions with minimal consequences. That way the narrative would simply flatten out. Which is why there needs to exist a noise layer that would allow for random (slightly non-sensible) actions to take place to introduce Spontaneity and unexpectedness into the system. (See dynamic temperature tweaking)
|
||||
23
packages/architect/src/architect.ts
Normal file
23
packages/architect/src/architect.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
0
packages/architect/src/delta.ts
Normal file
0
packages/architect/src/delta.ts
Normal file
@@ -1 +1,2 @@
|
||||
export {};
|
||||
export * from "./llmValidator.js";
|
||||
export * from "./architect.js";
|
||||
|
||||
112
packages/architect/src/llmValidator.ts
Normal file
112
packages/architect/src/llmValidator.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
73
packages/architect/tests/architect.test.ts
Normal file
73
packages/architect/tests/architect.test.ts
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,5 +5,8 @@
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": []
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../llm" }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user