feat: Implemented Delta generator for time advancement

This commit is contained in:
2026-07-06 20:11:59 +05:30
parent 6d8c3cee4f
commit de0510636d
9 changed files with 243 additions and 18 deletions

View File

@@ -1,12 +1,19 @@
import { WorldState } from "@omnia/core";
import { WorldState, SQLiteRepository } from "@omnia/core";
import { LLMValidator, ValidationResult } from "./llm-validator.js";
import { TimeDeltaGenerator, TimeDelta } from "./delta.js";
import { ILLMProvider } from "@omnia/llm";
export interface ProcessResult extends ValidationResult {
timeDelta?: TimeDelta;
}
export class Architect {
private validator: LLMValidator;
private timeDeltaGenerator: TimeDeltaGenerator;
constructor(llmProvider: ILLMProvider) {
constructor(llmProvider: ILLMProvider, private repo?: SQLiteRepository) {
this.validator = new LLMValidator(llmProvider);
this.timeDeltaGenerator = new TimeDeltaGenerator(llmProvider);
}
/**
@@ -20,4 +27,41 @@ export class Architect {
): Promise<ValidationResult> {
return this.validator.validate(worldState, actorId, actionIntent);
}
/**
* Processes, validates, generates deltas, applies them to the world state,
* and persists the changes to the database.
*/
async processIntent(
worldState: WorldState,
actorId: string,
actionIntent: string,
): Promise<ProcessResult> {
// 1. Validate the intent action
const validation = await this.validateIntent(worldState, actorId, actionIntent);
if (!validation.isValid) {
return validation;
}
// 2. Generate time delta for the valid action
const timeDelta = await this.timeDeltaGenerator.generate(
worldState,
actorId,
actionIntent,
);
// 3. Apply the time delta to the world state clock
worldState.clock.advance(timeDelta.minutesToAdvance);
// 4. Save and persist the updated world state
if (this.repo) {
this.repo.saveWorldState(worldState);
}
return {
isValid: true,
reason: validation.reason,
timeDelta,
};
}
}

View File

@@ -0,0 +1,63 @@
import { z } from "zod";
import { WorldState } from "@omnia/core";
import { ILLMProvider } from "@omnia/llm";
export const TimeDeltaSchema = z.object({
minutesToAdvance: z.number().int().nonnegative(),
explanation: z.string(),
});
export type TimeDelta = z.infer<typeof TimeDeltaSchema>;
export interface IDeltaGenerator<T> {
generate(
worldState: WorldState,
actorId: string,
actionIntent: string,
): Promise<T>;
}
export class TimeDeltaGenerator implements IDeltaGenerator<TimeDelta> {
constructor(private llmProvider: ILLMProvider) {}
async generate(
worldState: WorldState,
actorId: string,
actionIntent: string,
): Promise<TimeDelta> {
const systemPrompt = `
You are the Time Delta Generator for the World Architect.
Your task is to judge how much time (in minutes) a proposed action would logically take to execute in the physical world.
Enforce realistic physical constraints:
- Simple actions (e.g. picking up an unlocked key, looking around) take 1-2 minutes.
- Walking between rooms takes 2-5 minutes.
- Complex tasks (e.g. searching a room thoroughly, reading a book chapter, picking a lock) take 15-60 minutes.
Return a structured JSON object containing:
- "minutesToAdvance": integer (0 or more) representing the time elapsed.
- "explanation": a brief explanation of why this amount of time is appropriate.
`.trim();
const userContext = `
=== CURRENT WORLD STATE ===
Current Time: ${worldState.clock.get().toISOString()}
World Details:
${worldState.serialize()}
=== ACTION ===
Actor ID: ${actorId}
Action: "${actionIntent}"
`.trim();
const response = await this.llmProvider.generateStructuredResponse({
systemPrompt,
userContext,
schema: TimeDeltaSchema,
});
if (!response.success || !response.data) {
throw new Error(`Failed to generate time delta: ${response.error || "Unknown LLM error"}`);
}
return response.data;
}
}

View File

@@ -1,2 +1,3 @@
export * from "./llm-validator.js";
export * from "./architect.js";
export * from "./delta.js";

View File

@@ -39,6 +39,7 @@ 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 Boundaries (an actor cannot perform a task if their state doesn't allow them to do so).
- State/Attribute constraints.
You must respond with a JSON object containing:

View File

@@ -1,6 +1,6 @@
import { describe, test, expect } from "vitest";
import { WorldState } from "@omnia/core";
import { Entity } from "@omnia/core";
import Database from "better-sqlite3";
import { WorldState, Entity, SQLiteRepository } from "@omnia/core";
import { MockLLMProvider } from "@omnia/llm";
import { Architect } from "@omnia/architect";
@@ -71,3 +71,83 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
);
});
});
describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", () => {
test("processIntent advances clock and saves state for a valid intent", async () => {
const db = new Database(":memory:");
const repo = new SQLiteRepository(db);
const world = new WorldState("world-xyz", new Date("2026-07-06T12:00:00.000Z"));
const alice = new Entity("alice");
world.addEntity(alice);
// Initial save so it exists in db
repo.saveWorldState(world);
// Setup mock LLM responses:
// First call: validateIntent (ValidationResult)
// Second call: TimeDeltaGenerator (TimeDelta)
const mockValidation = { isValid: true, reason: "Alice has the lockpick kit and skill." };
const mockTimeDelta = { minutesToAdvance: 20, explanation: "Picking a lock takes time." };
const llmProvider = new MockLLMProvider([mockValidation, mockTimeDelta]);
const architect = new Architect(llmProvider, repo);
const result = await architect.processIntent(
world,
"alice",
"pick the lock of the wooden chest",
);
// Assert results
expect(result.isValid).toBe(true);
expect(result.reason).toBe("Alice has the lockpick kit and skill.");
expect(result.timeDelta).toBeDefined();
expect(result.timeDelta!.minutesToAdvance).toBe(20);
// Verify clock was advanced locally
const expectedTime = new Date(new Date("2026-07-06T12:00:00.000Z").getTime() + 20 * 60_000);
expect(world.clock.get().toISOString()).toBe(expectedTime.toISOString());
// Verify it was persisted to the database
const loaded = repo.loadWorldState("world-xyz")!;
expect(loaded.clock.get().toISOString()).toBe(expectedTime.toISOString());
db.close();
});
test("processIntent does not advance clock or save state for an invalid intent", async () => {
const db = new Database(":memory:");
const repo = new SQLiteRepository(db);
const startTime = new Date("2026-07-06T12:00:00.000Z");
const world = new WorldState("world-xyz", startTime);
const bob = new Entity("bob");
world.addEntity(bob);
repo.saveWorldState(world);
const mockValidation = { isValid: false, reason: "Bob is bound by chains." };
const llmProvider = new MockLLMProvider([mockValidation]); // TimeDeltaGenerator shouldn't be called
const architect = new Architect(llmProvider, repo);
const result = await architect.processIntent(
world,
"bob",
"run away",
);
expect(result.isValid).toBe(false);
expect(result.reason).toBe("Bob is bound by chains.");
expect(result.timeDelta).toBeUndefined();
// Verify clock did not advance
expect(world.clock.get().toISOString()).toBe(startTime.toISOString());
// Verify database value remained unchanged
const loaded = repo.loadWorldState("world-xyz")!;
expect(loaded.clock.get().toISOString()).toBe(startTime.toISOString());
db.close();
});
});