refactor(gui): Unify prompt analysis components for actor, intent decoder and Handoff

This commit is contained in:
rhit-lid2
2026-07-19 16:18:15 +05:30
parent f8977a14c6
commit 1e34becec7
9 changed files with 381 additions and 480 deletions

View File

@@ -9,6 +9,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import { PromptAnalyzer } from "@/components/play/PromptAnalyzer";
interface HandoffModalProps {
entry: SimSnapshot["log"][number];
@@ -191,25 +192,40 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) {
)}
{activeTab === "prompt" && entry.rawPrompt && (
<div className="space-y-4">
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono mb-2">
System Prompt
</h4>
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[250px] overflow-y-auto">
{entry.rawPrompt.systemPrompt}
</pre>
</div>
<div>
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono mb-2">
User Context (Candidates)
</h4>
<pre className="p-3 bg-muted rounded text-xs font-mono whitespace-pre-wrap text-foreground border max-h-[350px] overflow-y-auto font-sans leading-relaxed font-mono">
{entry.rawPrompt.userContext}
</pre>
</div>
</div>
<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" && (

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

@@ -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];
@@ -25,191 +20,39 @@ interface PromptModalProps {
export function PromptModal({ entry, onClose }: PromptModalProps) {
const [activeTab, setActiveTab] = useState<"actor" | "decoder">("actor");
const parseActorPrompt = (
systemPrompt: string,
userContext: string,
inputTokens: number,
sectionsObj?: {
worldInfo: string;
memoryLedger: string;
cognitiveBuffer: string;
},
) => {
let worldStr = "";
let recentStr = "";
let ledgerStr = "";
if (sectionsObj) {
worldStr = sectionsObj.worldInfo;
recentStr = sectionsObj.cognitiveBuffer;
ledgerStr = sectionsObj.memoryLedger;
} else {
const recentHeader = "=== COGNITIVE BUFFER ===";
const ledgerHeader = "=== MEMORY LEDGER ===";
const recentIdx = userContext.indexOf(recentHeader);
worldStr = userContext;
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: "Cognitive Buffer",
type: "events",
content: recentStr || "(No cognitive buffer entries.)",
},
{
label: "Memory Ledger",
type: "memories",
content: ledgerStr || "(No memory ledger entries.)",
},
];
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,
entry.rawPrompt.sections,
)
: 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;
useEffect(() => {
if (!entry.rawPrompt && entry.decoderPrompt) {
setActiveTab("decoder");
}
}, [entry]);
// Helper to resolve components with a fallback if none exist (for backwards-compatibility)
const getComponents = (
promptBreakdown: any,
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");
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">
@@ -228,241 +71,37 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
<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>
)}
{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>
)}
{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>
)}
</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.intents, null, 2)}
outputTokens={entry.decoderUsage?.outputTokens}
/>
)}
</div>
</DialogContent>

View File

