mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
chore: Format files
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user