ei-tui 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/package.json +3 -1
- package/src/cli/README.md +42 -14
- package/src/cli/mcp.ts +237 -0
- package/src/cli.ts +17 -51
- package/src/core/handlers/human-extraction.ts +6 -0
- package/src/core/handlers/human-matching.ts +45 -10
- package/src/core/llm-client.ts +40 -4
- package/src/core/orchestrators/human-extraction.ts +28 -0
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/processor.ts +37 -41
- package/src/core/prompt-context-builder.ts +1 -0
- package/src/core/queue-processor.ts +13 -4
- package/src/core/tools/builtin/fetch-memory.ts +92 -0
- package/src/core/tools/builtin/fetch-message.ts +123 -0
- package/src/core/tools/builtin/find-memory.ts +99 -0
- package/src/core/tools/index.ts +88 -5
- package/src/integrations/persona-history/importer.ts +3 -1
- package/src/prompts/ceremony/dedup.ts +3 -3
- package/src/prompts/ceremony/types.ts +1 -1
- package/src/prompts/human/person-scan.ts +17 -0
- package/src/prompts/human/types.ts +4 -0
- package/src/prompts/response/sections.ts +14 -7
- package/src/prompts/response/types.ts +1 -0
- package/src/core/tools/builtin/read-memory.ts +0 -70
|
@@ -311,6 +311,34 @@ const EMBEDDING_MIN_SIMILARITY = 0.3;
|
|
|
311
311
|
*/
|
|
312
312
|
export const VALIDATE_MIN_SIMILARITY = 0.92;
|
|
313
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Returns the best cosine similarity between a topic candidate and any existing
|
|
316
|
+
* topic in state. Used by queueTopicValidate to detect near-duplicates after
|
|
317
|
+
* a new topic is created.
|
|
318
|
+
* Returns 0 if no topics exist or embedding fails.
|
|
319
|
+
*/
|
|
320
|
+
export async function getBestTopicSimilarity(
|
|
321
|
+
candidate: TopicScanCandidate,
|
|
322
|
+
state: StateManager
|
|
323
|
+
): Promise<number> {
|
|
324
|
+
const human = state.getHuman();
|
|
325
|
+
const topicsWithEmbeddings = human.topics.filter(t => t.embedding && t.embedding.length > 0);
|
|
326
|
+
if (topicsWithEmbeddings.length === 0) return 0;
|
|
327
|
+
try {
|
|
328
|
+
const embeddingService = getEmbeddingService();
|
|
329
|
+
const candidateText = getTopicEmbeddingText({
|
|
330
|
+
name: candidate.name,
|
|
331
|
+
category: candidate.category,
|
|
332
|
+
description: candidate.description,
|
|
333
|
+
});
|
|
334
|
+
const candidateVector = await embeddingService.embed(candidateText);
|
|
335
|
+
const topK = findTopK(candidateVector, topicsWithEmbeddings, 1);
|
|
336
|
+
return topK.length > 0 ? topK[0].similarity : 0;
|
|
337
|
+
} catch {
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
314
342
|
/**
|
|
315
343
|
* Queue a topic match request using embedding-based similarity (topics only).
|
|
316
344
|
*/
|
package/src/core/processor.ts
CHANGED
|
@@ -36,8 +36,10 @@ import { handlers } from "./handlers/index.js";
|
|
|
36
36
|
import { normalizeRoomMessages, getMessageContent } from "./handlers/utils.js";
|
|
37
37
|
import { sanitizeEiPersonaIdentifiers } from "./utils/identifier-utils.js";
|
|
38
38
|
import { ContextStatus as ContextStatusEnum, RoomMode } from "./types.js";
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
39
|
+
import { registerFindMemoryExecutor, registerFetchMemoryExecutor, registerFetchMessageExecutor, registerFileReadExecutor, SYSTEM_TOOLS } from "./tools/index.js";
|
|
40
|
+
import { createFindMemoryExecutor } from "./tools/builtin/find-memory.js";
|
|
41
|
+
import { createFetchMemoryExecutor } from "./tools/builtin/fetch-memory.js";
|
|
42
|
+
import { createFetchMessageExecutor } from "./tools/builtin/fetch-message.js";
|
|
41
43
|
import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
|
|
42
44
|
import { EMMETT_PERSONA_DEFINITION } from "../templates/emmett.js";
|
|
43
45
|
import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueReflectionDrain, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction, queueTargetedPersonUpdate, queueTargetedTopicUpdate } from "./orchestrators/index.js";
|
|
@@ -240,7 +242,15 @@ export class Processor {
|
|
|
240
242
|
this.seedBuiltinFacts();
|
|
241
243
|
this.migrateLearnedOn();
|
|
242
244
|
this.seedSettings();
|
|
243
|
-
|
|
245
|
+
registerFindMemoryExecutor(createFindMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this), this.stateManager.getHuman.bind(this.stateManager)));
|
|
246
|
+
registerFetchMemoryExecutor(createFetchMemoryExecutor(this.stateManager.getHuman.bind(this.stateManager)));
|
|
247
|
+
registerFetchMessageExecutor(createFetchMessageExecutor(
|
|
248
|
+
this.stateManager.persona_getAll.bind(this.stateManager),
|
|
249
|
+
this.stateManager.messages_get.bind(this.stateManager),
|
|
250
|
+
this.stateManager.getRoomList.bind(this.stateManager),
|
|
251
|
+
this.stateManager.getRoomMessages.bind(this.stateManager),
|
|
252
|
+
(roomId: string) => this.stateManager.getRoom(roomId)?.display_name ?? null
|
|
253
|
+
));
|
|
244
254
|
if (this.isTUI) {
|
|
245
255
|
await registerFileReadExecutor();
|
|
246
256
|
}
|
|
@@ -294,13 +304,12 @@ export class Processor {
|
|
|
294
304
|
}
|
|
295
305
|
return;
|
|
296
306
|
}
|
|
297
|
-
const readMemoryTool = this.stateManager.tools_getByName("read_memory");
|
|
298
307
|
const emmettEntity: PersonaEntity = {
|
|
299
308
|
...EMMETT_PERSONA_DEFINITION,
|
|
300
309
|
id: "emmet",
|
|
301
310
|
display_name: "Emmett",
|
|
302
311
|
last_updated: new Date().toISOString(),
|
|
303
|
-
tools:
|
|
312
|
+
tools: [],
|
|
304
313
|
};
|
|
305
314
|
this.stateManager.persona_add(emmettEntity);
|
|
306
315
|
this.interface.onPersonaAdded?.();
|
|
@@ -334,6 +343,11 @@ export class Processor {
|
|
|
334
343
|
private bootstrapTools(): void {
|
|
335
344
|
const now = new Date().toISOString();
|
|
336
345
|
|
|
346
|
+
for (const name of ["find_memory", "fetch_memory", "fetch_message", "read_memory"]) {
|
|
347
|
+
const tool = this.stateManager.tools_getByName(name);
|
|
348
|
+
if (tool) this.stateManager.tools_remove(tool.id);
|
|
349
|
+
}
|
|
350
|
+
|
|
337
351
|
// --- Ei built-in provider ---
|
|
338
352
|
if (!this.stateManager.tools_getProviderById("ei")) {
|
|
339
353
|
const eiProvider: ToolProvider = {
|
|
@@ -349,35 +363,6 @@ export class Processor {
|
|
|
349
363
|
this.stateManager.tools_addProvider(eiProvider);
|
|
350
364
|
}
|
|
351
365
|
|
|
352
|
-
// read_memory tool
|
|
353
|
-
this.stateManager.tools_upsertBuiltin({
|
|
354
|
-
id: crypto.randomUUID(),
|
|
355
|
-
provider_id: "ei",
|
|
356
|
-
name: "read_memory",
|
|
357
|
-
display_name: "Read Memory",
|
|
358
|
-
description:
|
|
359
|
-
"Search Ei's persistent knowledge base — facts, topics, people, and quotes learned across ALL conversations over time, not just this one. Use this when you need context about the user, their life, relationships, or interests that may not be visible in the current exchange. Use `recent: true` to retrieve what's been discussed recently.",
|
|
360
|
-
input_schema: {
|
|
361
|
-
type: "object",
|
|
362
|
-
properties: {
|
|
363
|
-
query: { type: "string", description: "What to search for — a person, topic, fact, or anything Ei has learned about the user" },
|
|
364
|
-
types: {
|
|
365
|
-
type: "array",
|
|
366
|
-
items: { type: "string", enum: ["fact", "topic", "person", "quote"] },
|
|
367
|
-
description: "Limit search to specific memory types (default: all types)",
|
|
368
|
-
},
|
|
369
|
-
limit: { type: "number", description: "Max results to return (default: 10, max: 20)" },
|
|
370
|
-
recent: { type: "boolean", description: "If true, return recently-mentioned results sorted by last_mentioned date instead of relevance. Combine with a query to filter recent results by topic." },
|
|
371
|
-
},
|
|
372
|
-
required: [],
|
|
373
|
-
},
|
|
374
|
-
runtime: "any",
|
|
375
|
-
builtin: true,
|
|
376
|
-
enabled: true,
|
|
377
|
-
created_at: now,
|
|
378
|
-
max_calls_per_interaction: 6, // Dedup needs to verify relationships before irreversible merges. Typical cluster (3-8 items) requires: parent concept lookup + 2 relationship verifications + context validation. Still under HARD_TOOL_CALL_LIMIT (10).
|
|
379
|
-
});
|
|
380
|
-
|
|
381
366
|
// file_read tool (TUI only)
|
|
382
367
|
this.stateManager.tools_upsertBuiltin({
|
|
383
368
|
id: crypto.randomUUID(),
|
|
@@ -817,6 +802,20 @@ export class Processor {
|
|
|
817
802
|
max_calls_per_interaction: 1,
|
|
818
803
|
created_at: now,
|
|
819
804
|
});
|
|
805
|
+
|
|
806
|
+
// --- Reconcile pass: prune stale tool references from persona tool lists ---
|
|
807
|
+
// Build manifest of all tool IDs currently in state (everything seeded above).
|
|
808
|
+
const manifestIds = new Set(this.stateManager.tools_getAll().map(t => t.id));
|
|
809
|
+
|
|
810
|
+
for (const persona of this.stateManager.persona_getAll()) {
|
|
811
|
+
if (!persona.tools?.length) continue;
|
|
812
|
+
const pruned = persona.tools.filter(id => manifestIds.has(id));
|
|
813
|
+
if (pruned.length !== persona.tools.length) {
|
|
814
|
+
const removed = persona.tools.length - pruned.length;
|
|
815
|
+
this.stateManager.persona_update(persona.id, { tools: pruned });
|
|
816
|
+
console.log(`[Processor] Pruned ${removed} stale tool reference(s) from persona "${persona.display_name}"`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
820
819
|
}
|
|
821
820
|
|
|
822
821
|
/**
|
|
@@ -1093,10 +1092,10 @@ const toolNextSteps = new Set([
|
|
|
1093
1092
|
personaId ??
|
|
1094
1093
|
(request.next_step === LLMNextStep.HandleEiHeartbeat ? "ei" : undefined);
|
|
1095
1094
|
|
|
1096
|
-
// Dedup operates on Human data, not persona data
|
|
1095
|
+
// Dedup operates on Human data, not persona data — provide find_memory from SYSTEM_TOOLS directly.
|
|
1097
1096
|
// Also covers HandleToolContinuation originating from a dedup request: the
|
|
1098
1097
|
// continuation rebuilds tool lists from scratch and has no personaId, so without
|
|
1099
|
-
// this check Opus loses
|
|
1098
|
+
// this check Opus loses find_memory access after round 1.
|
|
1100
1099
|
const isDedupRequest =
|
|
1101
1100
|
request.next_step === LLMNextStep.HandleDedupCurate ||
|
|
1102
1101
|
(request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
@@ -1104,12 +1103,9 @@ const toolNextSteps = new Set([
|
|
|
1104
1103
|
|
|
1105
1104
|
let tools: ToolDefinition[] = [];
|
|
1106
1105
|
if (isDedupRequest) {
|
|
1107
|
-
|
|
1108
|
-
if (readMemory?.enabled) {
|
|
1109
|
-
tools = [readMemory];
|
|
1110
|
-
}
|
|
1106
|
+
tools = SYSTEM_TOOLS.filter(t => t.name === "find_memory");
|
|
1111
1107
|
} else if (toolNextSteps.has(request.next_step) && toolPersonaId) {
|
|
1112
|
-
tools = this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI);
|
|
1108
|
+
tools = [...SYSTEM_TOOLS, ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
|
|
1113
1109
|
}
|
|
1114
1110
|
|
|
1115
1111
|
// Auto-inject each handler's dedicated submit tool — infrastructure, not user-visible.
|
|
@@ -274,6 +274,7 @@ export async function buildResponsePromptData(
|
|
|
274
274
|
|
|
275
275
|
const alwaysMessages = sm.messages_getAlways(persona.id);
|
|
276
276
|
const temporalAnchors = alwaysMessages.map(m => ({
|
|
277
|
+
id: m.id,
|
|
277
278
|
role: m.role === "human" ? "human" as const : "system" as const,
|
|
278
279
|
content: m.content,
|
|
279
280
|
silence_reason: m.silence_reason,
|
|
@@ -508,6 +508,10 @@ export class QueueProcessor {
|
|
|
508
508
|
const cleaned = cleanResponseContent(reformatContent);
|
|
509
509
|
try {
|
|
510
510
|
const parsed = parseJSONResponse(cleaned);
|
|
511
|
+
if (!parsed || typeof parsed !== 'object' || Object.keys(parsed as object).length === 0) {
|
|
512
|
+
console.warn(`[QueueProcessor] Reformat pass returned empty object for handleToolContinuation — falling through to retry`);
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
511
515
|
console.log(`[QueueProcessor] Reformat pass succeeded for handleToolContinuation`);
|
|
512
516
|
return {
|
|
513
517
|
request,
|
|
@@ -544,11 +548,10 @@ export class QueueProcessor {
|
|
|
544
548
|
): Promise<LLMResponse | null> {
|
|
545
549
|
const reformatUserPrompt =
|
|
546
550
|
`An earlier version of you responded with the following content, but it could not ` +
|
|
547
|
-
`be parsed as valid JSON.
|
|
548
|
-
`
|
|
549
|
-
`are needed.\n\n---\n${malformedContent}\n---` +
|
|
551
|
+
`be parsed as valid JSON. Fix the syntax and return the corrected JSON object. ` +
|
|
552
|
+
`Return ONLY the fixed JSON — do not omit any fields or data from the original.\n\n---\n${malformedContent}\n---` +
|
|
550
553
|
`\n\nThe user does NOT know there was a problem - This request is from Ei to you to try to fix it for them.` +
|
|
551
|
-
`\n\n**CRITICAL INSTRUCTION** - DO NOT OMIT ANY DATA.
|
|
554
|
+
`\n\n**CRITICAL INSTRUCTION** - DO NOT OMIT ANY DATA. Return all original fields intact with only syntax corrected.`;
|
|
552
555
|
|
|
553
556
|
try {
|
|
554
557
|
const { content: reformatContent, finishReason: reformatReason } = await callLLMRaw(
|
|
@@ -563,6 +566,12 @@ export class QueueProcessor {
|
|
|
563
566
|
if (!reformatContent) return null;
|
|
564
567
|
|
|
565
568
|
const cleaned = cleanResponseContent(reformatContent);
|
|
569
|
+
const shrinkageRatio = cleaned.length / malformedContent.length;
|
|
570
|
+
if (shrinkageRatio < 0.95) {
|
|
571
|
+
console.warn(`[QueueProcessor] JSON reformat response too small for ${request.next_step} — ${cleaned.length} chars vs ${malformedContent.length} original (${Math.round(shrinkageRatio * 100)}%) — treating as data loss, falling through to retry`);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
566
575
|
try {
|
|
567
576
|
const parsed = parseJSONResponse(cleaned);
|
|
568
577
|
console.log(`[QueueProcessor] JSON reformat pass succeeded for ${request.next_step} — saved a retry`);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ToolExecutor } from "../types.js";
|
|
2
|
+
import type { Fact, Topic, Person, Quote, HumanEntity } from "../../types.js";
|
|
3
|
+
|
|
4
|
+
type GetHuman = () => HumanEntity;
|
|
5
|
+
|
|
6
|
+
function cleanFact(f: Fact): Record<string, unknown> {
|
|
7
|
+
const { embedding, rewrite_checked, persona_groups, ...rest } = f;
|
|
8
|
+
void embedding; void rewrite_checked; void persona_groups;
|
|
9
|
+
return rest;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function cleanTopic(t: Topic): Record<string, unknown> {
|
|
13
|
+
const { embedding, rewrite_checked, persona_groups, last_ei_asked, ...rest } = t;
|
|
14
|
+
void embedding; void rewrite_checked; void persona_groups; void last_ei_asked;
|
|
15
|
+
return rest;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cleanPerson(p: Person): Record<string, unknown> {
|
|
19
|
+
const { embedding, rewrite_checked, persona_groups, last_ei_asked, ...rest } = p;
|
|
20
|
+
void embedding; void rewrite_checked; void persona_groups; void last_ei_asked;
|
|
21
|
+
return rest;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function cleanQuote(
|
|
25
|
+
q: Quote,
|
|
26
|
+
facts: Fact[],
|
|
27
|
+
topics: Topic[],
|
|
28
|
+
people: Person[]
|
|
29
|
+
): Record<string, unknown> {
|
|
30
|
+
const { embedding, persona_groups, data_item_ids, ...rest } = q;
|
|
31
|
+
void embedding; void persona_groups;
|
|
32
|
+
|
|
33
|
+
const linked_items: Array<{ id: string; name: string; type: string }> = [];
|
|
34
|
+
for (const id of data_item_ids) {
|
|
35
|
+
const fact = facts.find(f => f.id === id);
|
|
36
|
+
if (fact) { linked_items.push({ id: fact.id, name: fact.name, type: "fact" }); continue; }
|
|
37
|
+
const topic = topics.find(t => t.id === id);
|
|
38
|
+
if (topic) { linked_items.push({ id: topic.id, name: topic.name, type: "topic" }); continue; }
|
|
39
|
+
const person = people.find(p => p.id === id);
|
|
40
|
+
if (person) { linked_items.push({ id: person.id, name: person.name, type: "person" }); continue; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { ...rest, linked_items };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createFetchMemoryExecutor(getHuman: GetHuman): ToolExecutor {
|
|
47
|
+
return {
|
|
48
|
+
name: "fetch_memory",
|
|
49
|
+
|
|
50
|
+
async execute(args: Record<string, unknown>): Promise<string> {
|
|
51
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
52
|
+
console.log(`[fetch_memory] called with id="${id}"`);
|
|
53
|
+
|
|
54
|
+
if (!id) {
|
|
55
|
+
console.warn("[fetch_memory] missing id argument");
|
|
56
|
+
return JSON.stringify({ error: "Missing required argument: id" });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const human = getHuman();
|
|
60
|
+
|
|
61
|
+
const fact = human.facts.find(f => f.id === id);
|
|
62
|
+
if (fact) {
|
|
63
|
+
console.log(`[fetch_memory] found fact id="${id}" name="${fact.name}"`);
|
|
64
|
+
return JSON.stringify({ type: "fact", ...cleanFact(fact) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const topic = human.topics.find(t => t.id === id);
|
|
68
|
+
if (topic) {
|
|
69
|
+
console.log(`[fetch_memory] found topic id="${id}" name="${topic.name}"`);
|
|
70
|
+
return JSON.stringify({ type: "topic", ...cleanTopic(topic) });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const person = human.people.find(p => p.id === id);
|
|
74
|
+
if (person) {
|
|
75
|
+
console.log(`[fetch_memory] found person id="${id}" name="${person.name}"`);
|
|
76
|
+
return JSON.stringify({ type: "person", ...cleanPerson(person) });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const quote = human.quotes.find(q => q.id === id);
|
|
80
|
+
if (quote) {
|
|
81
|
+
console.log(`[fetch_memory] found quote id="${id}"`);
|
|
82
|
+
return JSON.stringify({
|
|
83
|
+
type: "quote",
|
|
84
|
+
...cleanQuote(quote, human.facts, human.topics, human.people),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`[fetch_memory] no entity found for id="${id}"`);
|
|
89
|
+
return JSON.stringify({ error: "No accessible record found for this ID" });
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ToolExecutor } from "../types.js";
|
|
2
|
+
import type { Message } from "../../types.js";
|
|
3
|
+
import type { RoomMessage, RoomSummary } from "../../types/rooms.js";
|
|
4
|
+
import type { PersonaEntity } from "../../types/entities.js";
|
|
5
|
+
|
|
6
|
+
interface CleanMessage {
|
|
7
|
+
id: string;
|
|
8
|
+
role: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
silence_reason?: string;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
speaker_name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type GetAllPersonas = () => PersonaEntity[];
|
|
16
|
+
type GetPersonaMessages = (personaId: string) => Message[];
|
|
17
|
+
type GetRoomList = () => RoomSummary[];
|
|
18
|
+
type GetRoomMessages = (roomId: string) => RoomMessage[];
|
|
19
|
+
type GetRoomDisplayName = (roomId: string) => string | null;
|
|
20
|
+
|
|
21
|
+
function stripMessage(m: Message): CleanMessage {
|
|
22
|
+
return {
|
|
23
|
+
id: m.id,
|
|
24
|
+
role: m.role,
|
|
25
|
+
...(m.content !== undefined ? { content: m.content } : {}),
|
|
26
|
+
...(m.silence_reason !== undefined ? { silence_reason: m.silence_reason } : {}),
|
|
27
|
+
timestamp: m.timestamp,
|
|
28
|
+
...(m.speaker_name !== undefined ? { speaker_name: m.speaker_name } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stripRoomMessage(m: RoomMessage, personaDisplayName?: string): CleanMessage {
|
|
33
|
+
return {
|
|
34
|
+
id: m.id,
|
|
35
|
+
role: m.role,
|
|
36
|
+
...(m.content !== undefined ? { content: m.content } : {}),
|
|
37
|
+
...(m.silence_reason !== undefined ? { silence_reason: m.silence_reason } : {}),
|
|
38
|
+
timestamp: m.timestamp,
|
|
39
|
+
...(personaDisplayName !== undefined ? { speaker_name: personaDisplayName } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createFetchMessageExecutor(
|
|
44
|
+
getAllPersonas: GetAllPersonas,
|
|
45
|
+
getPersonaMessages: GetPersonaMessages,
|
|
46
|
+
getRoomList: GetRoomList,
|
|
47
|
+
getRoomMessages: GetRoomMessages,
|
|
48
|
+
getRoomDisplayName: GetRoomDisplayName
|
|
49
|
+
): ToolExecutor {
|
|
50
|
+
return {
|
|
51
|
+
name: "fetch_message",
|
|
52
|
+
|
|
53
|
+
async execute(args: Record<string, unknown>): Promise<string> {
|
|
54
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
55
|
+
const before = typeof args.before === "number" && args.before > 0 ? Math.floor(args.before) : 0;
|
|
56
|
+
const after = typeof args.after === "number" && args.after > 0 ? Math.floor(args.after) : 0;
|
|
57
|
+
|
|
58
|
+
console.log(`[fetch_message] called with id="${id}", before=${before}, after=${after}`);
|
|
59
|
+
|
|
60
|
+
if (!id) {
|
|
61
|
+
console.warn("[fetch_message] missing id argument");
|
|
62
|
+
return JSON.stringify({ error: "Missing required argument: id" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const personas = getAllPersonas();
|
|
66
|
+
|
|
67
|
+
// TODO: add persona access gate when calling context is available —
|
|
68
|
+
// the execute() signature has no requestingPersonaId, so we search all personas.
|
|
69
|
+
for (const persona of personas) {
|
|
70
|
+
const messages = getPersonaMessages(persona.id);
|
|
71
|
+
const idx = messages.findIndex(m => m.id === id);
|
|
72
|
+
if (idx === -1) continue;
|
|
73
|
+
|
|
74
|
+
const msg = messages[idx];
|
|
75
|
+
const beforeMsgs = messages.slice(Math.max(0, idx - before), idx).map(stripMessage);
|
|
76
|
+
const afterMsgs = messages.slice(idx + 1, idx + 1 + after).map(stripMessage);
|
|
77
|
+
|
|
78
|
+
console.log(`[fetch_message] found in persona "${persona.display_name}" at idx=${idx}`);
|
|
79
|
+
return JSON.stringify({
|
|
80
|
+
message: stripMessage(msg),
|
|
81
|
+
before: beforeMsgs,
|
|
82
|
+
after: afterMsgs,
|
|
83
|
+
persona: persona.display_name,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// TODO: add persona access gate when calling context is available.
|
|
88
|
+
const rooms = getRoomList();
|
|
89
|
+
for (const roomSummary of rooms) {
|
|
90
|
+
const messages = getRoomMessages(roomSummary.id);
|
|
91
|
+
const idx = messages.findIndex(m => m.id === id);
|
|
92
|
+
if (idx === -1) continue;
|
|
93
|
+
|
|
94
|
+
const msg = messages[idx];
|
|
95
|
+
const roomDisplayName = getRoomDisplayName(roomSummary.id) ?? roomSummary.display_name;
|
|
96
|
+
|
|
97
|
+
const resolvePersonaName = (m: RoomMessage): string | undefined => {
|
|
98
|
+
if (m.role !== "persona" || !m.persona_id) return undefined;
|
|
99
|
+
const p = personas.find(pe => pe.id === m.persona_id);
|
|
100
|
+
return p?.display_name;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const beforeMsgs = messages
|
|
104
|
+
.slice(Math.max(0, idx - before), idx)
|
|
105
|
+
.map(m => stripRoomMessage(m, resolvePersonaName(m)));
|
|
106
|
+
const afterMsgs = messages
|
|
107
|
+
.slice(idx + 1, idx + 1 + after)
|
|
108
|
+
.map(m => stripRoomMessage(m, resolvePersonaName(m)));
|
|
109
|
+
|
|
110
|
+
console.log(`[fetch_message] found in room "${roomDisplayName}" at idx=${idx}`);
|
|
111
|
+
return JSON.stringify({
|
|
112
|
+
message: stripRoomMessage(msg, resolvePersonaName(msg)),
|
|
113
|
+
before: beforeMsgs,
|
|
114
|
+
after: afterMsgs,
|
|
115
|
+
persona: roomDisplayName,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`[fetch_message] message not found for id="${id}"`);
|
|
120
|
+
return JSON.stringify({ error: "Message not found" });
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ToolExecutor } from "../types.js";
|
|
2
|
+
import type { Fact, Topic, Person, Quote, HumanEntity } from "../../types.js";
|
|
3
|
+
|
|
4
|
+
interface PersonaSummary {
|
|
5
|
+
id: string;
|
|
6
|
+
display_name: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type SearchHumanData = (
|
|
10
|
+
query: string,
|
|
11
|
+
options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean; persona_filter?: string }
|
|
12
|
+
) => Promise<{ facts: Fact[]; topics: Topic[]; people: Person[]; quotes: Quote[] }>;
|
|
13
|
+
|
|
14
|
+
type GetPersonaList = () => Promise<PersonaSummary[]>;
|
|
15
|
+
|
|
16
|
+
type GetHuman = () => HumanEntity;
|
|
17
|
+
|
|
18
|
+
export function createFindMemoryExecutor(searchHumanData: SearchHumanData, getPersonaList?: GetPersonaList, getHuman?: GetHuman): ToolExecutor {
|
|
19
|
+
return {
|
|
20
|
+
name: "find_memory",
|
|
21
|
+
|
|
22
|
+
async execute(args: Record<string, unknown>): Promise<string> {
|
|
23
|
+
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
24
|
+
const recent = args.recent === true;
|
|
25
|
+
const personaArg = typeof args.persona === "string" ? args.persona.trim() : "";
|
|
26
|
+
console.log(`[find_memory] called with query="${query}", types=${JSON.stringify(args.types ?? null)}, limit=${args.limit ?? 10}, recent=${recent}, persona="${personaArg}"`);
|
|
27
|
+
|
|
28
|
+
if (!query && !recent) {
|
|
29
|
+
console.warn("[find_memory] missing query argument");
|
|
30
|
+
return JSON.stringify({ error: "Missing required argument: query (or use recent: true)" });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const typeMap: Record<string, "fact" | "topic" | "person" | "quote"> = {
|
|
34
|
+
fact: "fact", facts: "fact",
|
|
35
|
+
topic: "topic", topics: "topic",
|
|
36
|
+
person: "person", people: "person",
|
|
37
|
+
quote: "quote", quotes: "quote",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const types = Array.isArray(args.types)
|
|
41
|
+
? (args.types
|
|
42
|
+
.filter((t): t is string => typeof t === "string")
|
|
43
|
+
.map(t => typeMap[t])
|
|
44
|
+
.filter((t): t is "fact" | "topic" | "person" | "quote" => t !== undefined)
|
|
45
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
46
|
+
)
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
const limit = typeof args.limit === "number" && args.limit > 0 ? Math.min(args.limit, 20) : 10;
|
|
50
|
+
|
|
51
|
+
// Resolve persona display_name to ID
|
|
52
|
+
let persona_filter: string | undefined;
|
|
53
|
+
if (personaArg && getPersonaList) {
|
|
54
|
+
const personas = await getPersonaList();
|
|
55
|
+
const match = personas.find(p => p.display_name.toLowerCase() === personaArg.toLowerCase());
|
|
56
|
+
if (match) {
|
|
57
|
+
persona_filter = match.id;
|
|
58
|
+
console.log(`[find_memory] resolved persona "${personaArg}" to ID "${persona_filter}"`);
|
|
59
|
+
} else {
|
|
60
|
+
console.warn(`[find_memory] persona "${personaArg}" not found, proceeding without filter`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const results = await searchHumanData(query, { types, limit, recent, persona_filter });
|
|
65
|
+
|
|
66
|
+
const total = results.facts.length + results.topics.length + results.people.length + results.quotes.length;
|
|
67
|
+
console.log(`[find_memory] query="${query}" => ${total} results (facts=${results.facts.length}, topics=${results.topics.length}, people=${results.people.length}, quotes=${results.quotes.length})`);
|
|
68
|
+
|
|
69
|
+
const output: Record<string, unknown[]> = {};
|
|
70
|
+
if (results.facts.length > 0) output.facts = results.facts.map(f => ({ id: f.id, name: f.name, description: f.description }));
|
|
71
|
+
if (results.topics.length > 0) output.topics = results.topics.map(t => ({ id: t.id, name: t.name, description: t.description }));
|
|
72
|
+
if (results.people.length > 0) output.people = results.people.map(p => ({ id: p.id, name: p.name, relationship: p.relationship, description: p.description, identifiers: p.identifiers ?? [] }));
|
|
73
|
+
|
|
74
|
+
if (results.quotes.length > 0) {
|
|
75
|
+
const human = getHuman ? getHuman() : null;
|
|
76
|
+
output.quotes = results.quotes.map(q => {
|
|
77
|
+
const linked_items: Array<{ id: string; name: string; type: string }> = [];
|
|
78
|
+
if (human && q.data_item_ids.length > 0) {
|
|
79
|
+
for (const itemId of q.data_item_ids) {
|
|
80
|
+
const fact = human.facts.find(f => f.id === itemId);
|
|
81
|
+
if (fact) { linked_items.push({ id: fact.id, name: fact.name, type: "fact" }); continue; }
|
|
82
|
+
const topic = human.topics.find(t => t.id === itemId);
|
|
83
|
+
if (topic) { linked_items.push({ id: topic.id, name: topic.name, type: "topic" }); continue; }
|
|
84
|
+
const person = human.people.find(p => p.id === itemId);
|
|
85
|
+
if (person) { linked_items.push({ id: person.id, name: person.name, type: "person" }); }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { id: q.id, text: q.text, speaker: q.speaker, linked_items };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (Object.keys(output).length === 0) {
|
|
93
|
+
return JSON.stringify({ result: "No relevant memories found for this query." });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return JSON.stringify(output);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|