mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
Compare commits
4 Commits
ee25bf4a4c
...
8f6fe0dc28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f6fe0dc28 | ||
| 01ec062194 | |||
| 8ff1650657 | |||
| 7baf583b13 |
@@ -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) => {
|
||||
|
||||
@@ -126,66 +126,73 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) {
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground font-mono">
|
||||
Memory Ledger Additions
|
||||
</h3>
|
||||
{chunks.map((chunk: any, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-border/30 bg-card p-4 shadow-[2px_2px_0_0_var(--border)] 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}>“{quote}”</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs pt-2 border-t border-dotted border-border/10">
|
||||
{chunk.retainInBuffer ? (
|
||||
{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="bg-primary/5 text-primary border-primary/20 text-[10px] font-mono"
|
||||
className={`font-mono text-xs ${getImportanceColor(chunk.importance)}`}
|
||||
>
|
||||
Pinned in Buffer
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-muted text-muted-foreground border-border/20 text-[10px] font-mono"
|
||||
>
|
||||
Pruned from Buffer
|
||||
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}>“{quote}”</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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 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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -17,14 +17,14 @@ import {
|
||||
|
||||
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",
|
||||
@@ -40,13 +40,27 @@ function IntentTag({
|
||||
outcome = intent.isValid ? " ✅" : ` ❌ (${intent.reason})`;
|
||||
}
|
||||
|
||||
const viewerAliasesMap = new Map(Object.entries(playerAliases || {}));
|
||||
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 any);
|
||||
const textToDisplay = hydrate(
|
||||
intent.content,
|
||||
viewerEntityMock as unknown as Parameters<typeof hydrate>[1],
|
||||
);
|
||||
|
||||
const modifiersStr =
|
||||
intent.modifiers && intent.modifiers.length > 0 ? (
|
||||
@@ -86,12 +100,14 @@ function LogEntryCard({
|
||||
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);
|
||||
|
||||
@@ -132,9 +148,9 @@ function LogEntryCard({
|
||||
<IntentTag
|
||||
key={i}
|
||||
intent={intent}
|
||||
isSelf={isPlayerCard}
|
||||
playerAliases={playerAliases}
|
||||
playerId={playerId}
|
||||
entities={entities}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -213,6 +229,7 @@ export function InteractView({
|
||||
isPlayerCard={entry.entityId === playerEntity?.id}
|
||||
playerAliases={playerAliases}
|
||||
playerId={playerId}
|
||||
entities={snapshot.entities}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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,
|
||||
@@ -18,7 +18,7 @@ interface PromptModalProps {
|
||||
}
|
||||
|
||||
export function PromptModal({ entry, onClose }: PromptModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<"actor" | "decoder">("actor");
|
||||
const [activeTab, setActiveTab] = useState<string>("actor");
|
||||
|
||||
useEffect(() => {
|
||||
if (!entry.rawPrompt && entry.decoderPrompt) {
|
||||
@@ -28,7 +28,7 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
|
||||
|
||||
// Helper to resolve components with a fallback if none exist (for backwards-compatibility)
|
||||
const getComponents = (
|
||||
promptBreakdown: any,
|
||||
promptBreakdown: PromptBreakdown | null | undefined,
|
||||
defaultType: "world" | "input",
|
||||
) => {
|
||||
if (!promptBreakdown) return [];
|
||||
@@ -53,9 +53,20 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
|
||||
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})
|
||||
@@ -67,6 +78,12 @@ 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">
|
||||
@@ -99,10 +116,42 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
|
||||
modelName={entry.decoderUsage?.modelName}
|
||||
providerInstanceName={entry.decoderUsage?.providerInstanceName}
|
||||
outputLabel="LLM Output (Decoded Intent Sequence)"
|
||||
outputText={JSON.stringify(entry.intents, null, 2)}
|
||||
outputText={JSON.stringify(
|
||||
entry.decodedIntents || entry.intents,
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
outputTokens={entry.decoderUsage?.outputTokens}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -20,6 +20,31 @@ export interface PromptBreakdown {
|
||||
components?: PromptComponent[];
|
||||
}
|
||||
|
||||
export interface ValidatorCall {
|
||||
intentIndex: number;
|
||||
intentContent: string;
|
||||
prompt?: PromptBreakdown;
|
||||
response: {
|
||||
isValid: boolean;
|
||||
reason: string;
|
||||
};
|
||||
usage?: {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
totalTokens: number;
|
||||
modelName?: string;
|
||||
providerInstanceName?: string;
|
||||
maxContext?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HandoffResult {
|
||||
chunks: {
|
||||
content: string;
|
||||
importance: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
turn: number;
|
||||
entityId: string;
|
||||
@@ -28,7 +53,9 @@ export interface LogEntry {
|
||||
intents: IntentInfo[];
|
||||
timestamp: string;
|
||||
isHandoff?: boolean;
|
||||
handoffResult?: any;
|
||||
handoffResult?: HandoffResult;
|
||||
decodedIntents?: IntentInfo[];
|
||||
validatorCalls?: ValidatorCall[];
|
||||
rawPrompt?: PromptBreakdown;
|
||||
usage?: {
|
||||
inputTokens: number;
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function runHandoffResolution(session: SimSession): Promise<void> {
|
||||
worldState.clock.get(),
|
||||
);
|
||||
if (ran) {
|
||||
const lastResult = (handoffEngine as any).lastResult;
|
||||
const lastResult = handoffEngine.lastResult;
|
||||
const lastCall =
|
||||
session.handoffProvider.lastCalls?.[
|
||||
(session.handoffProvider.lastCalls?.length || 0) - 1
|
||||
|
||||
@@ -49,10 +49,12 @@ 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();
|
||||
|
||||
@@ -66,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 };
|
||||
@@ -99,7 +145,7 @@ async function processIntents(
|
||||
}
|
||||
}
|
||||
|
||||
return intentInfos;
|
||||
return { intentInfos, validatorCalls };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -219,13 +265,21 @@ export async function processNpcTurn(
|
||||
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);
|
||||
@@ -303,13 +357,21 @@ export async function executePlayerAction(
|
||||
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);
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
WorldState,
|
||||
naturalizeTime,
|
||||
serializeSubjectiveWorldState,
|
||||
resolveAlias,
|
||||
} from "@omnia/core";
|
||||
import {
|
||||
BufferEntry,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
LedgerRepository,
|
||||
} from "@omnia/memory";
|
||||
import { hydrate } from "@omnia/voice";
|
||||
import { PromptComponent } from "@omnia/llm";
|
||||
import { PromptComponent, IPromptBuilder, PromptBreakdown } from "@omnia/llm";
|
||||
|
||||
/**
|
||||
* Zod schema for the structured response expected from the actor LLM.
|
||||
@@ -38,7 +37,9 @@ export type ActorResponse = z.infer<typeof ActorResponseSchema>;
|
||||
* ACL'd to it), its own Cognitive Buffer, and the entities co-located
|
||||
* with it. System UUIDs are surfaced as subjective aliases.
|
||||
*/
|
||||
export class ActorPromptBuilder {
|
||||
export class ActorPromptBuilder implements IPromptBuilder<
|
||||
[WorldState, Entity]
|
||||
> {
|
||||
/**
|
||||
* @param bufferRepo Used to fetch the actor's Cognitive Buffer. Optional —
|
||||
* if absent, the memory section is omitted.
|
||||
@@ -64,14 +65,7 @@ export class ActorPromptBuilder {
|
||||
/**
|
||||
* Assembles the system prompt and user context for a given entity.
|
||||
*/
|
||||
build(
|
||||
worldState: WorldState,
|
||||
entity: Entity,
|
||||
): {
|
||||
systemPrompt: string;
|
||||
userContext: string;
|
||||
components: PromptComponent[];
|
||||
} {
|
||||
build(worldState: WorldState, entity: Entity): PromptBreakdown {
|
||||
const systemPrompt = this.buildSystemPrompt();
|
||||
const { userContext, components } = this.buildUserContext(
|
||||
worldState,
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface ProcessResult extends ValidationResult {
|
||||
}
|
||||
|
||||
export class Architect {
|
||||
private validator: LLMValidator;
|
||||
public validator: LLMValidator;
|
||||
private timeDeltaGenerator: TimeDeltaGenerator;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./llm-validator.js";
|
||||
export * from "./llm-validator-prompt-builder.js";
|
||||
export * from "./architect.js";
|
||||
export * from "./delta.js";
|
||||
|
||||
59
packages/architect/src/llm-validator-prompt-builder.ts
Normal file
59
packages/architect/src/llm-validator-prompt-builder.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { WorldState, serializeObjectiveWorldState } from "@omnia/core";
|
||||
import { Intent } from "@omnia/intent";
|
||||
import { hydrateObjective } from "@omnia/voice";
|
||||
import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm";
|
||||
|
||||
/**
|
||||
* Prompt builder for the LLM Validator (World Architect).
|
||||
* Separates prompt generation, structure, and component breakdowns.
|
||||
*/
|
||||
export class LLMValidatorPromptBuilder implements IPromptBuilder<
|
||||
[WorldState, Intent]
|
||||
> {
|
||||
build(worldState: WorldState, intent: Intent): PromptBreakdown {
|
||||
const serializedWorld = serializeObjectiveWorldState(worldState);
|
||||
|
||||
const systemPrompt = `
|
||||
You are the World Architect, a deterministic and objective judge of reality, physics, and narration for a simulation game.
|
||||
Your task is to judge whether a proposed action (Intent) by an actor is physically and logically possible given the current objective state of the world.
|
||||
Exempt dialogue or speech actions from validation (consider them always valid).
|
||||
Enforce logical boundaries such as:
|
||||
- Spatial boundaries (an actor cannot grab an object in another location unless they are there).
|
||||
- Physical boundaries (an actor cannot open a locked drawer without a key or breaking it).
|
||||
- State Boundaries (an actor cannot perform a task if their state doesn't allow them to do so).
|
||||
- State/Attribute constraints.
|
||||
- An actor can perform actions on themselves as long as it follows the boundaries stated above.
|
||||
|
||||
You must respond with a JSON object containing:
|
||||
- "isValid": boolean indicating if the action is possible/allowed.
|
||||
- "reason": a very short explanation of why the action is allowed or denied.
|
||||
`.trim();
|
||||
|
||||
const objectiveContent = hydrateObjective(intent.content, worldState);
|
||||
|
||||
const worldStateSection = `=== CURRENT WORLD STATE ===\nCurrent Time: ${worldState.clock.get().toISOString()}\nEntities & Attributes:\n${serializedWorld}`;
|
||||
const proposedActionSection = `=== PROPOSED ACTION ===\nActor ID: ${intent.actorId}\nType: ${intent.type}\nContent: "${objectiveContent}"\nTarget IDs: ${intent.targetIds.join(", ") || "(None)"}`;
|
||||
|
||||
const userContext = `${worldStateSection}\n\n${proposedActionSection}\n\nDecide if the proposed action is logically valid and physically possible.`;
|
||||
|
||||
const components: PromptComponent[] = [
|
||||
{ label: "System Prompt", type: "system", content: systemPrompt },
|
||||
{
|
||||
label: "Current World State",
|
||||
type: "world",
|
||||
content: worldStateSection,
|
||||
},
|
||||
{
|
||||
label: "Proposed Action",
|
||||
type: "input",
|
||||
content: proposedActionSection,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
systemPrompt,
|
||||
userContext,
|
||||
components,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { WorldState, serializeObjectiveWorldState } from "@omnia/core";
|
||||
import { ILLMProvider } from "@omnia/llm";
|
||||
import { WorldState } from "@omnia/core";
|
||||
import { ILLMProvider, PromptBreakdown } from "@omnia/llm";
|
||||
import { Intent } from "@omnia/intent";
|
||||
import { hydrateObjective } from "@omnia/voice";
|
||||
import { LLMValidatorPromptBuilder } from "./llm-validator-prompt-builder.js";
|
||||
|
||||
export const ValidationResultSchema = z.object({
|
||||
isValid: z.boolean(),
|
||||
@@ -12,7 +12,12 @@ export const ValidationResultSchema = z.object({
|
||||
export type ValidationResult = z.infer<typeof ValidationResultSchema>;
|
||||
|
||||
export class LLMValidator {
|
||||
constructor(private llmProvider: ILLMProvider) {}
|
||||
public lastResult: PromptBreakdown | null = null;
|
||||
private promptBuilder: LLMValidatorPromptBuilder;
|
||||
|
||||
constructor(private llmProvider: ILLMProvider) {
|
||||
this.promptBuilder = new LLMValidatorPromptBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an action intent against the objective world state.
|
||||
@@ -25,6 +30,8 @@ export class LLMValidator {
|
||||
worldState: WorldState,
|
||||
intent: Intent,
|
||||
): Promise<ValidationResult> {
|
||||
this.lastResult = null;
|
||||
|
||||
// Defensive guard: monologue and thought intents bypass validation.
|
||||
if (intent.type === "monologue" || intent.type === "thought") {
|
||||
return {
|
||||
@@ -42,41 +49,16 @@ export class LLMValidator {
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Serialize the objective world state for the LLM
|
||||
const serializedWorld = serializeObjectiveWorldState(worldState);
|
||||
const { systemPrompt, userContext, components } = this.promptBuilder.build(
|
||||
worldState,
|
||||
intent,
|
||||
);
|
||||
|
||||
// 2. Build the prompts
|
||||
const systemPrompt = `
|
||||
You are the World Architect, a deterministic and objective judge of reality, physics, and narration for a simulation game.
|
||||
Your task is to judge whether a proposed action (Intent) by an actor is physically and logically possible given the current objective state of the world.
|
||||
Exempt dialogue or speech actions from validation (consider them always valid).
|
||||
Enforce logical boundaries such as:
|
||||
- Spatial boundaries (an actor cannot grab an object in another location unless they are there).
|
||||
- Physical boundaries (an actor cannot open a locked drawer without a key or breaking it).
|
||||
- State Boundaries (an actor cannot perform a task if their state doesn't allow them to do so).
|
||||
- State/Attribute constraints.
|
||||
|
||||
You must respond with a JSON object containing:
|
||||
- "isValid": boolean indicating if the action is possible/allowed.
|
||||
- "reason": a concise explanation of why the action is allowed or denied.
|
||||
`.trim();
|
||||
|
||||
const objectiveContent = hydrateObjective(intent.content, worldState);
|
||||
|
||||
const userContext = `
|
||||
=== CURRENT WORLD STATE ===
|
||||
Current Time: ${worldState.clock.get().toISOString()}
|
||||
Entities & Attributes:
|
||||
${serializedWorld}
|
||||
|
||||
=== PROPOSED ACTION ===
|
||||
Actor ID: ${intent.actorId}
|
||||
Type: ${intent.type}
|
||||
Content: "${objectiveContent}"
|
||||
Target IDs: ${intent.targetIds.join(", ") || "(None)"}
|
||||
|
||||
Decide if the proposed action is logically valid and physically possible.
|
||||
`.trim();
|
||||
this.lastResult = {
|
||||
systemPrompt,
|
||||
userContext,
|
||||
components,
|
||||
};
|
||||
|
||||
// structured call via the LLM provider
|
||||
const response = await this.llmProvider.generateStructuredResponse({
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { WorldState, resolveAlias } from "@omnia/core";
|
||||
import { PromptBreakdown, PromptComponent } from "@omnia/llm";
|
||||
import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm";
|
||||
import { Intent } from "./intent.js";
|
||||
|
||||
// TODO: Builder a generic interface for prompt builders in @omnia/llm: IPromptBuilder or something
|
||||
|
||||
/**
|
||||
* Prompt builder for the Intent Decoder.
|
||||
* Separates prompt generation, structure, and component breakdowns.
|
||||
*/
|
||||
export class IntentDecoderPromptBuilder {
|
||||
export class IntentDecoderPromptBuilder implements IPromptBuilder<
|
||||
[WorldState, string, string, Intent[]]
|
||||
> {
|
||||
build(
|
||||
worldState: WorldState,
|
||||
actorId: string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { WorldState, Entity } from "@omnia/core";
|
||||
import { MockLLMProvider } from "@omnia/llm";
|
||||
import { IntentDecoder, IntentSequence } from "@omnia/intent";
|
||||
import { IntentDecoder } from "@omnia/intent";
|
||||
|
||||
describe("IntentDecoder Unit Tests (Tier 1)", () => {
|
||||
test("decodes prose with a single action intent", async () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface PromptBreakdown {
|
||||
components?: PromptComponent[];
|
||||
}
|
||||
|
||||
export interface IPromptBuilder<TArgs extends any[]> {
|
||||
export interface IPromptBuilder<TArgs extends unknown[]> {
|
||||
build(...args: TArgs): PromptBreakdown;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export interface LLMCallRecord {
|
||||
providerInstanceName?: string;
|
||||
maxContext?: number;
|
||||
};
|
||||
response?: any;
|
||||
response?: unknown;
|
||||
}
|
||||
|
||||
export interface ILLMProvider {
|
||||
|
||||
@@ -30,6 +30,7 @@ export class MockLLMProvider implements ILLMProvider {
|
||||
registerGenerative("mock", () => new MockLLMProvider([]));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
static create(inst: ModelProviderInstance): ILLMProvider {
|
||||
return new MockLLMProvider([]);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ vi.mock("@langchain/openai", () => {
|
||||
constructor(config: unknown) {
|
||||
this.config = config;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
embedQuery = vi.fn().mockImplementation(async (text: string) => {
|
||||
return [0.1, 0.2, 0.3];
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { Entity, resolveAlias } from "@omnia/core";
|
||||
import { Entity } from "@omnia/core";
|
||||
import { Intent } from "@omnia/intent";
|
||||
import { hydrate } from "@omnia/voice";
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Entity } from "@omnia/core";
|
||||
import { PromptBreakdown, PromptComponent } from "@omnia/llm";
|
||||
import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm";
|
||||
import { BufferEntry, serializeSubjectiveBufferEntry } from "./buffer.js";
|
||||
|
||||
/**
|
||||
* Prompt builder for the Handoff Engine.
|
||||
* Separates prompt generation, structure, and component breakdowns.
|
||||
*/
|
||||
export class HandoffPromptBuilder {
|
||||
export class HandoffPromptBuilder implements IPromptBuilder<
|
||||
[Entity, BufferEntry[], Date]
|
||||
> {
|
||||
build(entity: Entity, candidates: BufferEntry[], now: Date): PromptBreakdown {
|
||||
const candidatesList = candidates
|
||||
.map((entry) => {
|
||||
|
||||
@@ -208,7 +208,7 @@ export interface HandoffRunResult {
|
||||
systemPrompt?: string;
|
||||
userContext?: string;
|
||||
promptComponents?: PromptComponent[];
|
||||
response?: any;
|
||||
response?: unknown;
|
||||
}
|
||||
|
||||
export class HandoffEngine {
|
||||
@@ -267,7 +267,11 @@ export class HandoffEngine {
|
||||
};
|
||||
|
||||
const result = response.data;
|
||||
const db = (this.bufferRepo as any).db;
|
||||
const db = (
|
||||
this.bufferRepo as unknown as {
|
||||
db: { transaction: (fn: () => void) => () => void };
|
||||
}
|
||||
).db;
|
||||
|
||||
const ledgerEntries: LedgerEntry[] = [];
|
||||
for (const chunk of result.chunks) {
|
||||
|
||||
@@ -10,7 +10,6 @@ export function splitQuotes(text: string): Segment[] {
|
||||
const segments: Segment[] = [];
|
||||
let current = "";
|
||||
let inQuote = false;
|
||||
let quoteChar = "";
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
@@ -78,7 +77,10 @@ export function dehydrate(
|
||||
// 2. Replace names and aliases with entity@<id>[name]
|
||||
sortedNames.forEach((name) => {
|
||||
const id = nameToId.get(name)!;
|
||||
const escapedName = name.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
||||
const escapedName = name.replace(
|
||||
new RegExp("[-/\\\\^$*+?.()|[\\]{}]", "g"),
|
||||
"\\$&",
|
||||
);
|
||||
const regex = new RegExp(`\\b${escapedName}\\b`, "gi");
|
||||
|
||||
text = text.replace(regex, (matched) => {
|
||||
|
||||
@@ -48,7 +48,7 @@ export function hydrate(content: string, viewer: Entity): string {
|
||||
return seg.text.replace(regex, (matchStr, id, original, followingWord) => {
|
||||
const isSelf = id === viewer.id;
|
||||
const lowerOriginal = original.toLowerCase();
|
||||
let resolvedSubject = "";
|
||||
let resolvedSubject: string;
|
||||
let isThirdPersonSingular = false;
|
||||
|
||||
if (isSelf) {
|
||||
@@ -129,7 +129,8 @@ export function hydrate(content: string, viewer: Entity): string {
|
||||
|
||||
if (followingWord) {
|
||||
if (isThirdPersonSingular) {
|
||||
const conj = nlp(followingWord).verbs().conjugate()[0] as any;
|
||||
const conj = nlp(followingWord).verbs().conjugate()[0] as
|
||||
{ Infinitive?: string; PresentTense?: string } | undefined;
|
||||
if (conj && conj.Infinitive === followingWord && conj.PresentTense) {
|
||||
return `${resolvedSubject} ${conj.PresentTense}`;
|
||||
}
|
||||
@@ -194,7 +195,7 @@ export function hydrateObjective(
|
||||
const entity = worldState.getEntity(id);
|
||||
const name = entity?.attributes.get("name")?.getValue() || id;
|
||||
const lowerOriginal = original.toLowerCase();
|
||||
let resolvedSubject = "";
|
||||
let resolvedSubject: string;
|
||||
let isThirdPersonSingular = false;
|
||||
|
||||
if (firstPersonSet.has(lowerOriginal)) {
|
||||
@@ -220,7 +221,8 @@ export function hydrateObjective(
|
||||
|
||||
if (followingWord) {
|
||||
if (isThirdPersonSingular) {
|
||||
const conj = nlp(followingWord).verbs().conjugate()[0] as any;
|
||||
const conj = nlp(followingWord).verbs().conjugate()[0] as
|
||||
{ Infinitive?: string; PresentTense?: string } | undefined;
|
||||
if (conj && conj.Infinitive === followingWord && conj.PresentTense) {
|
||||
return `${resolvedSubject} ${conj.PresentTense}`;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
AttributeVisibility,
|
||||
} from "@omnia/core";
|
||||
import { MockLLMProvider } from "@omnia/llm";
|
||||
import { IntentSequence } from "@omnia/intent";
|
||||
import { Architect } from "@omnia/architect";
|
||||
import { BufferRepository, BufferEntry } from "@omnia/memory";
|
||||
import {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest";
|
||||
import Database from "better-sqlite3";
|
||||
import { WorldState, Entity, SQLiteRepository } from "@omnia/core";
|
||||
import { MockLLMProvider } from "@omnia/llm";
|
||||
import { IntentDecoder, IntentSequence } from "@omnia/intent";
|
||||
import { IntentDecoder } from "@omnia/intent";
|
||||
import { Architect } from "@omnia/architect";
|
||||
|
||||
describe("Game Loop Integration Tests (Tier 2)", () => {
|
||||
|
||||
Reference in New Issue
Block a user