feat: refactor turn processing and world state management; remove obsolete files and enhance database interactions

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-27 00:09:11 -04:00
parent c32fa977a8
commit 0da62785d5
15 changed files with 375 additions and 225 deletions

View File

@@ -1,5 +1,6 @@
import type { Action } from "../../contracts/action";
import type { InterpreterOutput } from "../../contracts/intent";
import type { WorldState } from "../../contracts/world";
import type { ResolveIntentInput } from "../resolveIntent";
export const LLM_INTERPRETER_VERSION = "llm-v1-ollama";
@@ -140,20 +141,98 @@ function toReasonCode(value: string | undefined):
}
}
/**
* Build a compact world-context block so the model can resolve entity references
* (e.g. "the key" → targetId: "key_1") without inventing IDs.
*
* Includes:
* - The actor's current room
* - Every entity visible in that room (items, doors, characters)
* - Items in the actor's inventory
* - All room entities (needed to populate locationId for move actions)
*/
function buildWorldContext(worldState: WorldState, actorId: string): string {
const actor = worldState.entities[actorId];
const actorLocation = actor ? String(actor.attributes.location ?? "") : "";
const lines: string[] = [];
if (actorLocation) {
const room = worldState.entities[actorLocation];
lines.push(`actor_location_id: ${actorLocation}${room ? ` (${room.name})` : ""}`);
}
for (const entity of Object.values(worldState.entities)) {
if (entity.id === actorId) continue;
const loc = String(entity.attributes.location ?? "");
const inRoom = loc === actorLocation;
const inActorInventory = loc === `inventory:${actorId}`;
const isRoom = entity.type === "room";
if (inRoom || inActorInventory || isRoom) {
const context = inActorInventory
? "actor_inventory"
: isRoom
? "room"
: "in_actor_room";
// Emit only fields the model needs to build actions.
const extras: string[] = [];
if (entity.attributes.locked === true) extras.push("locked");
if (entity.attributes.open === true) extras.push("open");
if (entity.attributes.takeable === true) extras.push("takeable");
if (entity.attributes.openable === true) extras.push("openable");
const extrasStr = extras.length ? ` [${extras.join(",")}]` : "";
lines.push(
`entity id=${entity.id} name=${JSON.stringify(entity.name)} type=${entity.type} context=${context}${extrasStr}`
);
}
}
return lines.length ? lines.join("\n") : "(no world context available)";
}
function buildPrompt(input: ResolveIntentInput): { system: string; user: string } {
const system = [
"You are an intent-to-actions resolver for a text adventure engine.",
"Return ONLY JSON with this shape:",
'{"status":"resolved|needs_clarification|rejected","selectedActions":[{"type":"inspect|move|take|open|introduce|describe|transfer","targetId":"optional","locationId":"optional","metadata":{"optional":"object"}}],"selectedConfidence":0.0,"clarification":{"reasonCode":"UNRECOGNIZED_INTENT|AMBIGUOUS_REFERENCE|EMPTY_INPUT|LOW_CONFIDENCE|INTERNAL_INVALID_OUTPUT","question":"string","field":"verb|target|item|recipient|location"},"rationale":"brief"}',
"If unresolved, selectedActions must be an empty array and clarification must be present.",
"Use canonical action types only. Do not invent fields.",
].join(" ");
"You will receive the current world state and a player command.",
"Return ONLY a JSON object with this exact shape (no markdown, no prose):",
JSON.stringify({
status: "resolved|needs_clarification|rejected",
selectedActions: [
{
type: "inspect|move|open|take|introduce|describe|transfer",
targetId: "entity id from world context, or omit",
locationId: "room id for move, or omit",
metadata: { note: "omit if unused" },
},
],
selectedConfidence: 0.0,
clarification: {
reasonCode:
"UNRECOGNIZED_INTENT|AMBIGUOUS_REFERENCE|EMPTY_INPUT|LOW_CONFIDENCE|INTERNAL_INVALID_OUTPUT",
question: "question to ask player",
field: "verb|target|item|recipient|location",
},
rationale: "one sentence",
}),
"Rules:",
"- Use entity ids from the world context for targetId and locationId. Never invent ids.",
"- If status is resolved, selectedActions must be non-empty and clarification must be omitted.",
"- If status is not resolved, selectedActions must be empty and clarification must be present.",
"- Use only the canonical action types listed above.",
].join("\n");
const worldContext = input.worldState
? buildWorldContext(input.worldState, input.actorId)
: "(no world context provided)";
const user = [
`actorId: ${input.actorId}`,
`input: ${JSON.stringify(input.rawText)}`,
`minimum_confidence: ${input.minConfidence}`,
].join("\n");
`world_context:\n${worldContext}`,
`player_input: ${JSON.stringify(input.rawText)}`,
].join("\n\n");
return { system, user };
}

View File

@@ -1,4 +1,5 @@
import type { InterpreterOutput } from "../contracts/intent";
import type { WorldState } from "../contracts/world";
import { resolveDeterministicIntent } from "./adapters/deterministicResolver";
import { resolveLlmIntent } from "./adapters/llmResolver";
import {
@@ -11,6 +12,7 @@ const DEFAULT_MIN_CONFIDENCE = 0.65;
type InterpretTurnOptions = {
mode?: ResolverMode;
minConfidence?: number;
worldState?: WorldState;
};
function getResolverMode(options?: InterpretTurnOptions): ResolverMode {
@@ -25,6 +27,7 @@ function buildInput(rawText: string, actorId: string, options?: InterpretTurnOpt
rawText,
actorId,
minConfidence: options?.minConfidence ?? DEFAULT_MIN_CONFIDENCE,
worldState: options?.worldState,
};
}

View File

@@ -1,4 +1,5 @@
import type { InterpreterOutput } from "../contracts/intent";
import type { WorldState } from "../contracts/world";
export type ResolverMode = "deterministic" | "llm" | "hybrid";
@@ -6,6 +7,8 @@ export type ResolveIntentInput = {
rawText: string;
actorId: string;
minConfidence: number;
/** Optional world state — passed to LLM resolvers to provide entity context. */
worldState?: WorldState;
};
export type IntentResolver = {