diff --git a/apps/gui/src/components/play/HandoffModal.tsx b/apps/gui/src/components/play/HandoffModal.tsx index 4daa079..c8186a4 100644 --- a/apps/gui/src/components/play/HandoffModal.tsx +++ b/apps/gui/src/components/play/HandoffModal.tsx @@ -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 && ( -
-
-

- System Prompt -

-
-                  {entry.rawPrompt.systemPrompt}
-                
-
- -
-

- User Context (Candidates) -

-
-                  {entry.rawPrompt.userContext}
-                
-
-
+ 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" && ( diff --git a/apps/gui/src/components/play/PromptAnalyzer.tsx b/apps/gui/src/components/play/PromptAnalyzer.tsx new file mode 100644 index 0000000..8a064a7 --- /dev/null +++ b/apps/gui/src/components/play/PromptAnalyzer.tsx @@ -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 ( +
+ No prompt context recorded. +
+ ); + } + + 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 ( +
+ {/* Provider Details */} + {(providerInstanceName || modelName) && ( +
+ LLM Instance:{" "} + {providerInstanceName || "Default"} + {modelName && ({modelName})} +
+ )} + + {/* Progress Bar & Breakdown */} +
+
+ Input Prompt Breakdown + + Total Input Tokens: {inputTokens} + {maxContext > 0 ? ( + + {" "} + / {maxContext} ({usagePctOfContext.toFixed(1)}% used) + + ) : ( + (infinite context) + )} + +
+ + {/* Token Bar */} +
+ {sections.map((item, idx) => { + const widthPct = isAbsolute + ? item.pct * (inputTokens / maxContext) + : item.pct; + return ( +
+ ); + })} + {isAbsolute && ( +
+ )} +
+ + {/* Accordion Components */} + + {sections.map((item, idx) => { + return ( + + +
+ + {item.label}: + + {item.tokens} tokens ( + {item.pct.toFixed(0)}%) + +
+
+ +
+                    {item.content}
+                  
+
+
+ ); + })} +
+
+ + {/* Output Section */} + {outputText && ( +
+
+ {outputLabel} + {outputTokens !== undefined && ( + + Total Output Tokens: {outputTokens} + + )} +
+
+
+              {outputText}
+            
+
+
+ )} +
+ ); +} diff --git a/apps/gui/src/components/play/PromptModal.tsx b/apps/gui/src/components/play/PromptModal.tsx index 3d2c1fe..1d91392 100644 --- a/apps/gui/src/components/play/PromptModal.tsx +++ b/apps/gui/src/components/play/PromptModal.tsx @@ -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 ( !open && onClose()}> @@ -228,241 +71,37 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
{activeTab === "actor" && entry.rawPrompt && ( -
- {entry.usage ? ( -
- LLM Instance:{" "} - {entry.usage.providerInstanceName || "Default"} - {entry.usage.modelName && ( - ({entry.usage.modelName}) - )} -
- ) : ( -
- No LLM token usage (Player turn used fixed prose). -
- )} - - {scaledActorBreakdown && ( -
-
- - Input Prompt Breakdown - - - Total Input Tokens: {actorUsedTokens} - {actorMaxContext > 0 ? ( - - {" "} - / {actorMaxContext} ( - {actorUsagePctOfContext.toFixed(1)}% used) - - ) : ( - (infinite context) - )} - -
-
- {scaledActorBreakdown.map((item, idx) => { - const displayPct = - actorMaxContext > 0 - ? (item.tokens / actorMaxContext) * 100 - : item.relativePct; - return ( -
- ); - })} - {isActorAbsolute && ( -
- )} -
- - {scaledActorBreakdown.map((item, idx) => { - const displayPct = - actorMaxContext > 0 - ? (item.tokens / actorMaxContext) * 100 - : item.relativePct; - return ( - - - - {item.label}: {item.tokens} tokens - ({displayPct.toFixed(0)}%) - - -
-                              {item.content}
-                            
-
-
- ); - })} -
-
- )} - - {entry.usage && ( -
-
- LLM Output - - Total Output Tokens:{" "} - {entry.usage.outputTokens} - -
-
-
-                      {entry.narrativeProse}
-                    
-
-
- )} -
+ )} {activeTab === "decoder" && entry.decoderPrompt && ( -
- {entry.decoderUsage && ( -
- LLM Instance:{" "} - - {entry.decoderUsage.providerInstanceName || "Default"} - - {entry.decoderUsage.modelName && ( - ({entry.decoderUsage.modelName}) - )} -
- )} - - {scaledDecoderBreakdown && ( -
-
- - Input Prompt Breakdown - - - Total Input Tokens: {decoderUsedTokens} - {decoderMaxContext > 0 ? ( - - {" "} - / {decoderMaxContext} ( - {decoderUsagePctOfContext.toFixed(1)}% used) - - ) : ( - (infinite context) - )} - -
-
- {scaledDecoderBreakdown.map((item, idx) => { - const displayPct = - decoderMaxContext > 0 - ? (item.tokens / decoderMaxContext) * 100 - : item.relativePct; - return ( -
- ); - })} - {isDecoderAbsolute && ( -
- )} -
- - {scaledDecoderBreakdown.map((item, idx) => { - const displayPct = - decoderMaxContext > 0 - ? (item.tokens / decoderMaxContext) * 100 - : item.relativePct; - return ( - - - - {item.label}: {item.tokens} tokens - ({displayPct.toFixed(0)}%) - - -
-                              {item.content}
-                            
-
-
- ); - })} -
-
- )} - - {entry.decoderUsage && ( -
-
- LLM Output - - Total Output Tokens:{" "} - {entry.decoderUsage.outputTokens} - -
-
-
-                      {JSON.stringify(entry.intents, null, 2)}
-                    
-
-
- )} -
+ )}
diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index 11fa8ba..35cb8ca 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -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; diff --git a/apps/gui/src/lib/simulation/alias-handoff.ts b/apps/gui/src/lib/simulation/alias-handoff.ts index 36204fc..30295e5 100644 --- a/apps/gui/src/lib/simulation/alias-handoff.ts +++ b/apps/gui/src/lib/simulation/alias-handoff.ts @@ -45,6 +45,39 @@ export async function runHandoffResolution(session: SimSession): Promise { ]; 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 { 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, }); diff --git a/apps/gui/src/lib/simulation/turn-executor.ts b/apps/gui/src/lib/simulation/turn-executor.ts index e489cd4..60a8634 100644 --- a/apps/gui/src/lib/simulation/turn-executor.ts +++ b/apps/gui/src/lib/simulation/turn-executor.ts @@ -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; } diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index e94eae7..44707d9 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -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, }; } diff --git a/packages/actor/src/actor.ts b/packages/actor/src/actor.ts index 80a29c5..c31c730 100644 --- a/packages/actor/src/actor.ts +++ b/packages/actor/src/actor.ts @@ -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, }; } } diff --git a/packages/voice/src/index.ts b/packages/voice/src/index.ts index 629558a..7cc66dc 100644 --- a/packages/voice/src/index.ts +++ b/packages/voice/src/index.ts @@ -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[]; +}