diff --git a/apps/gui/src/components/play/HandoffModal.tsx b/apps/gui/src/components/play/HandoffModal.tsx new file mode 100644 index 0000000..0124d7e --- /dev/null +++ b/apps/gui/src/components/play/HandoffModal.tsx @@ -0,0 +1,231 @@ +"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"; + +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 ( + !open && onClose()}> + + + + Memory Handoff Details — {entry.entityName} + {entry.usage && ( + + {entry.usage.modelName || "Handoff Model"} + + )} + + + + {/* Custom Tab Switcher */} +
+ + + +
+ +
+ {activeTab === "chunks" && ( +
+ {entry.usage && ( +
+
+ + Input Tokens + + + {entry.usage.inputTokens} + +
+
+ + Output Tokens + + + {entry.usage.outputTokens} + +
+
+ + Total Tokens + + + {entry.usage.totalTokens} + +
+
+ )} + + {chunks.length === 0 ? ( +
+

+ No memories were promoted to the Ledger during this turn. +

+

+ All working memory buffer entries were summarized or + forgotten. +

+
+ ) : ( +
+

+ Ledger Additions +

+ {chunks.map((chunk: any, index: number) => ( +
+
+
+ {chunk.content} +
+ + Importance: {chunk.importance} + +
+ + {chunk.quotes && chunk.quotes.length > 0 && ( +
+ {chunk.quotes.map((quote: string, qIdx: number) => ( +
“{quote}”
+ ))} +
+ )} + +
+ {chunk.retainInBuffer ? ( + + Pinned in Buffer + + ) : ( + + Pruned from Buffer + + )} + + {chunk.involvedEntityIds && + chunk.involvedEntityIds.length > 0 && ( +
+ Entities: + {chunk.involvedEntityIds.map((entId: string) => ( + + {entId} + + ))} +
+ )} +
+
+ ))} +
+ )} +
+ )} + + {activeTab === "prompt" && entry.rawPrompt && ( +
+
+

+ System Prompt +

+
+                  {entry.rawPrompt.systemPrompt}
+                
+
+ +
+

+ User Context (Candidates) +

+
+                  {entry.rawPrompt.userContext}
+                
+
+
+ )} + + {activeTab === "output" && ( +
+

+ Raw JSON Output +

+
+                {handoffResult
+                  ? JSON.stringify(handoffResult, null, 2)
+                  : "No JSON Output recorded."}
+              
+
+ )} +
+
+
+ ); +} diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx index a8d8a92..b75a5b6 100644 --- a/apps/gui/src/components/play/PlayView.tsx +++ b/apps/gui/src/components/play/PlayView.tsx @@ -12,6 +12,13 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Spinner } from "@/components/ui/spinner"; import { PromptModal } from "./PromptModal"; +import { HandoffModal } from "./HandoffModal"; +import { + Alert, + AlertAction, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert"; import { cn } from "@/lib/utils"; import { ChevronLeft } from "lucide-react"; import { @@ -163,6 +170,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); @@ -490,14 +500,43 @@ export function PlayView() { const playerEntity = snapshot.entities.find( (e) => e.isPlayer, ); - return snapshot.log.map((entry, i) => ( - - )); + return snapshot.log.map((entry, i) => { + if (entry.isHandoff) { + return ( + +
+ + Handoff triggered for {entry.entityName} + + + Memories were transferred from Buffer to Memory + Ledger + +
+ + + +
+ ); + } + return ( + + ); + }); })()} {loading && (
@@ -662,6 +701,13 @@ export function PlayView() { onClose={() => setSelectedEntryForModal(null)} /> )} + + {selectedHandoffForModal && ( + setSelectedHandoffForModal(null)} + /> + )}
); diff --git a/apps/gui/src/components/ui/alert.tsx b/apps/gui/src/components/ui/alert.tsx new file mode 100644 index 0000000..591cad1 --- /dev/null +++ b/apps/gui/src/components/ui/alert.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +const AlertAction = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertAction.displayName = "AlertAction"; + +export { Alert, AlertTitle, AlertDescription, AlertAction }; diff --git a/apps/gui/src/components/ui/button.tsx b/apps/gui/src/components/ui/button.tsx index 6af517f..a9b62d9 100644 --- a/apps/gui/src/components/ui/button.tsx +++ b/apps/gui/src/components/ui/button.tsx @@ -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", }, diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index c4ee4bd..e2b6c72 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -16,6 +16,8 @@ export interface LogEntry { narrativeProse: string; intents: IntentInfo[]; timestamp: string; + isHandoff?: boolean; + handoffResult?: any; rawPrompt?: { systemPrompt: string; userContext: string; diff --git a/apps/gui/src/lib/simulation/alias-handoff.ts b/apps/gui/src/lib/simulation/alias-handoff.ts index dc17697..9ff4f25 100644 --- a/apps/gui/src/lib/simulation/alias-handoff.ts +++ b/apps/gui/src/lib/simulation/alias-handoff.ts @@ -33,11 +33,36 @@ export async function runHandoffResolution(session: SimSession): Promise { maxContext, ); if (trigger !== "none") { - await handoffEngine.runHandoff( + const ran = await handoffEngine.runHandoff( entity, bufferEntries, worldState.clock.get(), ); + if (ran) { + 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 Buffer to Memory Ledger`, + intents: [], + timestamp: worldState.clock.get().toISOString(), + isHandoff: true, + rawPrompt: lastCall + ? { + systemPrompt: lastCall.systemPrompt, + userContext: lastCall.userContext, + } + : undefined, + usage: lastCall?.usage, + handoffResult: lastCall?.response, + }); + } } } } diff --git a/packages/llm/src/base-provider.ts b/packages/llm/src/base-provider.ts index 4540b25..2d33436 100644 --- a/packages/llm/src/base-provider.ts +++ b/packages/llm/src/base-provider.ts @@ -105,6 +105,7 @@ export abstract class BaseLLMProvider implements ILLMProvider { systemPrompt: request.systemPrompt, userContext: request.userContext, usage, + response: parsed, }); return { success: true, data: parsed, usage }; diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 7d136da..03bb135 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -33,6 +33,7 @@ export interface LLMCallRecord { providerInstanceName?: string; maxContext?: number; }; + response?: any; } export interface ILLMProvider { diff --git a/packages/llm/src/providers/mock.ts b/packages/llm/src/providers/mock.ts index b898133..116cffb 100644 --- a/packages/llm/src/providers/mock.ts +++ b/packages/llm/src/providers/mock.ts @@ -48,15 +48,21 @@ export class MockLLMProvider implements ILLMProvider { return { success: false, error: "Mock responses exhausted" }; } const usage = { inputTokens: 100, outputTokens: 50, totalTokens: 150 }; - this.lastCalls.push({ - systemPrompt: request.systemPrompt, - userContext: request.userContext, - usage, - }); try { const parsed = request.schema.parse(next); + this.lastCalls.push({ + systemPrompt: request.systemPrompt, + userContext: request.userContext, + usage, + response: parsed, + }); return { success: true, data: parsed, usage }; } catch (e) { + this.lastCalls.push({ + systemPrompt: request.systemPrompt, + userContext: request.userContext, + usage, + }); return { success: false, error: e instanceof Error ? e.message : String(e), diff --git a/packages/memory/src/handoff.ts b/packages/memory/src/handoff.ts index 4e61028..98e84e3 100644 --- a/packages/memory/src/handoff.ts +++ b/packages/memory/src/handoff.ts @@ -230,10 +230,12 @@ export class HandoffEngine { You are the memory Handoff Engine. Your task is to process a list of recent working memory buffer entries for an entity and select which memories to promote to the long-term Ledger, and which to forget or summarize. Instructions: -1. **Cluster** related consecutive buffer entries into high-level narrative beats or events (e.g. a full back-and-forth conversation or a single physical action and its outcome). Combine them into a single summary chunk. -2. **Write in the third-person** for the "content" of each chunk (e.g. "John asked Mary for the key, and Mary reluctantly handed it over"). +1. **Cluster** related consecutive buffer entries into high-level narrative beats or events (e.g. physical action and its outcome or trivial actions). Combine them into a single chunk. +2. **Write in the third-person** for the events of other entities. (eg. Alan did that. Sarah did this, etc) +2. **Write in first-person for the events that you yourself did. (eg. I did this, I did that.) 3. **verbatim Quotes**: Extract verbatim, high-salience quotes from dialogue if relevant. Do not modify or invent quotes. 4. **Determine Importance**: Assign an importance score from 1 (trivial, e.g. waking up) to 10 (life-altering, e.g. witnessing a crime). +4. Discard small body movements like looking around, sighing, etc that do not contextually hold any meaning after it is done. 5. **Involved Entities**: Identify all entity IDs involved in the memories in this chunk. 6. **Retain in Buffer (Pinning)**: If a beat represents an unresolved high-stakes situation (e.g. a standing threat, an unanswered accusation, an ongoing chase or conflict), set "retainInBuffer" to true so it remains in the working memory buffer for immediate context. Otherwise, set it to false so it is safely pruned from the buffer. 7. **Exclude stage business**: Glances, sighs, ambient noticing, and irrelevant sensory details should be ignored and not included in any promoted chunk. They will be forgotten.