mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 20:12:48 +05:30
chore: Format files
This commit is contained in:
@@ -11,7 +11,7 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(":memory:");
|
||||
|
||||
|
||||
// Core database schemas for testing
|
||||
db.exec(`
|
||||
CREATE TABLE objects (
|
||||
@@ -32,8 +32,11 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => {
|
||||
});
|
||||
|
||||
it("should inject both recent memory and recalled long-term memory with subjective aliases resolved", () => {
|
||||
const world = new WorldState("world-123", new Date("2024-01-10T12:00:00.000Z"));
|
||||
|
||||
const world = new WorldState(
|
||||
"world-123",
|
||||
new Date("2024-01-10T12:00:00.000Z"),
|
||||
);
|
||||
|
||||
const alice = new Entity("alice", "tavern");
|
||||
// Add subjective alias for bob
|
||||
alice.aliases.set("bob", "Strider");
|
||||
@@ -85,7 +88,10 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => {
|
||||
});
|
||||
|
||||
it("should not explode if ledger contains no memories or is empty", () => {
|
||||
const world = new WorldState("world-123", new Date("2024-01-10T12:00:00.000Z"));
|
||||
const world = new WorldState(
|
||||
"world-123",
|
||||
new Date("2024-01-10T12:00:00.000Z"),
|
||||
);
|
||||
const alice = new Entity("alice", "tavern");
|
||||
world.addEntity(alice);
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ export class Architect {
|
||||
private timeDeltaGenerator: TimeDeltaGenerator;
|
||||
|
||||
constructor(
|
||||
llmProvider: ILLMProvider | { validator: ILLMProvider; timedelta: ILLMProvider },
|
||||
llmProvider:
|
||||
ILLMProvider | { validator: ILLMProvider; timedelta: ILLMProvider },
|
||||
private repo?: SQLiteRepository,
|
||||
) {
|
||||
let valProv: ILLMProvider;
|
||||
@@ -60,8 +61,12 @@ export class Architect {
|
||||
if (intent.type === "monologue") {
|
||||
return {
|
||||
isValid: true,
|
||||
reason: "Monologue intent bypasses validation (internal thought, not perceivable).",
|
||||
timeDelta: { minutesToAdvance: 0, explanation: "Internal thought — no time elapsed." },
|
||||
reason:
|
||||
"Monologue intent bypasses validation (internal thought, not perceivable).",
|
||||
timeDelta: {
|
||||
minutesToAdvance: 0,
|
||||
explanation: "Internal thought — no time elapsed.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./llm-validator.js";
|
||||
export * from "./architect.js";
|
||||
export * from "./delta.js";
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ export class LLMValidator {
|
||||
if (intent.type === "monologue") {
|
||||
return {
|
||||
isValid: true,
|
||||
reason: "Monologue intents are internal thoughts and bypass validation.",
|
||||
reason:
|
||||
"Monologue intents are internal thoughts and bypass validation.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import Database from "better-sqlite3";
|
||||
import { WorldState, Entity, SQLiteRepository, AttributeVisibility } from "@omnia/core";
|
||||
import {
|
||||
WorldState,
|
||||
Entity,
|
||||
SQLiteRepository,
|
||||
AttributeVisibility,
|
||||
} from "@omnia/core";
|
||||
import { MockLLMProvider } from "@omnia/llm";
|
||||
import { Architect, AliasDeltaGenerator } from "@omnia/architect";
|
||||
import { Intent } from "@omnia/intent";
|
||||
@@ -96,7 +101,10 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
|
||||
const db = new Database(":memory:");
|
||||
const repo = new SQLiteRepository(db);
|
||||
|
||||
const world = new WorldState("world-xyz", new Date("2026-07-06T12:00:00.000Z"));
|
||||
const world = new WorldState(
|
||||
"world-xyz",
|
||||
new Date("2026-07-06T12:00:00.000Z"),
|
||||
);
|
||||
const alice = new Entity("alice");
|
||||
world.addEntity(alice);
|
||||
|
||||
@@ -106,8 +114,14 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
|
||||
// Setup mock LLM responses:
|
||||
// First call: validateIntent (ValidationResult)
|
||||
// Second call: TimeDeltaGenerator (TimeDelta)
|
||||
const mockValidation = { isValid: true, reason: "Alice has the lockpick kit and skill." };
|
||||
const mockTimeDelta = { minutesToAdvance: 20, explanation: "Picking a lock takes time." };
|
||||
const mockValidation = {
|
||||
isValid: true,
|
||||
reason: "Alice has the lockpick kit and skill.",
|
||||
};
|
||||
const mockTimeDelta = {
|
||||
minutesToAdvance: 20,
|
||||
explanation: "Picking a lock takes time.",
|
||||
};
|
||||
const llmProvider = new MockLLMProvider([mockValidation, mockTimeDelta]);
|
||||
|
||||
const architect = new Architect(llmProvider, repo);
|
||||
@@ -131,7 +145,9 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
|
||||
expect(result.timeDelta!.minutesToAdvance).toBe(20);
|
||||
|
||||
// Verify clock was advanced locally
|
||||
const expectedTime = new Date(new Date("2026-07-06T12:00:00.000Z").getTime() + 20 * 60_000);
|
||||
const expectedTime = new Date(
|
||||
new Date("2026-07-06T12:00:00.000Z").getTime() + 20 * 60_000,
|
||||
);
|
||||
expect(world.clock.get().toISOString()).toBe(expectedTime.toISOString());
|
||||
|
||||
// Verify it was persisted to the database
|
||||
@@ -151,7 +167,10 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
|
||||
world.addEntity(bob);
|
||||
repo.saveWorldState(world);
|
||||
|
||||
const mockValidation = { isValid: false, reason: "Bob is bound by chains." };
|
||||
const mockValidation = {
|
||||
isValid: false,
|
||||
reason: "Bob is bound by chains.",
|
||||
};
|
||||
const llmProvider = new MockLLMProvider([mockValidation]); // TimeDeltaGenerator shouldn't be called
|
||||
|
||||
const architect = new Architect(llmProvider, repo);
|
||||
@@ -188,8 +207,16 @@ describe("AliasDeltaGenerator Unit Tests (Tier 1)", () => {
|
||||
const world = new WorldState("world-1");
|
||||
const viewer = new Entity("viewer-1");
|
||||
const target = new Entity("target-1");
|
||||
target.addAttribute("appearance", "A tall elf with silver hair", AttributeVisibility.PUBLIC);
|
||||
target.addAttribute("clothing", "A green tunic", AttributeVisibility.PUBLIC);
|
||||
target.addAttribute(
|
||||
"appearance",
|
||||
"A tall elf with silver hair",
|
||||
AttributeVisibility.PUBLIC,
|
||||
);
|
||||
target.addAttribute(
|
||||
"clothing",
|
||||
"A green tunic",
|
||||
AttributeVisibility.PUBLIC,
|
||||
);
|
||||
world.addEntity(viewer);
|
||||
world.addEntity(target);
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export function naturalizeTime(now: Date, past: Date): string {
|
||||
const nowHour = now.getHours();
|
||||
const pastIsWaking = pastHour >= 5 && pastHour < 22;
|
||||
const nowIsWaking = nowHour >= 5 && nowHour < 22;
|
||||
|
||||
|
||||
const isSameSubjectiveDay = pastIsWaking && nowIsWaking && deltaHours < 18;
|
||||
|
||||
if (isSameSubjectiveDay) {
|
||||
|
||||
@@ -10,4 +10,3 @@ export * from "./world.js";
|
||||
export * from "./clock.js";
|
||||
export * from "./repository.js";
|
||||
export * from "./alias.js";
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { AttributableObject, Attribute, serializeAttributes } from "./attribute.js";
|
||||
import {
|
||||
AttributableObject,
|
||||
Attribute,
|
||||
serializeAttributes,
|
||||
} from "./attribute.js";
|
||||
import { Entity } from "./entity.js";
|
||||
import { WorldClock } from "./clock.js";
|
||||
import { resolveAlias } from "./alias.js";
|
||||
@@ -54,8 +58,15 @@ export function serializeObjectiveWorldState(worldState: WorldState): string {
|
||||
// Serialize world attributes
|
||||
if (worldState.attributes.size > 0) {
|
||||
lines.push("World Attributes:");
|
||||
const worldAttrsStr = serializeAttributes(Array.from(worldState.attributes.values()));
|
||||
lines.push(worldAttrsStr.split("\n").map(l => " " + l).join("\n"));
|
||||
const worldAttrsStr = serializeAttributes(
|
||||
Array.from(worldState.attributes.values()),
|
||||
);
|
||||
lines.push(
|
||||
worldAttrsStr
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
// Serialize locations and their attributes/portals
|
||||
@@ -63,33 +74,46 @@ export function serializeObjectiveWorldState(worldState: WorldState): string {
|
||||
if (worldState.locations.size > 0) {
|
||||
for (const loc of worldState.locations.values()) {
|
||||
lines.push(` - Location [ID: ${loc.id}]:`);
|
||||
|
||||
|
||||
const parentId = (loc as { parentId?: string | null }).parentId;
|
||||
if (parentId) {
|
||||
lines.push(` * Parent Location ID: ${parentId}`);
|
||||
}
|
||||
|
||||
|
||||
if (loc.attributes.size > 0) {
|
||||
const locAttrsStr = serializeAttributes(Array.from(loc.attributes.values()));
|
||||
lines.push(locAttrsStr.split("\n").map(l => " " + l).join("\n"));
|
||||
const locAttrsStr = serializeAttributes(
|
||||
Array.from(loc.attributes.values()),
|
||||
);
|
||||
lines.push(
|
||||
locAttrsStr
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
} else {
|
||||
lines.push(" * (No attributes)");
|
||||
}
|
||||
|
||||
const connections = (loc as { connections?: unknown[] }).connections as {
|
||||
targetId: string;
|
||||
portalName?: string;
|
||||
portalStateDescriptor?: string;
|
||||
visionProp: number;
|
||||
soundProp: number;
|
||||
bidirectional: boolean;
|
||||
}[] | undefined;
|
||||
const connections = (loc as { connections?: unknown[] }).connections as
|
||||
| {
|
||||
targetId: string;
|
||||
portalName?: string;
|
||||
portalStateDescriptor?: string;
|
||||
visionProp: number;
|
||||
soundProp: number;
|
||||
bidirectional: boolean;
|
||||
}[]
|
||||
| undefined;
|
||||
|
||||
if (connections && connections.length > 0) {
|
||||
lines.push(" * Connections:");
|
||||
for (const conn of connections) {
|
||||
const portalStr = conn.portalName ? ` via ${conn.portalName} (${conn.portalStateDescriptor || "normal"})` : "";
|
||||
lines.push(` -> To: ${conn.targetId}${portalStr} (Vision: ${conn.visionProp}, Sound: ${conn.soundProp})`);
|
||||
const portalStr = conn.portalName
|
||||
? ` via ${conn.portalName} (${conn.portalStateDescriptor || "normal"})`
|
||||
: "";
|
||||
lines.push(
|
||||
` -> To: ${conn.targetId}${portalStr} (Vision: ${conn.visionProp}, Sound: ${conn.soundProp})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +130,15 @@ export function serializeObjectiveWorldState(worldState: WorldState): string {
|
||||
lines.push(` * Location ID: ${entity.locationId}`);
|
||||
}
|
||||
if (entity.attributes.size > 0) {
|
||||
const entityAttrsStr = serializeAttributes(Array.from(entity.attributes.values()));
|
||||
lines.push(entityAttrsStr.split("\n").map(l => " " + l).join("\n"));
|
||||
const entityAttrsStr = serializeAttributes(
|
||||
Array.from(entity.attributes.values()),
|
||||
);
|
||||
lines.push(
|
||||
entityAttrsStr
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
} else {
|
||||
lines.push(" * (No attributes)");
|
||||
}
|
||||
@@ -119,7 +150,6 @@ export function serializeObjectiveWorldState(worldState: WorldState): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Serializes a single attribute the way a viewer perceives it — name and
|
||||
* value only, no visibility/ACL metadata (the viewer already sees only
|
||||
@@ -158,13 +188,23 @@ export function serializeSubjectiveWorldState(
|
||||
const worldVisible = worldState.getVisibleAttributesFor(viewerId);
|
||||
if (worldVisible.length > 0) {
|
||||
lines.push("World (as you know it):");
|
||||
lines.push(serializeVisibleAttributes(worldVisible).split("\n").map((l) => " " + l).join("\n"));
|
||||
lines.push(
|
||||
serializeVisibleAttributes(worldVisible)
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Self ---
|
||||
lines.push(`Self (${viewerAlias}):`);
|
||||
const selfVisible = viewer.getVisibleAttributesFor(viewerId);
|
||||
lines.push(serializeVisibleAttributes(selfVisible).split("\n").map((l) => " " + l).join("\n"));
|
||||
lines.push(
|
||||
serializeVisibleAttributes(selfVisible)
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
|
||||
// --- Location / perceived entities ---
|
||||
lines.push("What you perceive around you:");
|
||||
@@ -175,7 +215,12 @@ export function serializeSubjectiveWorldState(
|
||||
const locVisible = location.getVisibleAttributesFor(viewerId);
|
||||
if (locVisible.length > 0) {
|
||||
lines.push(" Location attributes:");
|
||||
lines.push(serializeVisibleAttributes(locVisible).split("\n").map((l) => " " + l).join("\n"));
|
||||
lines.push(
|
||||
serializeVisibleAttributes(locVisible)
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -199,7 +244,12 @@ export function serializeSubjectiveWorldState(
|
||||
const alias = resolveAlias(viewer, e.id);
|
||||
lines.push(` - ${alias}:`);
|
||||
const eVisible = e.getVisibleAttributesFor(viewerId);
|
||||
lines.push(serializeVisibleAttributes(eVisible).split("\n").map((l) => " " + l).join("\n"));
|
||||
lines.push(
|
||||
serializeVisibleAttributes(eVisible)
|
||||
.split("\n")
|
||||
.map((l) => " " + l)
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lines.push(" You are alone here.");
|
||||
|
||||
@@ -11,23 +11,39 @@ describe("TimeNaturalization Unit Tests (Tier 1)", () => {
|
||||
|
||||
test("Tier 1: relative time ranges (< 6 hours)", () => {
|
||||
const now = new Date(2026, 6, 8, 12, 0, 0);
|
||||
|
||||
|
||||
// < 1 minute
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 59, 30))).toBe("just now");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 59, 30))).toBe(
|
||||
"just now",
|
||||
);
|
||||
// < 3 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 58, 0))).toBe("moments ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 58, 0))).toBe(
|
||||
"moments ago",
|
||||
);
|
||||
// < 10 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 52, 0))).toBe("a few minutes ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 52, 0))).toBe(
|
||||
"a few minutes ago",
|
||||
);
|
||||
// < 30 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 40, 0))).toBe("several minutes ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 40, 0))).toBe(
|
||||
"several minutes ago",
|
||||
);
|
||||
// < 45 minutes
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 20, 0))).toBe("about half an hour ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 20, 0))).toBe(
|
||||
"about half an hour ago",
|
||||
);
|
||||
// < 1.5 hours (90 minutes)
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 10, 50, 0))).toBe("about an hour ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 10, 50, 0))).toBe(
|
||||
"about an hour ago",
|
||||
);
|
||||
// < 3 hours
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 9, 30, 0))).toBe("a couple hours ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 9, 30, 0))).toBe(
|
||||
"a couple hours ago",
|
||||
);
|
||||
// < 6 hours
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 7, 0, 0))).toBe("a few hours ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 7, 0, 0))).toBe(
|
||||
"a few hours ago",
|
||||
);
|
||||
});
|
||||
|
||||
test("Tier 2: Same subjective day vs yesterday periods (6h <= delta < 48h)", () => {
|
||||
@@ -35,12 +51,16 @@ describe("TimeNaturalization Unit Tests (Tier 1)", () => {
|
||||
// 9am to 3pm (delta 6h) -> morning
|
||||
const nowWaking = new Date(2026, 6, 8, 15, 0, 0);
|
||||
const pastMorning = new Date(2026, 6, 8, 9, 0, 0);
|
||||
expect(naturalizeTime(nowWaking, pastMorning)).toBe("earlier today, in the morning");
|
||||
expect(naturalizeTime(nowWaking, pastMorning)).toBe(
|
||||
"earlier today, in the morning",
|
||||
);
|
||||
|
||||
// 2pm to 9pm (delta 7h) -> afternoon
|
||||
const nowEvening = new Date(2026, 6, 8, 21, 0, 0);
|
||||
const pastAfternoon = new Date(2026, 6, 8, 14, 0, 0);
|
||||
expect(naturalizeTime(nowEvening, pastAfternoon)).toBe("earlier today, in the afternoon");
|
||||
expect(naturalizeTime(nowEvening, pastAfternoon)).toBe(
|
||||
"earlier today, in the afternoon",
|
||||
);
|
||||
|
||||
// 2. Previous night (past period: night (21-23))
|
||||
// 11pm to 5am (delta 6h) -> last night
|
||||
@@ -52,45 +72,73 @@ describe("TimeNaturalization Unit Tests (Tier 1)", () => {
|
||||
// 1am to 7am (delta 6h) -> around midnight
|
||||
const nowMidnightRun = new Date(2026, 6, 8, 7, 0, 0);
|
||||
const pastMidnight = new Date(2026, 6, 8, 1, 0, 0);
|
||||
expect(naturalizeTime(nowMidnightRun, pastMidnight)).toBe("around midnight");
|
||||
expect(naturalizeTime(nowMidnightRun, pastMidnight)).toBe(
|
||||
"around midnight",
|
||||
);
|
||||
|
||||
// 4. Late night (past period: late night (3-4))
|
||||
// 3am to 9am (delta 6h) -> late last night
|
||||
const nowLateNightRun = new Date(2026, 6, 8, 9, 0, 0);
|
||||
const pastLateNight = new Date(2026, 6, 8, 3, 0, 0);
|
||||
expect(naturalizeTime(nowLateNightRun, pastLateNight)).toBe("late last night");
|
||||
expect(naturalizeTime(nowLateNightRun, pastLateNight)).toBe(
|
||||
"late last night",
|
||||
);
|
||||
|
||||
// 5. Yesterday waking hours (delta >= 6h, diff day / waking check fails)
|
||||
// 3pm to 9am next day (delta 18h) -> yesterday afternoon
|
||||
const nowNextDay = new Date(2026, 6, 9, 9, 0, 0);
|
||||
const pastYesterdayAfternoon = new Date(2026, 6, 8, 15, 0, 0);
|
||||
expect(naturalizeTime(nowNextDay, pastYesterdayAfternoon)).toBe("yesterday afternoon");
|
||||
expect(naturalizeTime(nowNextDay, pastYesterdayAfternoon)).toBe(
|
||||
"yesterday afternoon",
|
||||
);
|
||||
});
|
||||
|
||||
test("Tier 3: Coarse relative time ranges (delta >= 48 hours)", () => {
|
||||
const now = new Date(2026, 6, 10, 12, 0, 0);
|
||||
|
||||
// 2 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 0, 0))).toBe("a couple days ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 8, 11, 0, 0))).toBe(
|
||||
"a couple days ago",
|
||||
);
|
||||
// 3-6 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 6, 12, 0, 0))).toBe("a few days ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 6, 12, 0, 0))).toBe(
|
||||
"a few days ago",
|
||||
);
|
||||
// 7-13 days
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 1, 12, 0, 0))).toBe("about a week ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 6, 1, 12, 0, 0))).toBe(
|
||||
"about a week ago",
|
||||
);
|
||||
// 14-20 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 25, 12, 0, 0))).toBe("a couple weeks ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 25, 12, 0, 0))).toBe(
|
||||
"a couple weeks ago",
|
||||
);
|
||||
// 21-29 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 15, 12, 0, 0))).toBe("a few weeks ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 15, 12, 0, 0))).toBe(
|
||||
"a few weeks ago",
|
||||
);
|
||||
// 30-59 days
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 1, 12, 0, 0))).toBe("about a month ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 5, 1, 12, 0, 0))).toBe(
|
||||
"about a month ago",
|
||||
);
|
||||
// 60-89 days
|
||||
expect(naturalizeTime(now, new Date(2026, 4, 1, 12, 0, 0))).toBe("a couple months ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 4, 1, 12, 0, 0))).toBe(
|
||||
"a couple months ago",
|
||||
);
|
||||
// 90-179 days
|
||||
expect(naturalizeTime(now, new Date(2026, 3, 1, 12, 0, 0))).toBe("a few months ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 3, 1, 12, 0, 0))).toBe(
|
||||
"a few months ago",
|
||||
);
|
||||
// 180-364 days
|
||||
expect(naturalizeTime(now, new Date(2026, 0, 1, 12, 0, 0))).toBe("many months ago");
|
||||
expect(naturalizeTime(now, new Date(2026, 0, 1, 12, 0, 0))).toBe(
|
||||
"many months ago",
|
||||
);
|
||||
// 365-729 days
|
||||
expect(naturalizeTime(now, new Date(2025, 6, 1, 12, 0, 0))).toBe("about a year ago");
|
||||
expect(naturalizeTime(now, new Date(2025, 6, 1, 12, 0, 0))).toBe(
|
||||
"about a year ago",
|
||||
);
|
||||
// >= 730 days
|
||||
expect(naturalizeTime(now, new Date(2023, 6, 1, 12, 0, 0))).toBe("years ago");
|
||||
expect(naturalizeTime(now, new Date(2023, 6, 1, 12, 0, 0))).toBe(
|
||||
"years ago",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,11 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
|
||||
const llm = new MockLLMProvider([mockResponse]);
|
||||
const decoder = new IntentDecoder(llm);
|
||||
|
||||
const result = await decoder.decode(world, "alice", "Alice opened the chest.");
|
||||
const result = await decoder.decode(
|
||||
world,
|
||||
"alice",
|
||||
"Alice opened the chest.",
|
||||
);
|
||||
|
||||
expect(result.intents).toHaveLength(1);
|
||||
expect(result.intents[0].type).toBe("action");
|
||||
|
||||
@@ -5,8 +5,5 @@
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{ "path": "../core" },
|
||||
{ "path": "../llm" }
|
||||
]
|
||||
"references": [{ "path": "../core" }, { "path": "../llm" }]
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@ export const AVAILABLE_PROVIDERS: ModelProviderMeta[] = [
|
||||
{
|
||||
id: "openrouter",
|
||||
displayName: "OpenRouter",
|
||||
description: "Multi-model router supporting Anthropic, OpenAI, DeepSeek, and local models",
|
||||
description:
|
||||
"Multi-model router supporting Anthropic, OpenAI, DeepSeek, and local models",
|
||||
defaultModel: "google/gemini-2.5-flash",
|
||||
defaultEmbeddingModel: "openai/text-embedding-3-small",
|
||||
},
|
||||
|
||||
@@ -43,8 +43,9 @@ function getSettingsDb() {
|
||||
dbPath = path.join(dbDir, "settings.db");
|
||||
}
|
||||
const db = new Database(dbPath);
|
||||
|
||||
db.prepare(`
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS provider_instances (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
@@ -54,22 +55,29 @@ function getSettingsDb() {
|
||||
modelName TEXT,
|
||||
type TEXT NOT NULL DEFAULT 'generative'
|
||||
)
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
|
||||
try {
|
||||
db.prepare(`ALTER TABLE provider_instances ADD COLUMN modelName TEXT`).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE provider_instances ADD COLUMN modelName TEXT`,
|
||||
).run();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
db.prepare(`ALTER TABLE provider_instances ADD COLUMN type TEXT NOT NULL DEFAULT 'generative'`).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE provider_instances ADD COLUMN type TEXT NOT NULL DEFAULT 'generative'`,
|
||||
).run();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
db.prepare(`ALTER TABLE provider_instances ADD COLUMN maxContext INTEGER`).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE provider_instances ADD COLUMN maxContext INTEGER`,
|
||||
).run();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -77,7 +85,9 @@ function getSettingsDb() {
|
||||
// Auto-bootstrap environment variables if DB contains 0 instances
|
||||
try {
|
||||
if (!hasBootstrapped) {
|
||||
const totalCount = db.prepare(`SELECT COUNT(*) as count FROM provider_instances`).get() as { count: number };
|
||||
const totalCount = db
|
||||
.prepare(`SELECT COUNT(*) as count FROM provider_instances`)
|
||||
.get() as { count: number };
|
||||
if (totalCount.count === 0) {
|
||||
const googleKey = process.env.GOOGLE_API_KEY;
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
@@ -85,26 +95,59 @@ function getSettingsDb() {
|
||||
|
||||
if (googleKey && googleKey.trim()) {
|
||||
const id = "provider-default-google";
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, "Gemini (Env)", "google-genai", googleKey.trim(), 1, "gemini-2.5-flash", "generative", 32768);
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"Gemini (Env)",
|
||||
"google-genai",
|
||||
googleKey.trim(),
|
||||
1,
|
||||
"gemini-2.5-flash",
|
||||
"generative",
|
||||
32768,
|
||||
);
|
||||
hasInsertedGenerative = true;
|
||||
|
||||
const embedId = "provider-default-google-embed";
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(embedId, "Gemini Embed (Env)", "google-genai", googleKey.trim(), 1, "gemini-embedding-001", "embedding", 0);
|
||||
`,
|
||||
).run(
|
||||
embedId,
|
||||
"Gemini Embed (Env)",
|
||||
"google-genai",
|
||||
googleKey.trim(),
|
||||
1,
|
||||
"gemini-embedding-001",
|
||||
"embedding",
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
const id = "provider-default-openrouter";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, "OpenRouter (Env)", "openrouter", openRouterKey.trim(), isActive, "google/gemini-2.5-flash", "generative", 32768);
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"OpenRouter (Env)",
|
||||
"openrouter",
|
||||
openRouterKey.trim(),
|
||||
isActive,
|
||||
"google/gemini-2.5-flash",
|
||||
"generative",
|
||||
32768,
|
||||
);
|
||||
}
|
||||
}
|
||||
hasBootstrapped = true;
|
||||
@@ -112,7 +155,7 @@ function getSettingsDb() {
|
||||
} catch {
|
||||
// ignore write lock issues or other DB errors during bootstrap
|
||||
}
|
||||
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -138,7 +181,12 @@ export class ProviderManager {
|
||||
isActive: r.isActive === 1,
|
||||
modelName: r.modelName || undefined,
|
||||
type: (r.type as "generative" | "embedding") || "generative",
|
||||
maxContext: r.maxContext !== undefined && r.maxContext !== null ? r.maxContext : (r.type === "embedding" ? 0 : 32768),
|
||||
maxContext:
|
||||
r.maxContext !== undefined && r.maxContext !== null
|
||||
? r.maxContext
|
||||
: r.type === "embedding"
|
||||
? 0
|
||||
: 32768,
|
||||
}));
|
||||
} finally {
|
||||
db.close();
|
||||
@@ -151,24 +199,51 @@ export class ProviderManager {
|
||||
apiKey: string,
|
||||
modelName?: string,
|
||||
type: "generative" | "embedding" = "generative",
|
||||
maxContext?: number
|
||||
maxContext?: number,
|
||||
): ModelProviderInstance {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const id = "provider-" + Date.now();
|
||||
const activeCount = db
|
||||
.prepare(`SELECT COUNT(*) as count FROM provider_instances WHERE isActive = 1 AND type = ?`)
|
||||
.prepare(
|
||||
`SELECT COUNT(*) as count FROM provider_instances WHERE isActive = 1 AND type = ?`,
|
||||
)
|
||||
.get(type) as { count: number };
|
||||
const isActive = activeCount.count === 0 ? 1 : 0;
|
||||
|
||||
const actualMaxContext = maxContext !== undefined ? maxContext : (type === "generative" ? 32768 : 0);
|
||||
|
||||
db.prepare(`
|
||||
const actualMaxContext =
|
||||
maxContext !== undefined
|
||||
? maxContext
|
||||
: type === "generative"
|
||||
? 32768
|
||||
: 0;
|
||||
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, name, providerName, apiKey, isActive, modelName || null, type, actualMaxContext);
|
||||
|
||||
return { id, name, providerName, apiKey, isActive: isActive === 1, modelName, type, maxContext: actualMaxContext };
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
name,
|
||||
providerName,
|
||||
apiKey,
|
||||
isActive,
|
||||
modelName || null,
|
||||
type,
|
||||
actualMaxContext,
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
providerName,
|
||||
apiKey,
|
||||
isActive: isActive === 1,
|
||||
modelName,
|
||||
type,
|
||||
maxContext: actualMaxContext,
|
||||
};
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
@@ -177,15 +252,19 @@ export class ProviderManager {
|
||||
static delete(id: string): void {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const provider = db.prepare(`SELECT isActive, type FROM provider_instances WHERE id = ?`).get(id) as { isActive: number; type: string } | undefined;
|
||||
const provider = db
|
||||
.prepare(`SELECT isActive, type FROM provider_instances WHERE id = ?`)
|
||||
.get(id) as { isActive: number; type: string } | undefined;
|
||||
db.prepare(`DELETE FROM provider_instances WHERE id = ?`).run(id);
|
||||
|
||||
|
||||
if (provider && provider.isActive === 1) {
|
||||
const next = db
|
||||
.prepare(`SELECT id FROM provider_instances WHERE type = ? LIMIT 1`)
|
||||
.get(provider.type) as { id: string } | undefined;
|
||||
if (next) {
|
||||
db.prepare(`UPDATE provider_instances SET isActive = 1 WHERE id = ?`).run(next.id);
|
||||
db.prepare(
|
||||
`UPDATE provider_instances SET isActive = 1 WHERE id = ?`,
|
||||
).run(next.id);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -196,10 +275,16 @@ export class ProviderManager {
|
||||
static setActive(id: string): void {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const target = db.prepare(`SELECT type FROM provider_instances WHERE id = ?`).get(id) as { type: string } | undefined;
|
||||
const target = db
|
||||
.prepare(`SELECT type FROM provider_instances WHERE id = ?`)
|
||||
.get(id) as { type: string } | undefined;
|
||||
if (target) {
|
||||
db.prepare(`UPDATE provider_instances SET isActive = 0 WHERE type = ?`).run(target.type);
|
||||
db.prepare(`UPDATE provider_instances SET isActive = 1 WHERE id = ?`).run(id);
|
||||
db.prepare(
|
||||
`UPDATE provider_instances SET isActive = 0 WHERE type = ?`,
|
||||
).run(target.type);
|
||||
db.prepare(
|
||||
`UPDATE provider_instances SET isActive = 1 WHERE id = ?`,
|
||||
).run(id);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
@@ -213,75 +298,64 @@ export class ProviderManager {
|
||||
apiKey?: string,
|
||||
modelName?: string,
|
||||
type: "generative" | "embedding" = "generative",
|
||||
maxContext?: number
|
||||
maxContext?: number,
|
||||
): void {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const actualMaxContext = maxContext !== undefined ? maxContext : (type === "generative" ? 32768 : 0);
|
||||
const actualMaxContext =
|
||||
maxContext !== undefined
|
||||
? maxContext
|
||||
: type === "generative"
|
||||
? 32768
|
||||
: 0;
|
||||
if (apiKey && apiKey.trim()) {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE provider_instances
|
||||
SET name = ?, providerName = ?, apiKey = ?, modelName = ?, type = ?, maxContext = ?
|
||||
WHERE id = ?
|
||||
`).run(name, providerName, apiKey, modelName || null, type, actualMaxContext, id);
|
||||
`,
|
||||
).run(
|
||||
name,
|
||||
providerName,
|
||||
apiKey,
|
||||
modelName || null,
|
||||
type,
|
||||
actualMaxContext,
|
||||
id,
|
||||
);
|
||||
} else {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
UPDATE provider_instances
|
||||
SET name = ?, providerName = ?, modelName = ?, type = ?, maxContext = ?
|
||||
WHERE id = ?
|
||||
`).run(name, providerName, modelName || null, type, actualMaxContext, id);
|
||||
`,
|
||||
).run(
|
||||
name,
|
||||
providerName,
|
||||
modelName || null,
|
||||
type,
|
||||
actualMaxContext,
|
||||
id,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
static getActive(type: "generative" | "embedding" = "generative"): ModelProviderInstance | null {
|
||||
static getActive(
|
||||
type: "generative" | "embedding" = "generative",
|
||||
): ModelProviderInstance | null {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
const row = db.prepare(`SELECT * FROM provider_instances WHERE isActive = 1 AND type = ?`).get(type) as {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
apiKey: string;
|
||||
isActive: number;
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
} | undefined;
|
||||
|
||||
if (!row) {
|
||||
const totalCount = db.prepare(`SELECT COUNT(*) as count FROM provider_instances`).get() as { count: number };
|
||||
if (totalCount.count === 0) {
|
||||
const googleKey = process.env.GOOGLE_API_KEY;
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
let hasInsertedGenerative = false;
|
||||
|
||||
if (googleKey && googleKey.trim()) {
|
||||
const id = "provider-default-google";
|
||||
db.prepare(`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, "Gemini (Env)", "google-genai", googleKey.trim(), 1, "gemini-2.5-flash", "generative", 32768);
|
||||
hasInsertedGenerative = true;
|
||||
|
||||
const embedId = "provider-default-google-embed";
|
||||
db.prepare(`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(embedId, "Gemini Embed (Env)", "google-genai", googleKey.trim(), 1, "gemini-embedding-001", "embedding", 0);
|
||||
}
|
||||
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
const id = "provider-default-openrouter";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, "OpenRouter (Env)", "openrouter", openRouterKey.trim(), isActive, "google/gemini-2.5-flash", "generative", 32768);
|
||||
}
|
||||
|
||||
const retryRow = db.prepare(`SELECT * FROM provider_instances WHERE isActive = 1 AND type = ?`).get(type) as {
|
||||
const row = db
|
||||
.prepare(
|
||||
`SELECT * FROM provider_instances WHERE isActive = 1 AND type = ?`,
|
||||
)
|
||||
.get(type) as
|
||||
| {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
@@ -290,7 +364,91 @@ export class ProviderManager {
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
} | undefined;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (!row) {
|
||||
const totalCount = db
|
||||
.prepare(`SELECT COUNT(*) as count FROM provider_instances`)
|
||||
.get() as { count: number };
|
||||
if (totalCount.count === 0) {
|
||||
const googleKey = process.env.GOOGLE_API_KEY;
|
||||
const openRouterKey = process.env.OPENROUTER_API_KEY;
|
||||
let hasInsertedGenerative = false;
|
||||
|
||||
if (googleKey && googleKey.trim()) {
|
||||
const id = "provider-default-google";
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"Gemini (Env)",
|
||||
"google-genai",
|
||||
googleKey.trim(),
|
||||
1,
|
||||
"gemini-2.5-flash",
|
||||
"generative",
|
||||
32768,
|
||||
);
|
||||
hasInsertedGenerative = true;
|
||||
|
||||
const embedId = "provider-default-google-embed";
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
embedId,
|
||||
"Gemini Embed (Env)",
|
||||
"google-genai",
|
||||
googleKey.trim(),
|
||||
1,
|
||||
"gemini-embedding-001",
|
||||
"embedding",
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if (openRouterKey && openRouterKey.trim()) {
|
||||
const id = "provider-default-openrouter";
|
||||
const isActive = hasInsertedGenerative ? 0 : 1;
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName, type, maxContext)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
).run(
|
||||
id,
|
||||
"OpenRouter (Env)",
|
||||
"openrouter",
|
||||
openRouterKey.trim(),
|
||||
isActive,
|
||||
"google/gemini-2.5-flash",
|
||||
"generative",
|
||||
32768,
|
||||
);
|
||||
}
|
||||
|
||||
const retryRow = db
|
||||
.prepare(
|
||||
`SELECT * FROM provider_instances WHERE isActive = 1 AND type = ?`,
|
||||
)
|
||||
.get(type) as
|
||||
| {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
apiKey: string;
|
||||
isActive: number;
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
if (retryRow) {
|
||||
return {
|
||||
@@ -301,24 +459,36 @@ export class ProviderManager {
|
||||
isActive: true,
|
||||
modelName: retryRow.modelName || undefined,
|
||||
type: retryRow.type as "generative" | "embedding",
|
||||
maxContext: retryRow.maxContext !== undefined && retryRow.maxContext !== null ? retryRow.maxContext : (retryRow.type === "embedding" ? 0 : 32768),
|
||||
maxContext:
|
||||
retryRow.maxContext !== undefined &&
|
||||
retryRow.maxContext !== null
|
||||
? retryRow.maxContext
|
||||
: retryRow.type === "embedding"
|
||||
? 0
|
||||
: 32768,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no active row but some rows exist, return the first one as active, or update it
|
||||
const firstRow = db.prepare(`SELECT * FROM provider_instances WHERE type = ? LIMIT 1`).get(type) as {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
apiKey: string;
|
||||
isActive: number;
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
} | undefined;
|
||||
const firstRow = db
|
||||
.prepare(`SELECT * FROM provider_instances WHERE type = ? LIMIT 1`)
|
||||
.get(type) as
|
||||
| {
|
||||
id: string;
|
||||
name: string;
|
||||
providerName: string;
|
||||
apiKey: string;
|
||||
isActive: number;
|
||||
modelName?: string;
|
||||
type: string;
|
||||
maxContext?: number;
|
||||
}
|
||||
| undefined;
|
||||
if (firstRow) {
|
||||
db.prepare(`UPDATE provider_instances SET isActive = 1 WHERE id = ?`).run(firstRow.id);
|
||||
db.prepare(
|
||||
`UPDATE provider_instances SET isActive = 1 WHERE id = ?`,
|
||||
).run(firstRow.id);
|
||||
return {
|
||||
id: firstRow.id,
|
||||
name: firstRow.name,
|
||||
@@ -327,7 +497,12 @@ export class ProviderManager {
|
||||
isActive: true,
|
||||
modelName: firstRow.modelName || undefined,
|
||||
type: firstRow.type as "generative" | "embedding",
|
||||
maxContext: firstRow.maxContext !== undefined && firstRow.maxContext !== null ? firstRow.maxContext : (firstRow.type === "embedding" ? 0 : 32768),
|
||||
maxContext:
|
||||
firstRow.maxContext !== undefined && firstRow.maxContext !== null
|
||||
? firstRow.maxContext
|
||||
: firstRow.type === "embedding"
|
||||
? 0
|
||||
: 32768,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -341,7 +516,12 @@ export class ProviderManager {
|
||||
isActive: true,
|
||||
modelName: row.modelName || undefined,
|
||||
type: (row.type as "generative" | "embedding") || "generative",
|
||||
maxContext: row.maxContext !== undefined && row.maxContext !== null ? row.maxContext : (row.type === "embedding" ? 0 : 32768),
|
||||
maxContext:
|
||||
row.maxContext !== undefined && row.maxContext !== null
|
||||
? row.maxContext
|
||||
: row.type === "embedding"
|
||||
? 0
|
||||
: 32768,
|
||||
};
|
||||
} catch {
|
||||
const googleKey = process.env.GOOGLE_API_KEY;
|
||||
@@ -396,12 +576,14 @@ export class ProviderManager {
|
||||
static getMappings(): Record<string, string> {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS provider_mappings (
|
||||
task TEXT PRIMARY KEY,
|
||||
providerInstanceId TEXT NOT NULL
|
||||
)
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
const rows = db.prepare(`SELECT * FROM provider_mappings`).all() as {
|
||||
task: string;
|
||||
providerInstanceId: string;
|
||||
@@ -419,20 +601,24 @@ export class ProviderManager {
|
||||
static setMapping(task: string, providerInstanceId: string): void {
|
||||
const db = getSettingsDb();
|
||||
try {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
CREATE TABLE IF NOT EXISTS provider_mappings (
|
||||
task TEXT PRIMARY KEY,
|
||||
providerInstanceId TEXT NOT NULL
|
||||
)
|
||||
`).run();
|
||||
`,
|
||||
).run();
|
||||
if (!providerInstanceId) {
|
||||
db.prepare(`DELETE FROM provider_mappings WHERE task = ?`).run(task);
|
||||
} else {
|
||||
db.prepare(`
|
||||
db.prepare(
|
||||
`
|
||||
INSERT INTO provider_mappings (task, providerInstanceId)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(task) DO UPDATE SET providerInstanceId = excluded.providerInstanceId
|
||||
`).run(task, providerInstanceId);
|
||||
`,
|
||||
).run(task, providerInstanceId);
|
||||
}
|
||||
} finally {
|
||||
db.close();
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { z } from "zod";
|
||||
import { ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
|
||||
import { ILLMProvider, LLMRequest, LLMResponse, LLMCallRecord, IEmbeddingProvider } from "../llm.js";
|
||||
import {
|
||||
ChatGoogleGenerativeAI,
|
||||
GoogleGenerativeAIEmbeddings,
|
||||
} from "@langchain/google-genai";
|
||||
import {
|
||||
ILLMProvider,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
LLMCallRecord,
|
||||
IEmbeddingProvider,
|
||||
} from "../llm.js";
|
||||
import { llmConfig } from "../config.js";
|
||||
import { ProviderManager } from "../provider-manager.js";
|
||||
|
||||
export class GeminiProvider implements ILLMProvider {
|
||||
static readonly providerId = "google-genai";
|
||||
static readonly displayName = "Google Gemini";
|
||||
static readonly description = "Official Gemini integration using Google Gen AI SDK";
|
||||
static readonly description =
|
||||
"Official Gemini integration using Google Gen AI SDK";
|
||||
static readonly defaultModel = "gemini-2.5-flash";
|
||||
|
||||
providerName = "Gemini";
|
||||
@@ -17,7 +27,12 @@ export class GeminiProvider implements ILLMProvider {
|
||||
private maxContextUsed?: number;
|
||||
lastCalls: LLMCallRecord[] = [];
|
||||
|
||||
constructor(apiKey?: string, modelName?: string, providerInstanceName?: string, maxContext?: number) {
|
||||
constructor(
|
||||
apiKey?: string,
|
||||
modelName?: string,
|
||||
providerInstanceName?: string,
|
||||
maxContext?: number,
|
||||
) {
|
||||
let key = apiKey;
|
||||
let model = modelName;
|
||||
this.providerInstanceName = providerInstanceName;
|
||||
@@ -47,7 +62,9 @@ export class GeminiProvider implements ILLMProvider {
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error("GOOGLE_API_KEY is required to initialize GeminiProvider");
|
||||
throw new Error(
|
||||
"GOOGLE_API_KEY is required to initialize GeminiProvider",
|
||||
);
|
||||
}
|
||||
|
||||
this.modelNameUsed = model || "gemini-2.5-flash";
|
||||
@@ -60,7 +77,9 @@ export class GeminiProvider implements ILLMProvider {
|
||||
async generateStructuredResponse<T extends z.ZodTypeAny>(
|
||||
request: LLMRequest<T>,
|
||||
): Promise<LLMResponse<z.infer<T>>> {
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, { includeRaw: true });
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, {
|
||||
includeRaw: true,
|
||||
});
|
||||
const result = (await structuredModel.invoke([
|
||||
{ role: "system", content: request.systemPrompt },
|
||||
{ role: "user", content: request.userContext },
|
||||
@@ -84,7 +103,8 @@ export class GeminiProvider implements ILLMProvider {
|
||||
totalTokens: raw?.usage_metadata?.total_tokens || 0,
|
||||
modelName: this.modelNameUsed,
|
||||
providerInstanceName: this.providerInstanceName || "Default",
|
||||
maxContext: this.maxContextUsed !== undefined ? this.maxContextUsed : 32768,
|
||||
maxContext:
|
||||
this.maxContextUsed !== undefined ? this.maxContextUsed : 32768,
|
||||
};
|
||||
|
||||
this.lastCalls.push({
|
||||
@@ -123,7 +143,9 @@ export class GeminiEmbeddingProvider implements IEmbeddingProvider {
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error("GOOGLE_API_KEY is required to initialize GeminiEmbeddingProvider");
|
||||
throw new Error(
|
||||
"GOOGLE_API_KEY is required to initialize GeminiEmbeddingProvider",
|
||||
);
|
||||
}
|
||||
|
||||
this.model = new GoogleGenerativeAIEmbeddings({
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { z } from "zod";
|
||||
import { ILLMProvider, LLMRequest, LLMResponse, LLMCallRecord, IEmbeddingProvider } from "../llm.js";
|
||||
import {
|
||||
ILLMProvider,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
LLMCallRecord,
|
||||
IEmbeddingProvider,
|
||||
} from "../llm.js";
|
||||
|
||||
export class MockLLMProvider implements ILLMProvider {
|
||||
static readonly providerId = "mock";
|
||||
static readonly displayName = "Mock LLM Provider";
|
||||
static readonly description = "Stateless mock provider for testing and offline development";
|
||||
static readonly description =
|
||||
"Stateless mock provider for testing and offline development";
|
||||
static readonly defaultModel = "mock";
|
||||
|
||||
providerName = "mock";
|
||||
@@ -30,7 +37,10 @@ export class MockLLMProvider implements ILLMProvider {
|
||||
const parsed = request.schema.parse(next);
|
||||
return { success: true, data: parsed, usage };
|
||||
} catch (e) {
|
||||
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||
return {
|
||||
success: false,
|
||||
error: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { z } from "zod";
|
||||
import { ChatOpenRouter } from "@langchain/openrouter";
|
||||
import { ILLMProvider, LLMRequest, LLMResponse, LLMCallRecord } from "../llm.js";
|
||||
import {
|
||||
ILLMProvider,
|
||||
LLMRequest,
|
||||
LLMResponse,
|
||||
LLMCallRecord,
|
||||
} from "../llm.js";
|
||||
import { llmConfig } from "../config.js";
|
||||
import { ProviderManager } from "../provider-manager.js";
|
||||
|
||||
export class OpenRouterProvider implements ILLMProvider {
|
||||
static readonly providerId = "openrouter";
|
||||
static readonly displayName = "OpenRouter";
|
||||
static readonly description = "Multi-model router supporting Anthropic, OpenAI, DeepSeek, and local models";
|
||||
static readonly description =
|
||||
"Multi-model router supporting Anthropic, OpenAI, DeepSeek, and local models";
|
||||
static readonly defaultModel = "google/gemini-2.5-flash";
|
||||
|
||||
providerName = "OpenRouter";
|
||||
@@ -17,7 +23,12 @@ export class OpenRouterProvider implements ILLMProvider {
|
||||
private maxContextUsed?: number;
|
||||
lastCalls: LLMCallRecord[] = [];
|
||||
|
||||
constructor(apiKey?: string, modelName?: string, providerInstanceName?: string, maxContext?: number) {
|
||||
constructor(
|
||||
apiKey?: string,
|
||||
modelName?: string,
|
||||
providerInstanceName?: string,
|
||||
maxContext?: number,
|
||||
) {
|
||||
let key = apiKey;
|
||||
let model = modelName;
|
||||
this.providerInstanceName = providerInstanceName;
|
||||
@@ -47,7 +58,9 @@ export class OpenRouterProvider implements ILLMProvider {
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error("OPENROUTER_API_KEY is required to initialize OpenRouterProvider");
|
||||
throw new Error(
|
||||
"OPENROUTER_API_KEY is required to initialize OpenRouterProvider",
|
||||
);
|
||||
}
|
||||
|
||||
this.modelNameUsed = model || "google/gemini-2.5-flash";
|
||||
@@ -60,7 +73,9 @@ export class OpenRouterProvider implements ILLMProvider {
|
||||
async generateStructuredResponse<T extends z.ZodTypeAny>(
|
||||
request: LLMRequest<T>,
|
||||
): Promise<LLMResponse<z.infer<T>>> {
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, { includeRaw: true });
|
||||
const structuredModel = this.model.withStructuredOutput(request.schema, {
|
||||
includeRaw: true,
|
||||
});
|
||||
const result = (await structuredModel.invoke([
|
||||
{ role: "system", content: request.systemPrompt },
|
||||
{ role: "user", content: request.userContext },
|
||||
@@ -84,7 +99,8 @@ export class OpenRouterProvider implements ILLMProvider {
|
||||
totalTokens: raw?.usage_metadata?.total_tokens || 0,
|
||||
modelName: this.modelNameUsed,
|
||||
providerInstanceName: this.providerInstanceName || "Default",
|
||||
maxContext: this.maxContextUsed !== undefined ? this.maxContextUsed : 32768,
|
||||
maxContext:
|
||||
this.maxContextUsed !== undefined ? this.maxContextUsed : 32768,
|
||||
};
|
||||
|
||||
this.lastCalls.push({
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("MockEmbeddingProvider Unit Tests (Tier 1)", () => {
|
||||
expect(vec1.length).toBe(768);
|
||||
expect(vec2.length).toBe(768);
|
||||
expect(vec1).toEqual(vec2); // Deterministic
|
||||
|
||||
|
||||
// Ensure values are numbers between -1.0 and 1.0 (since they are generated with Math.sin)
|
||||
expect(typeof vec1[0]).toBe("number");
|
||||
expect(vec1[0]).toBeGreaterThanOrEqual(-1.0);
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("OpenRouterProvider Unit Tests (Tier 1)", () => {
|
||||
// Save current config
|
||||
const originalKey = llmConfig.OPENROUTER_API_KEY;
|
||||
llmConfig.OPENROUTER_API_KEY = "env-dummy-key";
|
||||
|
||||
|
||||
try {
|
||||
const provider = new OpenRouterProvider();
|
||||
expect(provider.providerName).toBe("OpenRouter");
|
||||
@@ -61,7 +61,7 @@ describe("OpenRouterProvider Unit Tests (Tier 1)", () => {
|
||||
|
||||
try {
|
||||
expect(() => new OpenRouterProvider()).toThrow(
|
||||
"OPENROUTER_API_KEY is required to initialize OpenRouterProvider"
|
||||
"OPENROUTER_API_KEY is required to initialize OpenRouterProvider",
|
||||
);
|
||||
} finally {
|
||||
llmConfig.OPENROUTER_API_KEY = originalKey;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { ProviderManager, setDbPathOverride, resetHasBootstrapped } from "../src/index.js";
|
||||
import {
|
||||
ProviderManager,
|
||||
setDbPathOverride,
|
||||
resetHasBootstrapped,
|
||||
} from "../src/index.js";
|
||||
|
||||
describe("ProviderManager Bootstrapping & CRUD Unit Tests", () => {
|
||||
let tempDbPath: string;
|
||||
@@ -17,7 +21,10 @@ describe("ProviderManager Bootstrapping & CRUD Unit Tests", () => {
|
||||
resetHasBootstrapped();
|
||||
|
||||
// Generate a unique temp database path for this test run
|
||||
tempDbPath = path.resolve(process.cwd(), `test-settings-${Date.now()}-${Math.random().toString(36).substring(2)}.db`);
|
||||
tempDbPath = path.resolve(
|
||||
process.cwd(),
|
||||
`test-settings-${Date.now()}-${Math.random().toString(36).substring(2)}.db`,
|
||||
);
|
||||
setDbPathOverride(tempDbPath);
|
||||
});
|
||||
|
||||
@@ -76,8 +83,14 @@ describe("ProviderManager Bootstrapping & CRUD Unit Tests", () => {
|
||||
expect(bootstrapped.isActive).toBe(true);
|
||||
|
||||
// Edit name and key
|
||||
ProviderManager.update(bootstrapped.id, "My Gemini Key", "google-genai", "new-secret-key", "gemini-2.5-pro");
|
||||
|
||||
ProviderManager.update(
|
||||
bootstrapped.id,
|
||||
"My Gemini Key",
|
||||
"google-genai",
|
||||
"new-secret-key",
|
||||
"gemini-2.5-pro",
|
||||
);
|
||||
|
||||
const listAfterUpdate = ProviderManager.list();
|
||||
expect(listAfterUpdate.length).toBe(2);
|
||||
const updated = listAfterUpdate.find((p) => p.id === bootstrapped.id);
|
||||
|
||||
@@ -26,7 +26,11 @@ export function serializeSubjectiveBufferEntry(
|
||||
const isSelf = viewer.id === entry.intent.actorId;
|
||||
|
||||
if (isSelf) {
|
||||
let details = (entry.intent.selfDescription || entry.intent.description || entry.intent.originalText).trim();
|
||||
let details = (
|
||||
entry.intent.selfDescription ||
|
||||
entry.intent.description ||
|
||||
entry.intent.originalText
|
||||
).trim();
|
||||
if (details.length > 0) {
|
||||
details = details.charAt(0).toUpperCase() + details.slice(1);
|
||||
}
|
||||
@@ -69,7 +73,9 @@ export class BufferRepository {
|
||||
`);
|
||||
|
||||
try {
|
||||
this.db.exec(`ALTER TABLE buffer_entries ADD COLUMN pinned INTEGER DEFAULT 0;`);
|
||||
this.db.exec(
|
||||
`ALTER TABLE buffer_entries ADD COLUMN pinned INTEGER DEFAULT 0;`,
|
||||
);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { Entity, naturalizeTime } from "@omnia/core";
|
||||
import { BufferEntry, serializeSubjectiveBufferEntry, BufferRepository } from "./buffer.js";
|
||||
import {
|
||||
BufferEntry,
|
||||
serializeSubjectiveBufferEntry,
|
||||
BufferRepository,
|
||||
} from "./buffer.js";
|
||||
import { LedgerEntry, LedgerRepository } from "./ledger.js";
|
||||
import { ILLMProvider, IEmbeddingProvider } from "@omnia/llm";
|
||||
|
||||
@@ -56,12 +60,18 @@ function checkSceneExit(entity: Entity, bufferEntries: BufferEntry[]): boolean {
|
||||
|
||||
// Find the location of the most recent buffer entries
|
||||
const lastEntry = bufferEntries[bufferEntries.length - 1];
|
||||
if (lastEntry.locationId && entity.locationId && lastEntry.locationId !== entity.locationId) {
|
||||
if (
|
||||
lastEntry.locationId &&
|
||||
entity.locationId &&
|
||||
lastEntry.locationId !== entity.locationId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check if there are entries from different locations in the buffer
|
||||
const locations = new Set(bufferEntries.map(e => e.locationId).filter(loc => loc !== null));
|
||||
const locations = new Set(
|
||||
bufferEntries.map((e) => e.locationId).filter((loc) => loc !== null),
|
||||
);
|
||||
if (locations.size > 1) {
|
||||
return true;
|
||||
}
|
||||
@@ -75,17 +85,25 @@ function checkIdleDecay(bufferEntries: BufferEntry[]): boolean {
|
||||
|
||||
// Check the last N entries
|
||||
const lastN = bufferEntries.slice(-N);
|
||||
return lastN.every(e => e.intent.type === "monologue");
|
||||
return lastN.every((e) => e.intent.type === "monologue");
|
||||
}
|
||||
|
||||
function checkAttributeTrigger(entity: Entity): boolean {
|
||||
const consciousness = entity.attributes.get("consciousness");
|
||||
if (consciousness && consciousness.getValue().toLowerCase() === "unconscious") {
|
||||
if (
|
||||
consciousness &&
|
||||
consciousness.getValue().toLowerCase() === "unconscious"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const status = entity.attributes.get("status");
|
||||
if (status && ["unconscious", "asleep", "dead", "inactive"].includes(status.getValue().toLowerCase())) {
|
||||
if (
|
||||
status &&
|
||||
["unconscious", "asleep", "dead", "inactive"].includes(
|
||||
status.getValue().toLowerCase(),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -108,7 +126,7 @@ export function checkHandoffTrigger(
|
||||
// Involuntary triggers first (hard)
|
||||
if (maxContext > 0) {
|
||||
const memoryLength = getMemorySectionLength(entity, bufferEntries, now);
|
||||
const charCeiling = maxContext * 4 * 0.60;
|
||||
const charCeiling = maxContext * 4 * 0.6;
|
||||
if (memoryLength > charCeiling) {
|
||||
return "involuntary";
|
||||
}
|
||||
@@ -201,10 +219,12 @@ export class HandoffEngine {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidatesList = candidates.map((entry) => {
|
||||
const serialized = serializeSubjectiveBufferEntry(entry, entity);
|
||||
return `ID: ${entry.id} | Timestamp: ${entry.timestamp} | Location: ${entry.locationId || "None"}\nContent: ${serialized}`;
|
||||
}).join("\n---\n");
|
||||
const candidatesList = candidates
|
||||
.map((entry) => {
|
||||
const serialized = serializeSubjectiveBufferEntry(entry, entity);
|
||||
return `ID: ${entry.id} | Timestamp: ${entry.timestamp} | Location: ${entry.locationId || "None"}\nContent: ${serialized}`;
|
||||
})
|
||||
.join("\n---\n");
|
||||
|
||||
const systemPrompt = `
|
||||
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.
|
||||
@@ -239,7 +259,11 @@ ${candidatesList}
|
||||
}
|
||||
|
||||
const result = response.data;
|
||||
const db = (this.bufferRepo as unknown as { db: { transaction: (fn: () => void) => void } }).db;
|
||||
const db = (
|
||||
this.bufferRepo as unknown as {
|
||||
db: { transaction: (fn: () => void) => void };
|
||||
}
|
||||
).db;
|
||||
|
||||
const ledgerEntries: LedgerEntry[] = [];
|
||||
for (const chunk of result.chunks) {
|
||||
@@ -252,7 +276,11 @@ ${candidatesList}
|
||||
}
|
||||
|
||||
ledgerEntries.push({
|
||||
id: "ledger-" + Math.random().toString(36).substr(2, 9) + "-" + Date.now(),
|
||||
id:
|
||||
"ledger-" +
|
||||
Math.random().toString(36).substr(2, 9) +
|
||||
"-" +
|
||||
Date.now(),
|
||||
ownerId: entity.id,
|
||||
timestamp: now.toISOString(),
|
||||
locationId: entity.locationId,
|
||||
|
||||
@@ -82,7 +82,7 @@ export class LedgerRepository {
|
||||
entry.importance,
|
||||
entry.embedding.length > 0
|
||||
? Buffer.from(new Float32Array(entry.embedding).buffer)
|
||||
: null
|
||||
: null,
|
||||
);
|
||||
|
||||
deleteEntities.run(entry.id);
|
||||
@@ -92,14 +92,18 @@ export class LedgerRepository {
|
||||
})();
|
||||
}
|
||||
|
||||
private mapRowToEntry(row: Record<string, unknown>, involvedEntityIds: string[]): LedgerEntry {
|
||||
private mapRowToEntry(
|
||||
row: Record<string, unknown>,
|
||||
involvedEntityIds: string[],
|
||||
): LedgerEntry {
|
||||
const embedding: number[] = row.embedding
|
||||
? Array.from(
|
||||
new Float32Array(
|
||||
(row.embedding as Buffer).buffer,
|
||||
(row.embedding as Buffer).byteOffset,
|
||||
(row.embedding as Buffer).byteLength / Float32Array.BYTES_PER_ELEMENT
|
||||
)
|
||||
(row.embedding as Buffer).byteLength /
|
||||
Float32Array.BYTES_PER_ELEMENT,
|
||||
),
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -123,7 +127,7 @@ export class LedgerRepository {
|
||||
SELECT id, owner_id, timestamp, location_id, content, quotes_json, importance, embedding
|
||||
FROM ledger_entries
|
||||
WHERE id = ?
|
||||
`
|
||||
`,
|
||||
)
|
||||
.get(id) as Record<string, unknown> | undefined;
|
||||
|
||||
@@ -133,11 +137,14 @@ export class LedgerRepository {
|
||||
.prepare(
|
||||
`
|
||||
SELECT entity_id FROM ledger_involved_entities WHERE entry_id = ?
|
||||
`
|
||||
`,
|
||||
)
|
||||
.all(id) as { entity_id: string }[];
|
||||
|
||||
return this.mapRowToEntry(row, entitiesRows.map((er) => er.entity_id));
|
||||
return this.mapRowToEntry(
|
||||
row,
|
||||
entitiesRows.map((er) => er.entity_id),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +158,7 @@ export class LedgerRepository {
|
||||
ownerId: string,
|
||||
currentLocationId: string | null,
|
||||
currentInvolvedEntityIds: string[],
|
||||
limit: number = 20
|
||||
limit: number = 20,
|
||||
): LedgerEntry[] {
|
||||
let query = `
|
||||
SELECT DISTINCT le.id, le.owner_id, le.timestamp, le.location_id, le.content, le.quotes_json, le.importance, le.embedding
|
||||
@@ -182,7 +189,10 @@ export class LedgerRepository {
|
||||
`;
|
||||
params.push(limit);
|
||||
|
||||
const rows = this.db.prepare(query).all(...params) as Record<string, unknown>[];
|
||||
const rows = this.db.prepare(query).all(...params) as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
|
||||
if (rows.length === 0) return [];
|
||||
|
||||
@@ -193,7 +203,7 @@ export class LedgerRepository {
|
||||
`
|
||||
SELECT entry_id, entity_id FROM ledger_involved_entities
|
||||
WHERE entry_id IN (${placeholders})
|
||||
`
|
||||
`,
|
||||
)
|
||||
.all(...entryIds) as { entry_id: string; entity_id: string }[];
|
||||
|
||||
@@ -205,7 +215,9 @@ export class LedgerRepository {
|
||||
entitiesMap.get(er.entry_id)!.push(er.entity_id);
|
||||
}
|
||||
|
||||
return rows.map((row) => this.mapRowToEntry(row, entitiesMap.get(row.id as string) || []));
|
||||
return rows.map((row) =>
|
||||
this.mapRowToEntry(row, entitiesMap.get(row.id as string) || []),
|
||||
);
|
||||
}
|
||||
|
||||
private fetchRawNeighbors(ownerId: string, timestamp: string): LedgerEntry[] {
|
||||
@@ -220,7 +232,7 @@ export class LedgerRepository {
|
||||
WHERE owner_id = ? AND timestamp < ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
`
|
||||
`,
|
||||
)
|
||||
.get(ownerId, timestamp) as Record<string, unknown> | undefined;
|
||||
|
||||
@@ -237,7 +249,7 @@ export class LedgerRepository {
|
||||
WHERE owner_id = ? AND timestamp > ?
|
||||
ORDER BY timestamp ASC
|
||||
LIMIT 1
|
||||
`
|
||||
`,
|
||||
)
|
||||
.get(ownerId, timestamp) as Record<string, unknown> | undefined;
|
||||
|
||||
@@ -269,16 +281,22 @@ export class LedgerRepository {
|
||||
importanceWeight?: number;
|
||||
relevanceWeight?: number;
|
||||
decayRate?: number;
|
||||
}
|
||||
},
|
||||
): LedgerEntry[] {
|
||||
const includeAssociativeNeighbors = options?.includeAssociativeNeighbors ?? false;
|
||||
const includeAssociativeNeighbors =
|
||||
options?.includeAssociativeNeighbors ?? false;
|
||||
const recencyWeight = options?.recencyWeight ?? 1.0;
|
||||
const importanceWeight = options?.importanceWeight ?? 1.0;
|
||||
const relevanceWeight = options?.relevanceWeight ?? 1.0;
|
||||
const decayRate = options?.decayRate ?? 0.99;
|
||||
|
||||
// Fetch candidate pool (limit 100 to provide enough options for Phase 2 ranking)
|
||||
const candidates = this.getRelevant(ownerId, currentLocationId, currentInvolvedEntityIds, 100);
|
||||
const candidates = this.getRelevant(
|
||||
ownerId,
|
||||
currentLocationId,
|
||||
currentInvolvedEntityIds,
|
||||
100,
|
||||
);
|
||||
if (candidates.length === 0) return [];
|
||||
|
||||
// Score candidates
|
||||
@@ -318,7 +336,10 @@ export class LedgerRepository {
|
||||
for (const entry of selected) {
|
||||
const rawNeighbors = this.fetchRawNeighbors(ownerId, entry.timestamp);
|
||||
for (const rn of rawNeighbors) {
|
||||
if (!finalEntries.some((fe) => fe.id === rn.id) && !neighborMap.has(rn.id)) {
|
||||
if (
|
||||
!finalEntries.some((fe) => fe.id === rn.id) &&
|
||||
!neighborMap.has(rn.id)
|
||||
) {
|
||||
neighborMap.set(rn.id, rn);
|
||||
}
|
||||
}
|
||||
@@ -333,7 +354,7 @@ export class LedgerRepository {
|
||||
`
|
||||
SELECT entry_id, entity_id FROM ledger_involved_entities
|
||||
WHERE entry_id IN (${placeholders})
|
||||
`
|
||||
`,
|
||||
)
|
||||
.all(...neighborIds) as { entry_id: string; entity_id: string }[];
|
||||
|
||||
@@ -353,7 +374,10 @@ export class LedgerRepository {
|
||||
}
|
||||
|
||||
// Sort chronologically ASC for the final prompt output
|
||||
finalEntries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
finalEntries.sort(
|
||||
(a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
|
||||
);
|
||||
|
||||
return finalEntries;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
// Add 12 older entries (older than 30 minutes)
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const minutesAgo = 60 - i;
|
||||
const timestamp = new Date(now.getTime() - minutesAgo * 60 * 1000).toISOString();
|
||||
const timestamp = new Date(
|
||||
now.getTime() - minutesAgo * 60 * 1000,
|
||||
).toISOString();
|
||||
entries.push({
|
||||
id: `entry-old-${i}`,
|
||||
ownerId: "alice",
|
||||
@@ -80,7 +82,13 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
ownerId: "alice",
|
||||
timestamp: now.toISOString(),
|
||||
locationId: "room-1",
|
||||
intent: { type: "dialogue", originalText: "hello", description: "says hello", actorId: "alice", targetIds: [] },
|
||||
intent: {
|
||||
type: "dialogue",
|
||||
originalText: "hello",
|
||||
description: "says hello",
|
||||
actorId: "alice",
|
||||
targetIds: [],
|
||||
},
|
||||
};
|
||||
expect(checkHandoffTrigger(entity, [entryAtRoom1], now)).toBe("voluntary");
|
||||
|
||||
@@ -90,7 +98,13 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
ownerId: "alice",
|
||||
timestamp: now.toISOString(),
|
||||
locationId: "room-2",
|
||||
intent: { type: "monologue", originalText: "think", description: "thinks", actorId: "alice", targetIds: [] },
|
||||
intent: {
|
||||
type: "monologue",
|
||||
originalText: "think",
|
||||
description: "thinks",
|
||||
actorId: "alice",
|
||||
targetIds: [],
|
||||
},
|
||||
}));
|
||||
expect(checkHandoffTrigger(entity, monologues, now)).toBe("voluntary");
|
||||
});
|
||||
@@ -114,7 +128,9 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
|
||||
const entries: BufferEntry[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const timestamp = new Date(now.getTime() - (50 - i) * 60 * 1000).toISOString();
|
||||
const timestamp = new Date(
|
||||
now.getTime() - (50 - i) * 60 * 1000,
|
||||
).toISOString();
|
||||
const entry: BufferEntry = {
|
||||
id: `entry-${i}`,
|
||||
ownerId: "alice",
|
||||
@@ -147,14 +163,23 @@ describe("Memory Handoff Tests (Tier 1)", () => {
|
||||
|
||||
const llmProvider = new MockLLMProvider([mockHandoffResult]);
|
||||
const embedProvider = new MockEmbeddingProvider();
|
||||
const engine = new HandoffEngine(llmProvider, embedProvider, bufferRepo, ledgerRepo);
|
||||
const engine = new HandoffEngine(
|
||||
llmProvider,
|
||||
embedProvider,
|
||||
bufferRepo,
|
||||
ledgerRepo,
|
||||
);
|
||||
|
||||
const success = await engine.runHandoff(entity, entries, now);
|
||||
expect(success).toBe(true);
|
||||
|
||||
const ledgerRows = db.prepare("SELECT * FROM ledger_entries WHERE owner_id = ?").all("alice") as Record<string, unknown>[];
|
||||
const ledgerRows = db
|
||||
.prepare("SELECT * FROM ledger_entries WHERE owner_id = ?")
|
||||
.all("alice") as Record<string, unknown>[];
|
||||
expect(ledgerRows.length).toBe(1);
|
||||
expect(ledgerRows[0].content).toBe("Alice initiated dialogue and performed various tasks.");
|
||||
expect(ledgerRows[0].content).toBe(
|
||||
"Alice initiated dialogue and performed various tasks.",
|
||||
);
|
||||
expect(JSON.parse(ledgerRows[0].quotes_json)).toEqual(["Event 0"]);
|
||||
expect(ledgerRows[0].importance).toBe(5);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("LedgerRepository", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(":memory:");
|
||||
|
||||
|
||||
// We need to create a dummy objects table to satisfy foreign keys
|
||||
db.exec(`
|
||||
CREATE TABLE objects (
|
||||
@@ -51,7 +51,7 @@ describe("LedgerRepository", () => {
|
||||
expect(loaded?.content).toBe(entry.content);
|
||||
expect(loaded?.quotes).toEqual(entry.quotes);
|
||||
expect(loaded?.importance).toBe(5);
|
||||
|
||||
|
||||
// Check float precision
|
||||
expect(loaded?.embedding[0]).toBeCloseTo(0.1);
|
||||
expect(loaded?.embedding[1]).toBeCloseTo(0.2);
|
||||
@@ -113,7 +113,7 @@ describe("LedgerRepository", () => {
|
||||
});
|
||||
|
||||
const relevant = repo.getRelevant("alice", "loc1", ["bob"]);
|
||||
|
||||
|
||||
expect(relevant).toHaveLength(3);
|
||||
const ids = relevant.map((r) => r.id);
|
||||
expect(ids).toContain("mem_high_salience"); // due to importance >= 8
|
||||
@@ -210,16 +210,32 @@ describe("LedgerRepository", () => {
|
||||
});
|
||||
|
||||
// Without neighbors: only returns mem_target
|
||||
const withoutNeighbors = repo.retrieve("alice", "loc1", [], undefined, new Date("2024-01-10T14:00:00.000Z"), 1, {
|
||||
includeAssociativeNeighbors: false,
|
||||
});
|
||||
const withoutNeighbors = repo.retrieve(
|
||||
"alice",
|
||||
"loc1",
|
||||
[],
|
||||
undefined,
|
||||
new Date("2024-01-10T14:00:00.000Z"),
|
||||
1,
|
||||
{
|
||||
includeAssociativeNeighbors: false,
|
||||
},
|
||||
);
|
||||
expect(withoutNeighbors).toHaveLength(1);
|
||||
expect(withoutNeighbors[0].id).toBe("mem_target");
|
||||
|
||||
// With neighbors: returns preceding, target, and succeeding sorted chronologically
|
||||
const withNeighbors = repo.retrieve("alice", "loc1", [], undefined, new Date("2024-01-10T14:00:00.000Z"), 1, {
|
||||
includeAssociativeNeighbors: true,
|
||||
});
|
||||
const withNeighbors = repo.retrieve(
|
||||
"alice",
|
||||
"loc1",
|
||||
[],
|
||||
undefined,
|
||||
new Date("2024-01-10T14:00:00.000Z"),
|
||||
1,
|
||||
{
|
||||
includeAssociativeNeighbors: true,
|
||||
},
|
||||
);
|
||||
expect(withNeighbors).toHaveLength(3);
|
||||
expect(withNeighbors[0].id).toBe("mem_preceding");
|
||||
expect(withNeighbors[1].id).toBe("mem_target");
|
||||
|
||||
@@ -41,7 +41,9 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
};
|
||||
|
||||
const result = serializeSubjectiveBufferEntry(entry, viewer);
|
||||
expect(result).toBe("The hooded figure says, 'Hello there' to the bartender");
|
||||
expect(result).toBe(
|
||||
"The hooded figure says, 'Hello there' to the bartender",
|
||||
);
|
||||
});
|
||||
|
||||
test("serializes action intent with outcome details", () => {
|
||||
@@ -69,7 +71,9 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
};
|
||||
|
||||
const result = serializeSubjectiveBufferEntry(entry, viewer);
|
||||
expect(result).toBe('The hooded figure attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)');
|
||||
expect(result).toBe(
|
||||
"The hooded figure attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)",
|
||||
);
|
||||
});
|
||||
|
||||
test("serializes self-reference and unfamiliar actors", () => {
|
||||
@@ -110,7 +114,10 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const resultUnfamiliar = serializeSubjectiveBufferEntry(entryUnfamiliar, viewer);
|
||||
const resultUnfamiliar = serializeSubjectiveBufferEntry(
|
||||
entryUnfamiliar,
|
||||
viewer,
|
||||
);
|
||||
expect(resultUnfamiliar).toBe("An unfamiliar figure knocks on the door");
|
||||
});
|
||||
});
|
||||
@@ -118,7 +125,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
describe("BufferRepository Persistence Tests (Tier 1)", () => {
|
||||
test("saves, loads, lists, and deletes buffer entries in SQLite database", () => {
|
||||
const db = new Database(":memory:");
|
||||
|
||||
|
||||
// We need SQLiteRepository to initialize the objects table because buffer_entries depends on objects(id) via FK
|
||||
const coreRepo = new SQLiteRepository(db);
|
||||
const repo = new BufferRepository(db);
|
||||
|
||||
@@ -12,17 +12,13 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
|
||||
description: "A spooky old manor.",
|
||||
startTime: "2026-07-09T08:00:00.000Z",
|
||||
world: {
|
||||
attributes: [
|
||||
{ name: "weather", value: "stormy", visibility: "PUBLIC" },
|
||||
],
|
||||
attributes: [{ name: "weather", value: "stormy", visibility: "PUBLIC" }],
|
||||
},
|
||||
locations: [
|
||||
{
|
||||
id: "lobby",
|
||||
parentId: null,
|
||||
attributes: [
|
||||
{ name: "light", value: "dim", visibility: "PUBLIC" },
|
||||
],
|
||||
attributes: [{ name: "light", value: "dim", visibility: "PUBLIC" }],
|
||||
connections: [
|
||||
{
|
||||
targetId: "kitchen",
|
||||
@@ -43,7 +39,12 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
|
||||
id: "investigator",
|
||||
locationId: "lobby",
|
||||
attributes: [
|
||||
{ name: "sanity", value: "100", visibility: "PRIVATE", allowedEntities: ["investigator"] },
|
||||
{
|
||||
name: "sanity",
|
||||
value: "100",
|
||||
visibility: "PRIVATE",
|
||||
allowedEntities: ["investigator"],
|
||||
},
|
||||
],
|
||||
aliases: {
|
||||
ghost: "shadowy specter",
|
||||
@@ -98,11 +99,16 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
|
||||
expect(world).not.toBeNull();
|
||||
expect(world!.id).toBe(targetWorldId);
|
||||
expect(world!.clock.get().toISOString()).toBe("2026-07-09T08:00:00.000Z");
|
||||
expect(world!.attributes.get("name")?.getValue()).toBe("Haunted House Mystery");
|
||||
expect(world!.attributes.get("name")?.getValue()).toBe(
|
||||
"Haunted House Mystery",
|
||||
);
|
||||
expect(world!.attributes.get("weather")?.getValue()).toBe("stormy");
|
||||
|
||||
// 2. Verify Locations loaded with connections & hierarchy
|
||||
const locations = coreRepo.listLocations(targetWorldId, (id, parentId) => new Location(id, parentId));
|
||||
const locations = coreRepo.listLocations(
|
||||
targetWorldId,
|
||||
(id, parentId) => new Location(id, parentId),
|
||||
);
|
||||
expect(locations).toHaveLength(2);
|
||||
|
||||
const lobby = locations.find((l) => l.id === "lobby");
|
||||
@@ -122,7 +128,9 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
|
||||
const loadedInvestigator = world!.getEntity("investigator");
|
||||
expect(loadedInvestigator).toBeDefined();
|
||||
expect(loadedInvestigator!.locationId).toBe("lobby");
|
||||
expect(loadedInvestigator!.attributes.get("sanity")?.getValue()).toBe("100");
|
||||
expect(loadedInvestigator!.attributes.get("sanity")?.getValue()).toBe(
|
||||
"100",
|
||||
);
|
||||
expect(loadedInvestigator!.aliases.get("ghost")).toBe("shadowy specter");
|
||||
|
||||
// 4. Verify pre-seeded memories loaded in BufferRepository
|
||||
|
||||
@@ -10,7 +10,10 @@ import { ScenarioLoader, ScenarioSchema } from "../src/index.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const SCENARIO_PATH = path.resolve(__dirname, "../../../content/demo/scenarios/talking-room.json");
|
||||
const SCENARIO_PATH = path.resolve(
|
||||
__dirname,
|
||||
"../../../content/demo/scenarios/talking-room.json",
|
||||
);
|
||||
|
||||
describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
test("talking-room.json exists, parses, and loads correctly into database", async () => {
|
||||
@@ -37,17 +40,30 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
expect(world).not.toBeNull();
|
||||
expect(world!.attributes.get("name")?.getValue()).toBe("Talking Room");
|
||||
expect(world!.attributes.get("name")?.visibility).toBe("PRIVATE");
|
||||
expect(world!.attributes.get("description")?.getValue()).toBe(scenarioJson.description);
|
||||
expect(world!.attributes.get("description")?.getValue()).toBe(
|
||||
scenarioJson.description,
|
||||
);
|
||||
expect(world!.attributes.get("description")?.visibility).toBe("PRIVATE");
|
||||
expect(world!.attributes.get("experiment_codename")?.getValue()).toBe("Project Tabula Rasa (Phase 3)");
|
||||
expect(world!.attributes.get("experiment_codename")?.visibility).toBe("PRIVATE");
|
||||
expect(world!.attributes.get("experiment_codename")?.getAllowedEntities()).toHaveLength(0); // System only!
|
||||
expect(world!.attributes.get("experiment_codename")?.getValue()).toBe(
|
||||
"Project Tabula Rasa (Phase 3)",
|
||||
);
|
||||
expect(world!.attributes.get("experiment_codename")?.visibility).toBe(
|
||||
"PRIVATE",
|
||||
);
|
||||
expect(
|
||||
world!.attributes.get("experiment_codename")?.getAllowedEntities(),
|
||||
).toHaveLength(0); // System only!
|
||||
|
||||
// 5. Assert location
|
||||
const locations = coreRepo.listLocations(worldInstanceId, (id, parentId) => new Location(id, parentId));
|
||||
const locations = coreRepo.listLocations(
|
||||
worldInstanceId,
|
||||
(id, parentId) => new Location(id, parentId),
|
||||
);
|
||||
expect(locations).toHaveLength(1);
|
||||
expect(locations[0].id).toBe("white-room");
|
||||
expect(locations[0].attributes.get("description")?.getValue()).toContain("A pristine, featureless room");
|
||||
expect(locations[0].attributes.get("description")?.getValue()).toContain(
|
||||
"A pristine, featureless room",
|
||||
);
|
||||
|
||||
// 6. Assert entities and their private attributes / allowedEntities
|
||||
const alphaId = "7c9b83b3-8cfb-4e89-8d77-626a5757d591";
|
||||
@@ -56,14 +72,14 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
const alpha = world!.getEntity(alphaId);
|
||||
expect(alpha).toBeDefined();
|
||||
expect(alpha!.locationId).toBe("white-room");
|
||||
|
||||
|
||||
// Name visibility check
|
||||
const alphaName = alpha!.attributes.get("name")!;
|
||||
expect(alphaName.getValue()).toBe("Bob");
|
||||
expect(alphaName.visibility).toBe("PRIVATE");
|
||||
expect(alphaName.hasAccess(alphaId)).toBe(true);
|
||||
expect(alphaName.hasAccess(betaId)).toBe(false);
|
||||
|
||||
|
||||
// Check system-only attribute (neural_erasure_dose)
|
||||
const alphaDose = alpha!.attributes.get("neural_erasure_dose")!;
|
||||
expect(alphaDose.visibility).toBe("PRIVATE");
|
||||
@@ -81,7 +97,9 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
// Verify subjective aliases can be dynamically resolved via AliasDeltaGenerator
|
||||
const { AliasDeltaGenerator } = await import("@omnia/architect");
|
||||
const { MockLLMProvider } = await import("@omnia/llm");
|
||||
const llmProvider = new MockLLMProvider([{ alias: "the person in the Beta jumpsuit" }]);
|
||||
const llmProvider = new MockLLMProvider([
|
||||
{ alias: "the person in the Beta jumpsuit" },
|
||||
]);
|
||||
const aliasGenerator = new AliasDeltaGenerator(llmProvider);
|
||||
|
||||
const generatedAlias = await aliasGenerator.generate(alpha!, beta!);
|
||||
@@ -94,7 +112,9 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
const subjectiveState = serializeSubjectiveWorldState(world!, alphaId);
|
||||
expect(subjectiveState).toContain("You are at location: white-room");
|
||||
expect(subjectiveState).toContain("Location attributes:");
|
||||
expect(subjectiveState).toContain("description: A pristine, featureless room");
|
||||
expect(subjectiveState).toContain(
|
||||
"description: A pristine, featureless room",
|
||||
);
|
||||
expect(subjectiveState).toContain("lighting: Bright, uniform illumination");
|
||||
|
||||
// Verify objective world state serializes locations (physics awareness)
|
||||
@@ -102,7 +122,9 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
|
||||
const objectiveState = serializeObjectiveWorldState(world!);
|
||||
expect(objectiveState).toContain("Locations:");
|
||||
expect(objectiveState).toContain("- Location [ID: white-room]:");
|
||||
expect(objectiveState).toContain("description: A pristine, featureless room");
|
||||
expect(objectiveState).toContain(
|
||||
"description: A pristine, featureless room",
|
||||
);
|
||||
|
||||
// 7. Assert initial pre-seeded memories
|
||||
const alphaMemories = bufferRepo.listForOwner(alphaId);
|
||||
|
||||
Reference in New Issue
Block a user