- {snapshot.log.map((entry, i) => (
-
- ))}
+ {snapshot.log.map((entry, i) => {
+ if (entry.isHandoff) {
+ return (
+
+
+
+ Handoff triggered for {entry.entityName}
+
+
+ Memories were transferred from Cognitive Buffer to Memory
+ Ledger
+
+
+
+
+
+
+ );
+ }
+ const playerAliases = playerEntity?.aliases || {};
+ const playerId = playerEntity?.id || "";
+
+ return (
+
+ );
+ })}
{loading && (
diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx
index 322d4f8..ed1b284 100644
--- a/apps/gui/src/components/play/PlayView.tsx
+++ b/apps/gui/src/components/play/PlayView.tsx
@@ -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(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 && (
+ setSelectedHandoffForModal(null)}
+ />
+ )}
);
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}
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/gui/src/components/play/PromptModal.tsx b/apps/gui/src/components/play/PromptModal.tsx
index 36c940f..20f05b4 100644
--- a/apps/gui/src/components/play/PromptModal.tsx
+++ b/apps/gui/src/components/play/PromptModal.tsx
@@ -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
("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 (