refactor(architect): Use IPromptBuilder Interface for LLMValidator

This commit is contained in:
2026-07-19 20:31:58 +05:30
parent ee25bf4a4c
commit 7baf583b13
8 changed files with 164 additions and 60 deletions

View File

@@ -49,10 +49,12 @@ async function processIntents(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
worldState: any,
session: SimSession,
): Promise<IntentInfo[]> {
): Promise<{ intentInfos: IntentInfo[]; validatorCalls: ValidatorCall[] }> {
const intentInfos: IntentInfo[] = [];
const validatorCalls: ValidatorCall[] = [];
for (const intent of intents) {
for (let i = 0; i < intents.length; i++) {
const intent = intents[i];
const outcome = await session.architect.processIntent(worldState, intent);
const ts = worldState.clock.get().toISOString();
@@ -66,6 +68,53 @@ async function processIntents(
minutesToAdvance: outcome.timeDelta?.minutesToAdvance,
});
if (
intent.type === "action" &&
(session.architect.validator as any).lastResult
) {
const lastResult = (session.architect.validator as any).lastResult;
let usage = undefined;
if (
session.validatorProvider.lastCalls &&
session.validatorProvider.lastCalls.length > 0
) {
const valCall =
session.validatorProvider.lastCalls[
session.validatorProvider.lastCalls.length - 1
];
usage = valCall.usage;
}
validatorCalls.push({
intentIndex: i,
intentContent: intent.content,
prompt: {
systemPrompt: lastResult.systemPrompt || "",
userContext: lastResult.userContext || "",
components: lastResult.promptComponents,
},
response: {
isValid: outcome.isValid,
reason: outcome.reason,
},
usage,
});
} else {
const reason =
intent.type === "dialogue"
? "Dialogue intents represent verbal/communication actions and are automatically valid."
: "Monologue/thought intents represent internal reflections and bypass validation.";
validatorCalls.push({
intentIndex: i,
intentContent: intent.content,
response: {
isValid: true,
reason: outcome.reason || reason,
},
});
}
const actorEntry = buildBufferEntryForIntent(intent, ts, entity.locationId);
if (intent.type === "action") {
actorEntry.outcome = { isValid: outcome.isValid, reason: outcome.reason };
@@ -99,7 +148,7 @@ async function processIntents(
}
}
return intentInfos;
return { intentInfos, validatorCalls };
}
// ---------------------------------------------------------------------------
@@ -219,13 +268,21 @@ export async function processNpcTurn(
entry.decoderUsage = decoderCall.usage;
}
entry.intents = await processIntents(
const { intentInfos, validatorCalls } = await processIntents(
result.intents.intents,
info.id,
entity,
worldState,
session,
);
entry.intents = intentInfos;
entry.validatorCalls = validatorCalls;
entry.decodedIntents = result.intents.intents.map((intent) => ({
type: intent.type,
content: intent.content,
modifiers: intent.modifiers || [],
targetIds: intent.targetIds,
}));
session.log.push(entry);
session.coreRepo.saveWorldState(worldState);
@@ -303,13 +360,21 @@ export async function executePlayerAction(
entry.decoderUsage = call.usage;
}
entry.intents = await processIntents(
const { intentInfos, validatorCalls: playerValCalls } = await processIntents(
result.intents.intents,
ctx.entityId,
entity,
worldState,
session,
);
entry.intents = intentInfos;
entry.validatorCalls = playerValCalls;
entry.decodedIntents = result.intents.intents.map((intent) => ({
type: intent.type,
content: intent.content,
modifiers: intent.modifiers || [],
targetIds: intent.targetIds,
}));
session.log.push(entry);
session.coreRepo.saveWorldState(worldState);

View File

@@ -14,7 +14,7 @@ import {
LedgerRepository,
} from "@omnia/memory";
import { hydrate } from "@omnia/voice";
import { PromptComponent } from "@omnia/llm";
import { PromptComponent, IPromptBuilder, PromptBreakdown } from "@omnia/llm";
/**
* Zod schema for the structured response expected from the actor LLM.
@@ -38,7 +38,9 @@ export type ActorResponse = z.infer<typeof ActorResponseSchema>;
* ACL'd to it), its own Cognitive Buffer, and the entities co-located
* with it. System UUIDs are surfaced as subjective aliases.
*/
export class ActorPromptBuilder {
export class ActorPromptBuilder implements IPromptBuilder<
[WorldState, Entity]
> {
/**
* @param bufferRepo Used to fetch the actor's Cognitive Buffer. Optional —
* if absent, the memory section is omitted.
@@ -64,14 +66,7 @@ export class ActorPromptBuilder {
/**
* Assembles the system prompt and user context for a given entity.
*/
build(
worldState: WorldState,
entity: Entity,
): {
systemPrompt: string;
userContext: string;
components: PromptComponent[];
} {
build(worldState: WorldState, entity: Entity): PromptBreakdown {
const systemPrompt = this.buildSystemPrompt();
const { userContext, components } = this.buildUserContext(
worldState,

View File

@@ -9,7 +9,7 @@ export interface ProcessResult extends ValidationResult {
}
export class Architect {
private validator: LLMValidator;
public validator: LLMValidator;
private timeDeltaGenerator: TimeDeltaGenerator;
constructor(

View File

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

View File

@@ -0,0 +1,59 @@
import { WorldState, serializeObjectiveWorldState } from "@omnia/core";
import { Intent } from "@omnia/intent";
import { hydrateObjective } from "@omnia/voice";
import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm";
/**
* Prompt builder for the LLM Validator (World Architect).
* Separates prompt generation, structure, and component breakdowns.
*/
export class LLMValidatorPromptBuilder implements IPromptBuilder<
[WorldState, Intent]
> {
build(worldState: WorldState, intent: Intent): PromptBreakdown {
const serializedWorld = serializeObjectiveWorldState(worldState);
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 Boundaries (an actor cannot perform a task if their state doesn't allow them to do so).
- State/Attribute constraints.
- An actor can perform actions on themselves as long as it follows the boundaries stated above.
You must respond with a JSON object containing:
- "isValid": boolean indicating if the action is possible/allowed.
- "reason": a very short explanation of why the action is allowed or denied.
`.trim();
const objectiveContent = hydrateObjective(intent.content, worldState);
const worldStateSection = `=== CURRENT WORLD STATE ===\nCurrent Time: ${worldState.clock.get().toISOString()}\nEntities & Attributes:\n${serializedWorld}`;
const proposedActionSection = `=== PROPOSED ACTION ===\nActor ID: ${intent.actorId}\nType: ${intent.type}\nContent: "${objectiveContent}"\nTarget IDs: ${intent.targetIds.join(", ") || "(None)"}`;
const userContext = `${worldStateSection}\n\n${proposedActionSection}\n\nDecide if the proposed action is logically valid and physically possible.`;
const components: PromptComponent[] = [
{ label: "System Prompt", type: "system", content: systemPrompt },
{
label: "Current World State",
type: "world",
content: worldStateSection,
},
{
label: "Proposed Action",
type: "input",
content: proposedActionSection,
},
];
return {
systemPrompt,
userContext,
components,
};
}
}

View File

@@ -1,8 +1,8 @@
import { z } from "zod";
import { WorldState, serializeObjectiveWorldState } from "@omnia/core";
import { ILLMProvider } from "@omnia/llm";
import { WorldState } from "@omnia/core";
import { ILLMProvider, PromptBreakdown } from "@omnia/llm";
import { Intent } from "@omnia/intent";
import { hydrateObjective } from "@omnia/voice";
import { LLMValidatorPromptBuilder } from "./llm-validator-prompt-builder.js";
export const ValidationResultSchema = z.object({
isValid: z.boolean(),
@@ -12,7 +12,12 @@ export const ValidationResultSchema = z.object({
export type ValidationResult = z.infer<typeof ValidationResultSchema>;
export class LLMValidator {
constructor(private llmProvider: ILLMProvider) {}
public lastResult: PromptBreakdown | null = null;
private promptBuilder: LLMValidatorPromptBuilder;
constructor(private llmProvider: ILLMProvider) {
this.promptBuilder = new LLMValidatorPromptBuilder();
}
/**
* Validates an action intent against the objective world state.
@@ -25,6 +30,8 @@ export class LLMValidator {
worldState: WorldState,
intent: Intent,
): Promise<ValidationResult> {
this.lastResult = null;
// Defensive guard: monologue and thought intents bypass validation.
if (intent.type === "monologue" || intent.type === "thought") {
return {
@@ -42,41 +49,16 @@ export class LLMValidator {
};
}
// 1. Serialize the objective world state for the LLM
const serializedWorld = serializeObjectiveWorldState(worldState);
const { systemPrompt, userContext, components } = this.promptBuilder.build(
worldState,
intent,
);
// 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 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:
- "isValid": boolean indicating if the action is possible/allowed.
- "reason": a concise explanation of why the action is allowed or denied.
`.trim();
const objectiveContent = hydrateObjective(intent.content, worldState);
const userContext = `
=== CURRENT WORLD STATE ===
Current Time: ${worldState.clock.get().toISOString()}
Entities & Attributes:
${serializedWorld}
=== PROPOSED ACTION ===
Actor ID: ${intent.actorId}
Type: ${intent.type}
Content: "${objectiveContent}"
Target IDs: ${intent.targetIds.join(", ") || "(None)"}
Decide if the proposed action is logically valid and physically possible.
`.trim();
this.lastResult = {
systemPrompt,
userContext,
components,
};
// structured call via the LLM provider
const response = await this.llmProvider.generateStructuredResponse({

View File

@@ -1,14 +1,14 @@
import { WorldState, resolveAlias } from "@omnia/core";
import { PromptBreakdown, PromptComponent } from "@omnia/llm";
import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm";
import { Intent } from "./intent.js";
// TODO: Builder a generic interface for prompt builders in @omnia/llm: IPromptBuilder or something
/**
* Prompt builder for the Intent Decoder.
* Separates prompt generation, structure, and component breakdowns.
*/
export class IntentDecoderPromptBuilder {
export class IntentDecoderPromptBuilder implements IPromptBuilder<
[WorldState, string, string, Intent[]]
> {
build(
worldState: WorldState,
actorId: string,

View File

@@ -1,12 +1,14 @@
import { Entity } from "@omnia/core";
import { PromptBreakdown, PromptComponent } from "@omnia/llm";
import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm";
import { BufferEntry, serializeSubjectiveBufferEntry } from "./buffer.js";
/**
* Prompt builder for the Handoff Engine.
* Separates prompt generation, structure, and component breakdowns.
*/
export class HandoffPromptBuilder {
export class HandoffPromptBuilder implements IPromptBuilder<
[Entity, BufferEntry[], Date]
> {
build(entity: Entity, candidates: BufferEntry[], now: Date): PromptBreakdown {
const candidatesList = candidates
.map((entry) => {