refactor: Unified prompt composition, structured log analysis, parallel validator tracing, and intent hydration fixes

This commit is contained in:
2026-07-19 22:22:58 +05:30
committed by GitHub
74 changed files with 2269 additions and 952 deletions

View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -19,6 +19,7 @@
"@omnia/memory": "workspace:*",
"@omnia/scenario": "workspace:*",
"@omnia/spatial": "workspace:*",
"@omnia/voice": "workspace:*",
"@radix-ui/react-dialog": "^1.1.19",
"@radix-ui/react-separator": "^1.1.11",
"@radix-ui/react-slot": "^1.3.0",

View File

@@ -177,13 +177,13 @@ export function ConfigView() {
{
key: "handoff",
label: "Memory Handoff Engine",
desc: "Promotes entities' working memories to the long-term Ledger via LLM summarization and pruning.",
desc: "Promotes entities' Cognitive Buffer entries to the Memory Ledger via LLM summarization and pruning.",
type: "generative",
},
{
key: "embeddings",
label: "Text Embeddings Generator",
desc: "Generates vector embeddings for long-term memory retrieval.",
desc: "Generates vector embeddings for Memory Ledger retrieval.",
type: "embedding",
},
].map((task) => (

View File

@@ -43,13 +43,7 @@ import {
CardTitle,
CardAction,
} from "@/components/ui/card";
import {
Item,
ItemContent,
ItemGroup,
ItemTitle,
ItemDescription,
} from "@/components/ui/item";
import { Item, ItemContent, ItemGroup, ItemTitle } from "@/components/ui/item";
import { Empty, EmptyTitle, EmptyDescription } from "@/components/ui/empty";
import { cn } from "@/lib/utils";
import { RefreshCwIcon } from "lucide-react";
@@ -200,14 +194,12 @@ export function ProviderInstancesConfig({
fetchModelsForExistingInstance(selectedInstanceId);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedInstanceId, instances, availableProviders]);
// Re-fetch models when provider/key/endpoint changes on new instance form
useEffect(() => {
if (selectedInstanceId !== "new") return;
fetchModelsForNewInstance(editProvider, editKey, editEndpointUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editProvider, editKey, editEndpointUrl, selectedInstanceId]);
const handleProviderChange = (providerId: string | null) => {

View File

@@ -0,0 +1,254 @@
"use client";
import { useState } from "react";
import type { SimSnapshot } from "@/lib/simulation-types";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { PromptAnalyzer } from "@/components/play/PromptAnalyzer";
interface HandoffModalProps {
entry: SimSnapshot["log"][number];
onClose: () => void;
}
export function HandoffModal({ entry, onClose }: HandoffModalProps) {
const [activeTab, setActiveTab] = useState<"chunks" | "prompt" | "output">(
"chunks",
);
const handoffResult = entry.handoffResult;
const chunks = handoffResult?.chunks || [];
const getImportanceColor = (score: number) => {
if (score >= 8)
return "bg-destructive/10 text-destructive border-destructive/30";
if (score >= 5) return "bg-amber-500/10 text-amber-500 border-amber-500/30";
return "bg-emerald-500/10 text-emerald-500 border-emerald-500/30";
};
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-[800px] sm:max-w-[800px] h-[85vh] overflow-hidden flex flex-col p-0 gap-0 border-2">
<DialogHeader className="px-6 pt-5 pb-4 border-b">
<DialogTitle className="text-lg font-head tracking-wide text-primary flex items-center justify-between">
<span>Memory Handoff Details &mdash; {entry.entityName}</span>
{entry.usage && (
<span className="text-xs font-mono font-normal text-muted-foreground">
{entry.usage.modelName || "Handoff Model"}
</span>
)}
</DialogTitle>
</DialogHeader>
{/* Custom Tab Switcher */}
<div className="flex border-b bg-muted/20 px-6 py-2 gap-2">
<button
onClick={() => setActiveTab("chunks")}
className={`px-3 py-1.5 text-xs font-medium border transition-all duration-100 ${
activeTab === "chunks"
? "border-primary bg-primary/10 text-primary shadow-[1px_1px_0_0_var(--primary)]"
: "border-transparent hover:bg-secondary text-muted-foreground"
}`}
>
Promoted Chunks ({chunks.length})
</button>
<button
onClick={() => setActiveTab("prompt")}
className={`px-3 py-1.5 text-xs font-medium border transition-all duration-100 ${
activeTab === "prompt"
? "border-primary bg-primary/10 text-primary shadow-[1px_1px_0_0_var(--primary)]"
: "border-transparent hover:bg-secondary text-muted-foreground"
}`}
>
Raw LLM Prompt
</button>
<button
onClick={() => setActiveTab("output")}
className={`px-3 py-1.5 text-xs font-medium border transition-all duration-100 ${
activeTab === "output"
? "border-primary bg-primary/10 text-primary shadow-[1px_1px_0_0_var(--primary)]"
: "border-transparent hover:bg-secondary text-muted-foreground"
}`}
>
Raw JSON Output
</button>
</div>
<div className="overflow-y-auto flex-1 p-6 space-y-4">
{activeTab === "chunks" && (
<div className="space-y-4">
{entry.usage && (
<div className="grid grid-cols-3 gap-4 border border-dotted border-border/20 p-3 bg-secondary/10 rounded text-xs font-mono">
<div>
<span className="text-muted-foreground block uppercase tracking-wider text-[10px]">
Input Tokens
</span>
<strong className="text-foreground">
{entry.usage.inputTokens}
</strong>
</div>
<div>
<span className="text-muted-foreground block uppercase tracking-wider text-[10px]">
Output Tokens
</span>
<strong className="text-foreground">
{entry.usage.outputTokens}
</strong>
</div>
<div>
<span className="text-muted-foreground block uppercase tracking-wider text-[10px]">
Total Tokens
</span>
<strong className="text-foreground">
{entry.usage.totalTokens}
</strong>
</div>
</div>
)}
{chunks.length === 0 ? (
<div className="border border-dotted border-border/30 p-8 text-center bg-card text-muted-foreground rounded">
<p className="text-sm">
No memories were promoted to the Memory Ledger during this
turn.
</p>
<p className="text-xs mt-1">
All Cognitive Buffer entries were summarized or forgotten.
</p>
</div>
) : (
<div className="space-y-4">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
Memory Ledger Additions
</h3>
{chunks.map(
(
chunk: { content: string; importance: number },
index: number,
) => (
<div
key={index}
className="border border-border/30 bg-card p-4 shadow-sm relative flex flex-col gap-3"
>
<div className="flex justify-between items-start gap-4">
<div className="flex-1 text-sm text-foreground/90 leading-relaxed font-sans">
{chunk.content}
</div>
<Badge
variant="outline"
className={`font-mono text-xs ${getImportanceColor(chunk.importance)}`}
>
Importance: {chunk.importance}
</Badge>
</div>
{chunk.quotes && chunk.quotes.length > 0 && (
<div className="bg-secondary/10 border-l-2 border-primary/50 p-2.5 my-1 text-xs italic text-muted-foreground space-y-1">
{chunk.quotes.map((quote: string, qIdx: number) => (
<div key={qIdx}>&ldquo;{quote}&rdquo;</div>
))}
</div>
)}
<div className="flex flex-wrap gap-2 text-xs pt-2 border-t border-dotted border-border/10">
{chunk.retainInBuffer ? (
<Badge
variant="outline"
className="bg-primary/5 text-primary border-primary/20 text-[10px] font-mono"
>
Pinned in Buffer
</Badge>
) : (
<Badge
variant="outline"
className="bg-muted text-muted-foreground border-border/20 text-[10px] font-mono"
>
Pruned from Buffer
</Badge>
)}
{chunk.involvedEntityIds &&
chunk.involvedEntityIds.length > 0 && (
<div className="flex items-center gap-1.5 ml-auto text-[10px] font-mono text-muted-foreground">
<span>Entities:</span>
{chunk.involvedEntityIds.map(
(entId: string) => (
<Badge
key={entId}
variant="outline"
className="text-[10px] px-1 py-0 border-border/20 font-mono"
>
{entId}
</Badge>
),
)}
</div>
)}
</div>
</div>
),
)}
</div>
)}
</div>
)}
{activeTab === "prompt" && entry.rawPrompt && (
<PromptAnalyzer
components={
entry.rawPrompt.components &&
entry.rawPrompt.components.length > 0
? entry.rawPrompt.components
: [
{
label: "System Prompt",
type: "system",
content: entry.rawPrompt.systemPrompt || "",
},
{
label: "User Context",
type: "world",
content: entry.rawPrompt.userContext || "",
},
]
}
inputTokens={entry.usage?.inputTokens || 0}
maxContext={
entry.usage?.maxContext !== undefined
? entry.usage.maxContext
: 32768
}
modelName={entry.usage?.modelName}
providerInstanceName={entry.usage?.providerInstanceName}
outputLabel="LLM Output (Promoted Memory Chunks)"
outputText={
handoffResult
? JSON.stringify(handoffResult, null, 2)
: undefined
}
outputTokens={entry.usage?.outputTokens}
/>
)}
{activeTab === "output" && (
<div className="space-y-2 h-full flex flex-col">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
Raw JSON Output
</h4>
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border flex-1 overflow-y-auto max-h-[500px]">
{handoffResult
? JSON.stringify(handoffResult, null, 2)
: "No JSON Output recorded."}
</pre>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -7,16 +7,28 @@ import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";
import { hydrate } from "@omnia/voice";
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
function IntentTag({
intent,
isSelf,
playerAliases,
playerId,
entities,
}: {
intent: SimSnapshot["log"][number]["intents"][number];
isSelf?: boolean;
playerAliases: Record<string, string>;
playerId: string;
entities: SimSnapshot["entities"];
}) {
const labels: Record<string, string> = {
monologue: "thought",
thought: "thought",
dialogue: "dialogue",
action: "action",
};
@@ -28,10 +40,27 @@ function IntentTag({
outcome = intent.isValid ? " ✅" : ` ❌ (${intent.reason})`;
}
const textToDisplay =
isSelf && intent.selfDescription
? intent.selfDescription
: intent.description;
const viewerAliasesMap = new Map<string, string>();
if (entities) {
for (const ent of entities) {
viewerAliasesMap.set(ent.id, ent.name || ent.id);
}
}
if (playerAliases) {
for (const [targetId, alias] of Object.entries(playerAliases)) {
viewerAliasesMap.set(targetId, alias);
}
}
const viewerEntityMock = {
id: playerId || "",
aliases: viewerAliasesMap,
};
const textToDisplay = hydrate(
intent.content,
viewerEntityMock as unknown as Parameters<typeof hydrate>[1],
);
const modifiersStr =
intent.modifiers && intent.modifiers.length > 0 ? (
@@ -69,10 +98,16 @@ function LogEntryCard({
entry,
onShowPrompt,
isPlayerCard,
playerAliases,
playerId,
entities,
}: {
entry: SimSnapshot["log"][number];
onShowPrompt: (entry: SimSnapshot["log"][number]) => void;
isPlayerCard: boolean;
playerAliases: Record<string, string>;
playerId: string;
entities: SimSnapshot["entities"];
}) {
const showMenu = !!(entry.rawPrompt || entry.decoderPrompt);
@@ -110,7 +145,13 @@ function LogEntryCard({
</div>
<div className="flex flex-col gap-1.5 mt-2 border-t border-dotted border-border/10 pt-2">
{entry.intents.map((intent, i) => (
<IntentTag key={i} intent={intent} isSelf={isPlayerCard} />
<IntentTag
key={i}
intent={intent}
playerAliases={playerAliases}
playerId={playerId}
entities={entities}
/>
))}
</div>
</div>
@@ -125,6 +166,7 @@ interface InteractViewProps {
setPlayerInput: (value: string) => void;
onSubmitAction: (e: React.FormEvent<HTMLFormElement>) => void;
onShowPrompt: (entry: SimSnapshot["log"][number]) => void;
onShowHandoff: (entry: SimSnapshot["log"][number]) => void;
logEndRef: React.RefObject<HTMLDivElement | null>;
}
@@ -136,6 +178,7 @@ export function InteractView({
setPlayerInput,
onSubmitAction,
onShowPrompt,
onShowHandoff,
logEndRef,
}: InteractViewProps) {
const router = useRouter();
@@ -147,14 +190,49 @@ export function InteractView({
{/* Scrollable Center Viewport */}
<main className="flex-1 overflow-y-auto px-8 py-6">
<div className="flex flex-col gap-4 max-w-[800px] mx-auto pb-12">
{snapshot.log.map((entry, i) => (
<LogEntryCard
key={i}
entry={entry}
onShowPrompt={onShowPrompt}
isPlayerCard={entry.entityId === playerEntity?.id}
/>
))}
{snapshot.log.map((entry, i) => {
if (entry.isHandoff) {
return (
<Alert
key={i}
className="max-w-md border-dashed bg-secondary/10"
>
<div className="flex-1">
<AlertTitle>
Handoff triggered for {entry.entityName}
</AlertTitle>
<AlertDescription>
Memories were transferred from Cognitive Buffer to Memory
Ledger
</AlertDescription>
</div>
<AlertAction>
<Button
size="xs"
variant="default"
onClick={() => onShowHandoff(entry)}
>
View Details
</Button>
</AlertAction>
</Alert>
);
}
const playerAliases = playerEntity?.aliases || {};
const playerId = playerEntity?.id || "";
return (
<LogEntryCard
key={i}
entry={entry}
onShowPrompt={onShowPrompt}
isPlayerCard={entry.entityId === playerEntity?.id}
playerAliases={playerAliases}
playerId={playerId}
entities={snapshot.entities}
/>
);
})}
{loading && (
<div className="flex items-center gap-2 text-sm italic text-muted-foreground p-2 font-mono">
<Spinner />

View File

@@ -11,6 +11,7 @@ import type { SimSnapshot } from "@/lib/simulation-types";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { PromptModal } from "./PromptModal";
import { HandoffModal } from "./HandoffModal";
import { InteractView } from "./InteractView";
import { ManageView } from "./ManageView";
import { cn } from "@/lib/utils";
@@ -56,6 +57,9 @@ export function PlayView() {
const [selectedEntryForModal, setSelectedEntryForModal] = useState<
SimSnapshot["log"][number] | null
>(null);
const [selectedHandoffForModal, setSelectedHandoffForModal] = useState<
SimSnapshot["log"][number] | null
>(null);
const logEndRef = useRef<HTMLDivElement>(null);
const steppingRef = useRef(false);
@@ -384,6 +388,7 @@ export function PlayView() {
setPlayerInput={setPlayerInput}
onSubmitAction={handleSubmitAction}
onShowPrompt={setSelectedEntryForModal}
onShowHandoff={setSelectedHandoffForModal}
logEndRef={logEndRef}
/>
) : (
@@ -403,6 +408,13 @@ export function PlayView() {
onClose={() => setSelectedEntryForModal(null)}
/>
)}
{selectedHandoffForModal && (
<HandoffModal
entry={selectedHandoffForModal}
onClose={() => setSelectedHandoffForModal(null)}
/>
)}
</div>
</SidebarProvider>
);

View File

@@ -0,0 +1,172 @@
"use client";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import type { PromptComponent } from "@/lib/simulation-types";
interface PromptAnalyzerProps {
components: PromptComponent[];
inputTokens: number;
maxContext?: number;
modelName?: string;
providerInstanceName?: string;
outputLabel?: string;
outputText?: string;
outputTokens?: number;
}
export function PromptAnalyzer({
components,
inputTokens,
maxContext = 32768,
modelName,
providerInstanceName,
outputLabel = "LLM Output",
outputText,
outputTokens,
}: PromptAnalyzerProps) {
const totalLen = components.reduce((sum, s) => sum + s.content.length, 0);
if (totalLen === 0) {
return (
<div className="text-sm italic text-muted-foreground">
No prompt context recorded.
</div>
);
}
const sections = components.map((s) => {
const pct = totalLen > 0 ? (s.content.length / totalLen) * 100 : 0;
return {
...s,
pct,
tokens: Math.round((s.content.length / totalLen) * inputTokens),
};
});
const usagePctOfContext =
maxContext > 0 ? (inputTokens / maxContext) * 100 : 0;
const isAbsolute = maxContext > 0 && usagePctOfContext >= 20;
const getColorClass = (type: string) => {
switch (type) {
case "system":
return "bg-blue-500";
case "world":
return "bg-emerald-500";
case "events":
return "bg-purple-500";
case "memories":
return "bg-pink-500";
case "input":
return "bg-amber-500";
default:
return "bg-slate-500";
}
};
return (
<div className="flex flex-col gap-4">
{/* Provider Details */}
{(providerInstanceName || modelName) && (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
<strong>LLM Instance:</strong>{" "}
<span>{providerInstanceName || "Default"}</span>
{modelName && <span> ({modelName})</span>}
</div>
)}
{/* Progress Bar & Breakdown */}
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
<span className="font-semibold">Input Prompt Breakdown</span>
<span>
Total Input Tokens: <strong>{inputTokens}</strong>
{maxContext > 0 ? (
<span>
{" "}
/ {maxContext} ({usagePctOfContext.toFixed(1)}% used)
</span>
) : (
<span> (infinite context)</span>
)}
</span>
</div>
{/* Token Bar */}
<div className="flex h-6 w-full rounded border overflow-hidden bg-muted shadow-inner mb-2">
{sections.map((item, idx) => {
const widthPct = isAbsolute
? item.pct * (inputTokens / maxContext)
: item.pct;
return (
<div
key={idx}
className={`h-full transition-all duration-300 ${getColorClass(item.type)}`}
style={{ width: `${widthPct}%` }}
title={`${item.label}: ${item.tokens} tokens (${item.pct.toFixed(1)}%)`}
/>
);
})}
{isAbsolute && (
<div
className="bg-white h-full"
style={{ width: `${100 - usagePctOfContext}%` }}
title={`Available: ${maxContext - inputTokens} tokens (${(100 - usagePctOfContext).toFixed(1)}% remaining)`}
/>
)}
</div>
{/* Accordion Components */}
<Accordion type="multiple" className="w-full">
{sections.map((item, idx) => {
return (
<AccordionItem key={idx} value={String(idx)}>
<AccordionTrigger className="text-sm py-2.5 hover:no-underline">
<div className="flex items-center gap-2">
<span
className={`inline-block w-2.5 h-2.5 rounded-sm ${getColorClass(item.type)}`}
/>
<span>{item.label}:</span>
<span className="text-muted-foreground font-normal">
<strong>{item.tokens}</strong> tokens (
{item.pct.toFixed(0)}%)
</span>
</div>
</AccordionTrigger>
<AccordionContent>
<pre className="m-0 p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[300px] overflow-y-auto">
{item.content}
</pre>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
{/* Output Section */}
{outputText && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2 font-mono">
<span className="font-semibold">{outputLabel}</span>
{outputTokens !== undefined && (
<span>
Total Output Tokens: <strong>{outputTokens}</strong>
</span>
)}
</div>
<div className="rounded border-2">
<pre className="m-0 p-3 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground max-h-[250px] overflow-y-auto">
{outputText}
</pre>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import type { SimSnapshot } from "@/lib/simulation-types";
import type { SimSnapshot, PromptBreakdown } from "@/lib/simulation-types";
import {
Dialog,
DialogContent,
@@ -9,13 +9,8 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/components/ui/accordion";
import { PromptSwitcher } from "@/components/play/PromptSwitcher";
import { PromptAnalyzer } from "@/components/play/PromptAnalyzer";
interface PromptModalProps {
entry: SimSnapshot["log"][number];
@@ -23,172 +18,7 @@ interface PromptModalProps {
}
export function PromptModal({ entry, onClose }: PromptModalProps) {
const [activeTab, setActiveTab] = useState<"actor" | "decoder">("actor");
const parseActorPrompt = (
systemPrompt: string,
userContext: string,
inputTokens: number,
) => {
const recentHeader = "=== RECENT EVENTS ===";
const ledgerHeader = "=== YOUR MEMORIES ===";
const recentIdx = userContext.indexOf(recentHeader);
let worldStr = userContext;
let recentStr = "";
let ledgerStr = "";
if (recentIdx !== -1) {
worldStr = userContext.substring(0, recentIdx).trim();
const rest = userContext.substring(recentIdx).trim();
const ledgerIdx = rest.indexOf(ledgerHeader);
if (ledgerIdx !== -1) {
recentStr = rest.substring(0, ledgerIdx).trim();
ledgerStr = rest.substring(ledgerIdx).trim();
} else {
recentStr = rest;
}
}
const sections: { label: string; type: string; content: string }[] = [
{ label: "System Prompt", type: "system", content: systemPrompt },
{ label: "World Info", type: "world", content: worldStr },
{
label: "Recent Events",
type: "events",
content: recentStr || "(No recent events.)",
},
{
label: "Long-Term Memories",
type: "memories",
content: ledgerStr || "(No long-term memories.)",
},
];
const totalLen = sections.reduce((sum, s) => sum + s.content.length, 0);
if (totalLen === 0) return null;
return sections.map((s) => {
const pct = (s.content.length / totalLen) * 100;
return {
...s,
pct,
relativePct: pct,
tokens: Math.round((s.content.length / totalLen) * inputTokens),
};
});
};
const parseDecoderPrompt = (
systemPrompt: string,
userContext: string,
inputTokens: number,
) => {
const proseHeader = "=== NARRATIVE PROSE ===";
const idx = userContext.indexOf(proseHeader);
let worldStr = userContext;
let proseStr = "";
if (idx !== -1) {
worldStr = userContext.substring(0, idx).trim();
proseStr = userContext.substring(idx).trim();
}
const sysLen = systemPrompt.length;
const worldLen = worldStr.length;
const proseLen = proseStr.length;
const totalLen = sysLen + worldLen + proseLen;
if (totalLen === 0) return null;
const sysPct = (sysLen / totalLen) * 100;
const worldPct = (worldLen / totalLen) * 100;
const prosePct = (proseLen / totalLen) * 100;
const sysTokens = Math.round((sysLen / totalLen) * inputTokens);
const worldTokens = Math.round((worldLen / totalLen) * inputTokens);
const proseTokens = Math.max(0, inputTokens - sysTokens - worldTokens);
return [
{
label: "System Prompt",
pct: sysPct,
relativePct: sysPct,
tokens: sysTokens,
type: "system",
content: systemPrompt,
},
{
label: "Decoder Context",
pct: worldPct,
relativePct: worldPct,
tokens: worldTokens,
type: "world",
content: worldStr,
},
{
label: "Narrative Prose",
pct: prosePct,
relativePct: prosePct,
tokens: proseTokens,
type: "memories",
content: proseStr,
},
];
};
const actorBreakdown =
entry.rawPrompt && entry.usage
? parseActorPrompt(
entry.rawPrompt.systemPrompt,
entry.rawPrompt.userContext,
entry.usage.inputTokens,
)
: null;
const decoderBreakdown =
entry.decoderPrompt && entry.decoderUsage
? parseDecoderPrompt(
entry.decoderPrompt.systemPrompt,
entry.decoderPrompt.userContext,
entry.decoderUsage.inputTokens,
)
: null;
const actorMaxContext =
entry.usage?.maxContext !== undefined ? entry.usage.maxContext : 32768;
const actorUsedTokens = entry.usage?.inputTokens || 0;
const actorUsagePctOfContext =
actorMaxContext > 0 ? (actorUsedTokens / actorMaxContext) * 100 : 0;
const isActorAbsolute = actorMaxContext > 0 && actorUsagePctOfContext >= 20;
const scaledActorBreakdown = actorBreakdown
? actorBreakdown.map((item) => ({
...item,
pct: isActorAbsolute
? item.relativePct * (actorUsedTokens / actorMaxContext)
: item.relativePct,
}))
: null;
const decoderMaxContext =
entry.decoderUsage?.maxContext !== undefined
? entry.decoderUsage.maxContext
: 32768;
const decoderUsedTokens = entry.decoderUsage?.inputTokens || 0;
const decoderUsagePctOfContext =
decoderMaxContext > 0 ? (decoderUsedTokens / decoderMaxContext) * 100 : 0;
const isDecoderAbsolute =
decoderMaxContext > 0 && decoderUsagePctOfContext >= 20;
const scaledDecoderBreakdown = decoderBreakdown
? decoderBreakdown.map((item) => ({
...item,
pct: isDecoderAbsolute
? item.relativePct * (decoderUsedTokens / decoderMaxContext)
: item.relativePct,
}))
: null;
const [activeTab, setActiveTab] = useState<string>("actor");
useEffect(() => {
if (!entry.rawPrompt && entry.decoderPrompt) {
@@ -196,9 +26,47 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
}
}, [entry]);
// Helper to resolve components with a fallback if none exist (for backwards-compatibility)
const getComponents = (
promptBreakdown: PromptBreakdown | null | undefined,
defaultType: "world" | "input",
) => {
if (!promptBreakdown) return [];
if (promptBreakdown.components && promptBreakdown.components.length > 0) {
return promptBreakdown.components;
}
// Fallback: convert flat strings into components list
return [
{
label: "System Prompt",
type: "system" as const,
content: promptBreakdown.systemPrompt || "",
},
{
label: "User Context",
type: defaultType,
content: promptBreakdown.userContext || "",
},
];
};
const actorComponents = getComponents(entry.rawPrompt, "world");
const decoderComponents = getComponents(entry.decoderPrompt, "input");
const isValidatorTab = activeTab.startsWith("validator-");
const validatorIndex = isValidatorTab
? parseInt(activeTab.substring("validator-".length), 10)
: -1;
const validatorCall = isValidatorTab
? entry.validatorCalls?.find((c) => c.intentIndex === validatorIndex)
: null;
const validatorComponents = validatorCall
? getComponents(validatorCall.prompt, "world")
: [];
return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-[750px] sm:max-w-[750px] h-[90vh] overflow-hidden flex flex-col p-0 gap-0">
<DialogContent className="max-w-187.5 sm:max-w-187.5 h-[90vh] overflow-hidden flex flex-col p-0 gap-0">
<DialogHeader className="px-6 pt-5 pb-4 border-b">
<DialogTitle className="text-lg">
Raw Prompts & Token Usage ({entry.entityName})
@@ -210,244 +78,78 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
onTabChange={setActiveTab}
hasActor={!!entry.rawPrompt}
hasDecoder={!!entry.decoderPrompt}
validatorCalls={
entry.validatorCalls?.map((c) => ({
intentIndex: c.intentIndex,
intentContent: c.intentContent,
})) || []
}
/>
<div className="overflow-y-auto flex-1 p-5">
{activeTab === "actor" && entry.rawPrompt && (
<div className="flex flex-col gap-4">
{entry.usage ? (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
<strong>LLM Instance:</strong>{" "}
<span>{entry.usage.providerInstanceName || "Default"}</span>
{entry.usage.modelName && (
<span> ({entry.usage.modelName})</span>
)}
</div>
) : (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm italic text-muted-foreground">
No LLM token usage (Player turn used fixed prose).
</div>
)}
{scaledActorBreakdown && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
<span className="font-semibold">
Input Prompt Breakdown
</span>
<span>
Total Input Tokens: <strong>{actorUsedTokens}</strong>
{actorMaxContext > 0 ? (
<span>
{" "}
/ {actorMaxContext} (
{actorUsagePctOfContext.toFixed(1)}% used)
</span>
) : (
<span> (infinite context)</span>
)}
</span>
</div>
<div className="flex h-6 w-full rounded border overflow-hidden bg-muted shadow-inner mb-2">
{scaledActorBreakdown.map((item, idx) => {
const displayPct =
actorMaxContext > 0
? (item.tokens / actorMaxContext) * 100
: item.relativePct;
return (
<div
key={idx}
className={`h-full transition-all duration-300 ${
item.type === "system"
? "bg-blue-500"
: item.type === "world"
? "bg-emerald-500"
: item.type === "memories"
? "bg-purple-500"
: "bg-amber-500"
}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
/>
);
})}
{isActorAbsolute && (
<div
className="bg-white h-full"
style={{ width: `${100 - actorUsagePctOfContext}%` }}
title={`Available: ${actorMaxContext - actorUsedTokens} tokens (${(100 - actorUsagePctOfContext).toFixed(1)}% remaining)`}
/>
)}
</div>
<Accordion type="multiple">
{scaledActorBreakdown.map((item, idx) => {
const displayPct =
actorMaxContext > 0
? (item.tokens / actorMaxContext) * 100
: item.relativePct;
return (
<AccordionItem key={idx} value={String(idx)}>
<AccordionTrigger className="text-sm">
<span
className={`inline-block w-2.5 h-2.5 rounded-sm mr-2 ${
item.type === "system"
? "bg-blue-500"
: item.type === "world"
? "bg-emerald-500"
: item.type === "memories"
? "bg-purple-500"
: "bg-amber-500"
}`}
/>
{item.label}: <strong>{item.tokens}</strong> tokens
({displayPct.toFixed(0)}%)
</AccordionTrigger>
<AccordionContent>
<pre className="m-0 p-2 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground">
{item.content}
</pre>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
)}
{entry.usage && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2">
<span className="font-semibold">LLM Output</span>
<span>
Total Output Tokens:{" "}
<strong>{entry.usage.outputTokens}</strong>
</span>
</div>
<div className="rounded border-2">
<pre className="m-0 p-2 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground">
{entry.narrativeProse}
</pre>
</div>
</div>
)}
</div>
<PromptAnalyzer
components={actorComponents}
inputTokens={entry.usage?.inputTokens || 0}
maxContext={
entry.usage?.maxContext !== undefined
? entry.usage.maxContext
: 32768
}
modelName={entry.usage?.modelName}
providerInstanceName={entry.usage?.providerInstanceName}
outputLabel="LLM Output (Narrative Prose)"
outputText={entry.narrativeProse}
outputTokens={entry.usage?.outputTokens}
/>
)}
{activeTab === "decoder" && entry.decoderPrompt && (
<div className="flex flex-col gap-4">
{entry.decoderUsage && (
<div className="rounded border-2 bg-muted/50 px-3 py-2 text-sm text-muted-foreground">
<strong>LLM Instance:</strong>{" "}
<span>
{entry.decoderUsage.providerInstanceName || "Default"}
</span>
{entry.decoderUsage.modelName && (
<span> ({entry.decoderUsage.modelName})</span>
)}
</div>
<PromptAnalyzer
components={decoderComponents}
inputTokens={entry.decoderUsage?.inputTokens || 0}
maxContext={
entry.decoderUsage?.maxContext !== undefined
? entry.decoderUsage.maxContext
: 32768
}
modelName={entry.decoderUsage?.modelName}
providerInstanceName={entry.decoderUsage?.providerInstanceName}
outputLabel="LLM Output (Decoded Intent Sequence)"
outputText={JSON.stringify(
entry.decodedIntents || entry.intents,
null,
2,
)}
outputTokens={entry.decoderUsage?.outputTokens}
/>
)}
{scaledDecoderBreakdown && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
<span className="font-semibold">
Input Prompt Breakdown
</span>
<span>
Total Input Tokens: <strong>{decoderUsedTokens}</strong>
{decoderMaxContext > 0 ? (
<span>
{" "}
/ {decoderMaxContext} (
{decoderUsagePctOfContext.toFixed(1)}% used)
</span>
) : (
<span> (infinite context)</span>
)}
</span>
</div>
<div className="flex h-6 w-full rounded border overflow-hidden bg-muted shadow-inner mb-2">
{scaledDecoderBreakdown.map((item, idx) => {
const displayPct =
decoderMaxContext > 0
? (item.tokens / decoderMaxContext) * 100
: item.relativePct;
return (
<div
key={idx}
className={`h-full transition-all duration-300 ${
item.type === "system"
? "bg-blue-500"
: item.type === "world"
? "bg-emerald-500"
: item.type === "memories"
? "bg-purple-500"
: "bg-amber-500"
}`}
style={{ width: `${item.pct}%` }}
title={`${item.label}: ${item.tokens} tokens (${displayPct.toFixed(1)}%)`}
/>
);
})}
{isDecoderAbsolute && (
<div
className="bg-white h-full"
style={{ width: `${100 - decoderUsagePctOfContext}%` }}
title={`Available: ${decoderMaxContext - decoderUsedTokens} tokens (${(100 - decoderUsagePctOfContext).toFixed(1)}% remaining)`}
/>
)}
</div>
<Accordion type="multiple">
{scaledDecoderBreakdown.map((item, idx) => {
const displayPct =
decoderMaxContext > 0
? (item.tokens / decoderMaxContext) * 100
: item.relativePct;
return (
<AccordionItem key={idx} value={String(idx)}>
<AccordionTrigger className="text-sm">
<span
className={`inline-block w-2.5 h-2.5 rounded-sm mr-2 ${
item.type === "system"
? "bg-blue-500"
: item.type === "world"
? "bg-emerald-500"
: item.type === "memories"
? "bg-purple-500"
: "bg-amber-500"
}`}
/>
{item.label}: <strong>{item.tokens}</strong> tokens
({displayPct.toFixed(0)}%)
</AccordionTrigger>
<AccordionContent>
<pre className="m-0 p-2 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground">
{item.content}
</pre>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
)}
{validatorCall && validatorCall.prompt && (
<PromptAnalyzer
components={validatorComponents}
inputTokens={validatorCall.usage?.inputTokens || 0}
maxContext={
validatorCall.usage?.maxContext !== undefined
? validatorCall.usage.maxContext
: 32768
}
modelName={validatorCall.usage?.modelName}
providerInstanceName={validatorCall.usage?.providerInstanceName}
outputLabel={`LLM Output (Validation for: "${validatorCall.intentContent}")`}
outputText={JSON.stringify(validatorCall.response, null, 2)}
outputTokens={validatorCall.usage?.outputTokens}
/>
)}
{entry.decoderUsage && (
<div>
<div className="flex justify-between items-center text-xs text-muted-foreground mb-2">
<span className="font-semibold">LLM Output</span>
<span>
Total Output Tokens:{" "}
<strong>{entry.decoderUsage.outputTokens}</strong>
</span>
</div>
<div className="rounded border-2">
<pre className="m-0 p-2 bg-muted text-xs font-mono whitespace-pre-wrap text-foreground">
{JSON.stringify(entry.intents, null, 2)}
</pre>
</div>
</div>
)}
{validatorCall && !validatorCall.prompt && (
<div className="flex flex-col items-center justify-center border border-dashed rounded-lg bg-muted/20 text-muted-foreground p-8 my-6">
<span className="text-sm font-semibold mb-2 text-foreground">
Bypassed LLM Validation
</span>
<p className="text-xs text-center text-muted-foreground max-w-md">
{validatorCall.response.reason}
</p>
</div>
)}
</div>

View File

@@ -1,10 +1,11 @@
"use client";
interface PromptSwitcherProps {
activeTab: "actor" | "decoder";
onTabChange: (tab: "actor" | "decoder") => void;
activeTab: string;
onTabChange: (tab: string) => void;
hasActor: boolean;
hasDecoder: boolean;
validatorCalls?: { intentIndex: number; intentContent: string }[];
}
export function PromptSwitcher({
@@ -12,32 +13,67 @@ export function PromptSwitcher({
onTabChange,
hasActor,
hasDecoder,
validatorCalls = [],
}: PromptSwitcherProps) {
return (
<div className="flex items-center justify-center gap-4 border-b bg-muted/50 px-5 py-4">
<button
onClick={() => onTabChange("actor")}
disabled={!hasActor}
className={`flex h-14 w-40 items-center justify-center border-2 text-sm font-medium transition-all ${
activeTab === "actor"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border/30 bg-card text-foreground hover:border-primary/50"
} disabled:cursor-not-allowed disabled:opacity-40`}
>
Actor Prompt
</button>
<span className="text-xl text-muted-foreground"></span>
<button
onClick={() => onTabChange("decoder")}
disabled={!hasDecoder}
className={`flex h-14 w-44 items-center justify-center border-2 text-sm font-medium transition-all ${
activeTab === "decoder"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border/30 bg-card text-foreground hover:border-primary/50"
} disabled:cursor-not-allowed disabled:opacity-40`}
>
Intent Decoder
</button>
<div className="flex items-center justify-center gap-4 border-b bg-muted/40 px-6 py-5 overflow-x-auto">
{/* Primary Pipeline (Linear flow to the left) */}
<div className="flex items-center gap-3 shrink-0">
<button
onClick={() => onTabChange("actor")}
disabled={!hasActor}
className={`flex h-12 w-36 items-center justify-center border-2 text-xs font-semibold uppercase tracking-wider transition-all ${
activeTab === "actor"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border bg-card text-foreground hover:border-primary/50"
} disabled:cursor-not-allowed disabled:opacity-40 rounded`}
>
Actor Prompt
</button>
<span className="text-lg text-muted-foreground"></span>
<button
onClick={() => onTabChange("decoder")}
disabled={!hasDecoder}
className={`flex h-12 w-36 items-center justify-center border-2 text-xs font-semibold uppercase tracking-wider transition-all ${
activeTab === "decoder"
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border bg-card text-foreground hover:border-primary/50"
} disabled:cursor-not-allowed disabled:opacity-40 rounded`}
>
Intent Decoder
</button>
</div>
{/* Branching Validator Column to the right of Intent Decoder */}
{validatorCalls.length > 0 && (
<div className="flex items-center gap-3 shrink-0">
<span className="text-lg text-muted-foreground"></span>
<div className="flex flex-col gap-2 pl-3">
<div className="flex flex-col gap-2">
{validatorCalls.map((call) => {
const tabKey = `validator-${call.intentIndex}`;
return (
<button
key={tabKey}
onClick={() => onTabChange(tabKey)}
className={`flex h-12 w-36 items-center justify-center border-2 text-xs font-semibold uppercase tracking-wider transition-all rounded ${
activeTab === tabKey
? "border-primary bg-primary text-primary-foreground shadow-sm"
: "border-border bg-card text-foreground hover:border-primary/50"
}`}
title={call.intentContent}
>
LLM Validator (Intent #{call.intentIndex})
</button>
);
})}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,62 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(
"relative w-full rounded border border-border/30 bg-card p-4 text-sm shadow-[2px_2px_0_0_var(--border)] flex flex-col md:flex-row md:items-center gap-3 justify-between",
className,
)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn(
"font-head font-bold leading-none tracking-tight text-foreground",
className,
)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-xs text-muted-foreground mt-1 flex-1 leading-relaxed md:mt-0",
className,
)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
const AlertAction = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("shrink-0 flex items-center mt-2 md:mt-0 md:ml-4", className)}
{...props}
/>
));
AlertAction.displayName = "AlertAction";
export { Alert, AlertTitle, AlertDescription, AlertAction };

View File

@@ -22,6 +22,7 @@ const buttonVariants = cva(
size: {
default: "h-10 px-4 py-2",
sm: "h-9 px-3 text-xs",
xs: "h-7 px-2.5 text-xs",
lg: "h-11 px-8 text-base",
icon: "h-10 w-10",
},

View File

@@ -5,7 +5,6 @@ import { Combobox as ComboboxPrimitive } from "@base-ui/react";
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
InputGroup,
InputGroupAddon,

View File

@@ -99,12 +99,13 @@ function InputGroupButton({
return React.cloneElement(render, {
className: cn(
inputGroupButtonVariants({ size }),
(render.props as any)?.className,
(render.props as Record<string, unknown>)?.className as
string | undefined,
className,
),
type,
...props,
} as any);
} as Record<string, unknown> as React.HTMLAttributes<HTMLElement>);
}
return (

View File

@@ -1,7 +1,6 @@
export interface IntentInfo {
type: string;
description: string;
selfDescription?: string;
content: string;
modifiers: string[];
targetIds: string[];
isValid?: boolean;
@@ -9,16 +8,25 @@ export interface IntentInfo {
minutesToAdvance?: number;
}
export interface LogEntry {
turn: number;
entityId: string;
entityName: string;
narrativeProse: string;
intents: IntentInfo[];
timestamp: string;
rawPrompt?: {
systemPrompt: string;
userContext: string;
export interface PromptComponent {
label: string;
type: "system" | "world" | "events" | "memories" | "input" | "other";
content: string;
}
export interface PromptBreakdown {
systemPrompt: string;
userContext: string;
components?: PromptComponent[];
}
export interface ValidatorCall {
intentIndex: number;
intentContent: string;
prompt?: PromptBreakdown;
response: {
isValid: boolean;
reason: string;
};
usage?: {
inputTokens: number;
@@ -28,10 +36,36 @@ export interface LogEntry {
providerInstanceName?: string;
maxContext?: number;
};
decoderPrompt?: {
systemPrompt: string;
userContext: string;
}
export interface HandoffResult {
chunks: {
content: string;
importance: number;
}[];
}
export interface LogEntry {
turn: number;
entityId: string;
entityName: string;
narrativeProse: string;
intents: IntentInfo[];
timestamp: string;
isHandoff?: boolean;
handoffResult?: HandoffResult;
decodedIntents?: IntentInfo[];
validatorCalls?: ValidatorCall[];
rawPrompt?: PromptBreakdown;
usage?: {
inputTokens: number;
outputTokens: number;
totalTokens: number;
modelName?: string;
providerInstanceName?: string;
maxContext?: number;
};
decoderPrompt?: PromptBreakdown;
decoderUsage?: {
inputTokens: number;
outputTokens: number;
@@ -47,6 +81,7 @@ export interface EntityInfo {
name: string;
isPlayer: boolean;
isAgent: boolean;
aliases?: Record<string, string>;
}
export interface WaitingContext {

View File

@@ -3,7 +3,7 @@ import type { SimSession } from "./types";
/**
* Runs the HandoffEngine for every agent entity that has accumulated enough
* buffer entries to warrant a handoff (compression to long-term memory).
* buffer entries to warrant a handoff (compression to the Memory Ledger).
*/
export async function runHandoffResolution(session: SimSession): Promise<void> {
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
@@ -33,11 +33,39 @@ export async function runHandoffResolution(session: SimSession): Promise<void> {
maxContext,
);
if (trigger !== "none") {
await handoffEngine.runHandoff(
const ran = await handoffEngine.runHandoff(
entity,
bufferEntries,
worldState.clock.get(),
);
if (ran) {
const lastResult = handoffEngine.lastResult;
const lastCall =
session.handoffProvider.lastCalls?.[
(session.handoffProvider.lastCalls?.length || 0) - 1
];
const info = session.entities.find((e) => e.id === entity.id);
const entityName = info?.name || entity.id;
session.log.push({
turn: session.turn,
entityId: entity.id,
entityName,
narrativeProse: `Handoff triggered for ${entityName}: memories were transferred from Cognitive Buffer to Memory Ledger`,
intents: [],
timestamp: worldState.clock.get().toISOString(),
isHandoff: true,
rawPrompt: lastResult
? {
systemPrompt: lastResult.systemPrompt || "",
userContext: lastResult.userContext || "",
components: lastResult.promptComponents,
}
: undefined,
usage: lastCall?.usage,
handoffResult: lastResult?.response || lastCall?.response,
});
}
}
}
}

View File

@@ -468,6 +468,21 @@ export class SimulationManager {
// ---------------------------------------------------------------------------
private snapshot(session: SimSession): SimSnapshot {
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
const hydratedEntities = session.entities.map((e) => {
const actualEntity = worldState?.getEntity(e.id);
const aliases: Record<string, string> = {};
if (actualEntity) {
for (const [targetId, alias] of actualEntity.aliases.entries()) {
aliases[targetId] = alias;
}
}
return {
...e,
aliases,
};
});
return {
id: session.worldInstanceId,
status: session.status,
@@ -475,7 +490,7 @@ export class SimulationManager {
maxTurns: session.maxTurns,
scenarioName: session.scenarioName,
scenarioDescription: session.scenarioDescription,
entities: session.entities,
entities: hydratedEntities,
log: session.log,
entityIndex: session.entityIndex,
waitingEntity: session.waitingEntity,

View File

@@ -49,17 +49,18 @@ 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();
intentInfos.push({
type: intent.type,
description: intent.description,
selfDescription: intent.selfDescription,
content: intent.content,
modifiers: intent.modifiers || [],
targetIds: intent.targetIds,
isValid: outcome.isValid,
@@ -67,6 +68,50 @@ async function processIntents(
minutesToAdvance: outcome.timeDelta?.minutesToAdvance,
});
if (intent.type === "action" && session.architect.validator.lastResult) {
const lastResult = session.architect.validator.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 };
@@ -100,7 +145,7 @@ async function processIntents(
}
}
return intentInfos;
return { intentInfos, validatorCalls };
}
// ---------------------------------------------------------------------------
@@ -166,6 +211,11 @@ export async function processNpcTurn(
narrativeProse: result.narrativeProse,
intents: [],
timestamp: worldState.clock.get().toISOString(),
rawPrompt: {
systemPrompt: result.systemPrompt || "",
userContext: result.userContext || "",
components: result.promptComponents,
},
};
if (
@@ -176,10 +226,6 @@ export async function processNpcTurn(
session.actorProvider.lastCalls[
session.actorProvider.lastCalls.length - 1
];
entry.rawPrompt = {
systemPrompt: actorCall.systemPrompt,
userContext: actorCall.userContext,
};
entry.usage = actorCall.usage;
}
@@ -191,20 +237,49 @@ export async function processNpcTurn(
session.decoderProvider.lastCalls[
session.decoderProvider.lastCalls.length - 1
];
const proseHeader = "=== NARRATIVE PROSE ===";
const userContext = decoderCall.userContext;
const idx = userContext.indexOf(proseHeader);
let contextStr = userContext;
let proseStr = "";
if (idx !== -1) {
contextStr = userContext.substring(0, idx).trim();
proseStr = userContext.substring(idx).trim();
}
entry.decoderPrompt = {
systemPrompt: decoderCall.systemPrompt,
userContext: decoderCall.userContext,
components: [
{
label: "System Prompt",
type: "system",
content: decoderCall.systemPrompt,
},
{ label: "Decoder Context", type: "world", content: contextStr },
{ label: "Narrative Prose", type: "input", content: proseStr },
],
};
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);
@@ -244,8 +319,9 @@ export async function executePlayerAction(
intents: [],
timestamp: worldState.clock.get().toISOString(),
rawPrompt: {
systemPrompt: ctx.systemPrompt,
userContext: ctx.userContext,
systemPrompt: result.systemPrompt || ctx.systemPrompt,
userContext: result.userContext || ctx.userContext,
components: result.promptComponents,
},
};
@@ -257,20 +333,45 @@ export async function executePlayerAction(
session.decoderProvider.lastCalls[
session.decoderProvider.lastCalls.length - 1
];
const proseHeader = "=== NARRATIVE PROSE ===";
const userContext = call.userContext;
const idx = userContext.indexOf(proseHeader);
let contextStr = userContext;
let proseStr = "";
if (idx !== -1) {
contextStr = userContext.substring(0, idx).trim();
proseStr = userContext.substring(idx).trim();
}
entry.decoderPrompt = {
systemPrompt: call.systemPrompt,
userContext: call.userContext,
components: [
{ label: "System Prompt", type: "system", content: call.systemPrompt },
{ label: "Decoder Context", type: "world", content: contextStr },
{ label: "Narrative Prose", type: "input", content: proseStr },
],
};
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);