@@ -8,6 +8,18 @@ export interface IntentInfo {
minutesToAdvance?: number;
}
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 LogEntry {
turn: number;
entityId: string;
@@ -17,15 +29,7 @@ export interface LogEntry {
timestamp: string;
isHandoff?: boolean;
handoffResult?: any;
rawPrompt?: {
systemPrompt: string;
userContext: string;
sections?: {
worldInfo: string;
memoryLedger: string;
cognitiveBuffer: string;
};
};
rawPrompt?: PromptBreakdown;
usage?: {
inputTokens: number;
outputTokens: number;
@@ -34,10 +38,7 @@ export interface LogEntry {
providerInstanceName?: string;
maxContext?: number;
};
decoderPrompt?: {
systemPrompt: string;
userContext: string;
};
decoderPrompt?: PromptBreakdown;
decoderUsage?: {
inputTokens: number;
outputTokens: number;

View File

@@ -45,6 +45,39 @@ export async function runHandoffResolution(session: SimSession): Promise<void> {
];
const info = session.entities.find((e) => e.id === entity.id);
const entityName = info?.name || entity.id;
let handoffPrompt = undefined;
if (lastCall) {
const header = "Cognitive Buffer Candidates for Handoff:";
const userContext = lastCall.userContext;
const idx = userContext.indexOf(header);
let contextStr = userContext;
let candidatesStr = "";
if (idx !== -1) {
contextStr = userContext.substring(0, idx).trim();
candidatesStr = userContext.substring(idx).trim();
}
handoffPrompt = {
systemPrompt: lastCall.systemPrompt,
userContext: lastCall.userContext,
components: [
{
label: "System Prompt",
type: "system",
content: lastCall.systemPrompt,
},
{ label: "Entity Context", type: "world", content: contextStr },
{
label: "Cognitive Candidates",
type: "input",
content: candidatesStr,
},
],
};
}
session.log.push({
turn: session.turn,
entityId: entity.id,
@@ -53,12 +86,7 @@ export async function runHandoffResolution(session: SimSession): Promise<void> {
intents: [],
timestamp: worldState.clock.get().toISOString(),
isHandoff: true,
rawPrompt: lastCall
? {
systemPrompt: lastCall.systemPrompt,
userContext: lastCall.userContext,
}
: undefined,
rawPrompt: handoffPrompt,
usage: lastCall?.usage,
handoffResult: lastCall?.response,
});

View File

@@ -191,9 +191,30 @@ 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;
}

View File

@@ -13,7 +13,7 @@ import {
LedgerEntry,
LedgerRepository,
} from "@omnia/memory";
import { hydrate } from "@omnia/voice";
import { hydrate, PromptComponent } from "@omnia/voice";
/**
* Zod schema for the structured response expected from the actor LLM.
@@ -60,21 +60,24 @@ export class ActorPromptBuilder {
/**
* Assembles the system prompt and user context for a given entity.
*/
/**
* Assembles the system prompt and user context for a given entity.
*/
build(
worldState: WorldState,
entity: Entity,
): {
systemPrompt: string;
userContext: string;
sections: {
worldInfo: string;
memoryLedger: string;
cognitiveBuffer: string;
};
components: PromptComponent[];
} {
const systemPrompt = this.buildSystemPrompt();
const { userContext, sections } = this.buildUserContext(worldState, entity);
return { systemPrompt, userContext, sections };
const { userContext, components } = this.buildUserContext(
worldState,
entity,
systemPrompt,
);
return { systemPrompt, userContext, components };
}
private buildSystemPrompt(): string {
@@ -103,13 +106,10 @@ Guidelines:
private buildUserContext(
worldState: WorldState,
entity: Entity,
systemPrompt: string,
): {
userContext: string;
sections: {
worldInfo: string;
memoryLedger: string;
cognitiveBuffer: string;
};
components: PromptComponent[];
} {
const now = worldState.clock.get();
@@ -151,13 +151,28 @@ Guidelines:
if (cognitiveBuffer) parts.push(cognitiveBuffer);
const userContext = parts.join("\n\n");
const components: PromptComponent[] = [
{ label: "System Prompt", type: "system", content: systemPrompt },
{ label: "World Info", type: "world", content: worldInfo },
];
if (memoryLedger) {
components.push({
label: "Memory Ledger",
type: "memories",
content: memoryLedger,
});
}
if (cognitiveBuffer) {
components.push({
label: "Cognitive Buffer",
type: "events",
content: cognitiveBuffer,
});
}
return {
userContext,
sections: {
worldInfo,
memoryLedger,
cognitiveBuffer,
},
components,
};
}

View File

@@ -2,6 +2,7 @@ import { Entity, WorldState } from "@omnia/core";
import { ILLMProvider } from "@omnia/llm";
import { BufferEntry, BufferRepository, LedgerRepository } from "@omnia/memory";
import { Intent, IntentDecoder, IntentSequence } from "@omnia/intent";
import { PromptComponent } from "@omnia/voice";
import {
ActorPromptBuilder,
ActorResponseSchema,
@@ -56,11 +57,7 @@ export interface ActorTurnResult {
intents: IntentSequence;
systemPrompt?: string;
userContext?: string;
promptComponents?: {
worldInfo: string;
memoryLedger: string;
cognitiveBuffer: string;
};
promptComponents?: PromptComponent[];
}
/**
@@ -123,7 +120,7 @@ export class ActorAgent {
);
}
const { systemPrompt, userContext, sections } = this.promptBuilder.build(
const { systemPrompt, userContext, components } = this.promptBuilder.build(
worldState,
entity,
);
@@ -154,7 +151,7 @@ export class ActorAgent {
intents,
systemPrompt,
userContext,
promptComponents: sections,
promptComponents: components,
};
}
}

View File

@@ -1,3 +1,15 @@
export * from "./dehydration.js";
export * from "./hydration.js";
export * from "./contractions.js";
export interface PromptComponent {
label: string;
type: "system" | "world" | "events" | "memories" | "input" | "other";
content: string;
}
export interface PromptBreakdown {
systemPrompt: string;
userContext: string;
components?: PromptComponent[];
}