ei-tui 1.0.1 → 1.2.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 +2 -21
- package/src/cli/README.md +42 -14
- package/src/cli/mcp.ts +237 -0
- package/src/cli.ts +17 -51
- package/src/core/handlers/dedup.ts +4 -15
- package/src/core/handlers/document-segmentation.ts +2 -3
- package/src/core/handlers/heartbeat.ts +5 -10
- package/src/core/handlers/human-extraction.ts +6 -0
- package/src/core/handlers/human-matching.ts +53 -10
- package/src/core/handlers/index.ts +2 -0
- package/src/core/handlers/knowledge-synthesis.ts +50 -0
- package/src/core/handlers/persona-generation.ts +4 -8
- package/src/core/handlers/persona-response.ts +3 -4
- package/src/core/handlers/persona-topics.ts +2 -4
- package/src/core/handlers/rewrite.ts +26 -9
- package/src/core/handlers/rooms.ts +6 -12
- package/src/core/llm-client.ts +53 -7
- package/src/core/message-manager.ts +2 -4
- package/src/core/orchestrators/ceremony.ts +44 -13
- package/src/core/orchestrators/human-extraction.ts +38 -1
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/processor.ts +192 -41
- package/src/core/prompt-context-builder.ts +1 -0
- package/src/core/queue-manager.ts +10 -0
- package/src/core/queue-processor.ts +13 -4
- package/src/core/state-manager.ts +35 -0
- 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/core/tools/types.ts +1 -1
- package/src/core/types/data-items.ts +1 -1
- package/src/core/types/entities.ts +7 -1
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +3 -1
- package/src/integrations/claude-code/importer.ts +6 -0
- package/src/integrations/cursor/importer.ts +6 -0
- package/src/integrations/document/unsource.ts +5 -3
- package/src/integrations/opencode/importer.ts +13 -1
- package/src/integrations/persona-history/importer.ts +12 -1
- package/src/prompts/ceremony/dedup.ts +3 -3
- package/src/prompts/ceremony/people-rewrite.ts +2 -2
- package/src/prompts/ceremony/topic-rewrite.ts +2 -2
- 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/index.ts +3 -0
- package/src/prompts/response/sections.ts +14 -7
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/synthesis/index.ts +101 -0
- package/src/prompts/synthesis/types.ts +26 -0
- package/tui/src/commands/generate.tsx +98 -0
- package/tui/src/commands/unsource.tsx +17 -10
- package/tui/src/components/GeneratedDocsOverlay.tsx +136 -0
- package/tui/src/components/PromptInput.tsx +2 -0
- package/tui/src/context/ei.tsx +49 -2
- package/tui/src/util/logger.ts +22 -2
- package/tui/src/util/provider-detection.ts +5 -2
- package/tui/src/util/yaml-provider.ts +2 -8
- package/src/core/tools/builtin/read-memory.ts +0 -70
|
@@ -137,10 +137,7 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
|
|
|
137
137
|
messages_analyze: unextractedPeople,
|
|
138
138
|
extraction_flag: "p",
|
|
139
139
|
};
|
|
140
|
-
|
|
141
|
-
? { ...options, reflection_progress: 1 }
|
|
142
|
-
: options;
|
|
143
|
-
queuePersonScan(context, state, personScanOptions);
|
|
140
|
+
queuePersonScan(context, state, options);
|
|
144
141
|
}
|
|
145
142
|
|
|
146
143
|
const totalUnextracted = unextractedFacts.length + unextractedTopics.length + unextractedPeople.length;
|
|
@@ -287,7 +284,7 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
287
284
|
// Person Rewrite phase (phase 4): scan bloated Person records, extract Topics from them.
|
|
288
285
|
// Gated via ceremony_progress so Topic Rewrite can run after — Topics created here
|
|
289
286
|
// need to be visible before Topic Rewrite snapshots the threshold.
|
|
290
|
-
queuePersonRewritePhase(state);
|
|
287
|
+
queuePersonRewritePhase(state, { ceremonyProgress: 4 });
|
|
291
288
|
|
|
292
289
|
// Zero-work guard: if no person rewrites queued, advance to topic rewrite immediately
|
|
293
290
|
if (!state.queue_hasPendingCeremonies()) {
|
|
@@ -482,7 +479,7 @@ export function queueReflectionDrain(personaId: string, state: StateManager): vo
|
|
|
482
479
|
messages_analyze: unextractedPeople,
|
|
483
480
|
extraction_flag: "p",
|
|
484
481
|
};
|
|
485
|
-
queuePersonScan(context, state
|
|
482
|
+
queuePersonScan(context, state);
|
|
486
483
|
console.log(`[reflection:drain] Queued Person scan for ${persona.display_name} (${unextractedPeople.length} messages) — clears on completion`);
|
|
487
484
|
}
|
|
488
485
|
|
|
@@ -490,7 +487,7 @@ function getRewriteModel(state: StateManager): string | undefined {
|
|
|
490
487
|
return state.getHuman().settings?.rewrite_model;
|
|
491
488
|
}
|
|
492
489
|
|
|
493
|
-
export function queuePersonRewritePhase(state: StateManager): void {
|
|
490
|
+
export function queuePersonRewritePhase(state: StateManager, options?: { ceremonyProgress?: number }): void {
|
|
494
491
|
const rewriteModel = getRewriteModel(state);
|
|
495
492
|
if (!rewriteModel) {
|
|
496
493
|
console.log("[ceremony:rewrite] rewrite_model not set — skipping person rewrite phase");
|
|
@@ -498,13 +495,30 @@ export function queuePersonRewritePhase(state: StateManager): void {
|
|
|
498
495
|
}
|
|
499
496
|
|
|
500
497
|
const human = state.getHuman();
|
|
501
|
-
const
|
|
498
|
+
const allCandidates = human.people.filter(person => {
|
|
502
499
|
const isPersonaLinked = (person.identifiers ?? []).some(
|
|
503
500
|
i => i.type.toLowerCase() === 'ei persona'
|
|
504
501
|
);
|
|
505
502
|
return !isPersonaLinked
|
|
506
|
-
&& (person.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD
|
|
507
|
-
|
|
503
|
+
&& (person.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const alreadyChecked = allCandidates.filter(p => {
|
|
507
|
+
const descLen = p.description?.length ?? 0;
|
|
508
|
+
return p.rewrite_length_floor !== undefined && descLen < p.rewrite_length_floor;
|
|
509
|
+
});
|
|
510
|
+
if (alreadyChecked.length > 0) {
|
|
511
|
+
for (const person of alreadyChecked) {
|
|
512
|
+
console.log(
|
|
513
|
+
`[ceremony:rewrite] Person "${person.name}" is ${person.description?.length ?? 0} chars ` +
|
|
514
|
+
`(floor: ${person.rewrite_length_floor}) — already reviewed, skipping`
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const personsToScan = allCandidates.filter(p => {
|
|
520
|
+
if (p.rewrite_length_floor === undefined) return true;
|
|
521
|
+
return (p.description?.length ?? 0) >= p.rewrite_length_floor;
|
|
508
522
|
});
|
|
509
523
|
|
|
510
524
|
if (personsToScan.length === 0) {
|
|
@@ -527,7 +541,7 @@ export function queuePersonRewritePhase(state: StateManager): void {
|
|
|
527
541
|
itemId: person.id,
|
|
528
542
|
itemType: "person" as RewriteItemType,
|
|
529
543
|
rewriteModel,
|
|
530
|
-
ceremony_progress:
|
|
544
|
+
...(options?.ceremonyProgress !== undefined && { ceremony_progress: options.ceremonyProgress }),
|
|
531
545
|
},
|
|
532
546
|
});
|
|
533
547
|
}
|
|
@@ -543,11 +557,28 @@ export function queueTopicRewritePhase(state: StateManager): void {
|
|
|
543
557
|
}
|
|
544
558
|
|
|
545
559
|
const human = state.getHuman();
|
|
546
|
-
const
|
|
560
|
+
const allCandidateTopics = human.topics.filter(topic =>
|
|
547
561
|
(topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD
|
|
548
|
-
&& !topic.rewrite_checked
|
|
549
562
|
);
|
|
550
563
|
|
|
564
|
+
const alreadyCheckedTopics = allCandidateTopics.filter(t => {
|
|
565
|
+
const descLen = t.description?.length ?? 0;
|
|
566
|
+
return t.rewrite_length_floor !== undefined && descLen < t.rewrite_length_floor;
|
|
567
|
+
});
|
|
568
|
+
if (alreadyCheckedTopics.length > 0) {
|
|
569
|
+
for (const topic of alreadyCheckedTopics) {
|
|
570
|
+
console.log(
|
|
571
|
+
`[ceremony:rewrite] Topic "${topic.name}" is ${topic.description?.length ?? 0} chars ` +
|
|
572
|
+
`(floor: ${topic.rewrite_length_floor}) — already reviewed, skipping`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const topicsToScan = allCandidateTopics.filter(t => {
|
|
578
|
+
if (t.rewrite_length_floor === undefined) return true;
|
|
579
|
+
return (t.description?.length ?? 0) >= t.rewrite_length_floor;
|
|
580
|
+
});
|
|
581
|
+
|
|
551
582
|
if (topicsToScan.length === 0) {
|
|
552
583
|
console.log("[ceremony:rewrite] No topics above threshold — skipping topic rewrite phase");
|
|
553
584
|
return;
|
|
@@ -195,6 +195,15 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
|
|
|
195
195
|
|
|
196
196
|
if (chunks.length === 0) return 0;
|
|
197
197
|
|
|
198
|
+
// If the persona has a pending_update (reflection in progress), gate person
|
|
199
|
+
// scans so handleHumanPersonScan won't queue updates for persona-linked people.
|
|
200
|
+
// This prevents importers and other callers from bypassing the reflection lock
|
|
201
|
+
// — they don't know about pending_update, so we enforce it here centrally.
|
|
202
|
+
const persona = state.persona_getById(context.personaId);
|
|
203
|
+
const effectiveOptions: ExtractionOptions | undefined = persona?.pending_update
|
|
204
|
+
? { ...options, reflection_progress: 1 }
|
|
205
|
+
: options;
|
|
206
|
+
|
|
198
207
|
// Pre-mark messages before enqueuing — prevents duplicate scans if the
|
|
199
208
|
// queue check fires again during LLM latency (100ms loop × 5s call = 50 dupes)
|
|
200
209
|
for (const chunk of chunks) {
|
|
@@ -225,7 +234,7 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
|
|
|
225
234
|
user: prompt.user,
|
|
226
235
|
next_step: LLMNextStep.HandleHumanPersonScan,
|
|
227
236
|
data: {
|
|
228
|
-
...
|
|
237
|
+
...effectiveOptions,
|
|
229
238
|
personaId: chunk.personaId,
|
|
230
239
|
personaDisplayName: chunk.channelDisplayName,
|
|
231
240
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
@@ -311,6 +320,34 @@ const EMBEDDING_MIN_SIMILARITY = 0.3;
|
|
|
311
320
|
*/
|
|
312
321
|
export const VALIDATE_MIN_SIMILARITY = 0.92;
|
|
313
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Returns the best cosine similarity between a topic candidate and any existing
|
|
325
|
+
* topic in state. Used by queueTopicValidate to detect near-duplicates after
|
|
326
|
+
* a new topic is created.
|
|
327
|
+
* Returns 0 if no topics exist or embedding fails.
|
|
328
|
+
*/
|
|
329
|
+
export async function getBestTopicSimilarity(
|
|
330
|
+
candidate: TopicScanCandidate,
|
|
331
|
+
state: StateManager
|
|
332
|
+
): Promise<number> {
|
|
333
|
+
const human = state.getHuman();
|
|
334
|
+
const topicsWithEmbeddings = human.topics.filter(t => t.embedding && t.embedding.length > 0);
|
|
335
|
+
if (topicsWithEmbeddings.length === 0) return 0;
|
|
336
|
+
try {
|
|
337
|
+
const embeddingService = getEmbeddingService();
|
|
338
|
+
const candidateText = getTopicEmbeddingText({
|
|
339
|
+
name: candidate.name,
|
|
340
|
+
category: candidate.category,
|
|
341
|
+
description: candidate.description,
|
|
342
|
+
});
|
|
343
|
+
const candidateVector = await embeddingService.embed(candidateText);
|
|
344
|
+
const topK = findTopK(candidateVector, topicsWithEmbeddings, 1);
|
|
345
|
+
return topK.length > 0 ? topK[0].similarity : 0;
|
|
346
|
+
} catch {
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
314
351
|
/**
|
|
315
352
|
* Queue a topic match request using embedding-based similarity (topics only).
|
|
316
353
|
*/
|
package/src/core/processor.ts
CHANGED
|
@@ -36,12 +36,15 @@ 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";
|
|
44
46
|
import { finishDocumentBatch } from "./handlers/document-segmentation.js";
|
|
47
|
+
import { buildSynthesisPrompt } from "../prompts/synthesis/index.js";
|
|
45
48
|
import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
|
|
46
49
|
import { DEFAULT_SEED_TRAITS } from "./constants/seed-traits.js";
|
|
47
50
|
|
|
@@ -240,7 +243,15 @@ export class Processor {
|
|
|
240
243
|
this.seedBuiltinFacts();
|
|
241
244
|
this.migrateLearnedOn();
|
|
242
245
|
this.seedSettings();
|
|
243
|
-
|
|
246
|
+
registerFindMemoryExecutor(createFindMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this), this.stateManager.getHuman.bind(this.stateManager)));
|
|
247
|
+
registerFetchMemoryExecutor(createFetchMemoryExecutor(this.stateManager.getHuman.bind(this.stateManager)));
|
|
248
|
+
registerFetchMessageExecutor(createFetchMessageExecutor(
|
|
249
|
+
this.stateManager.persona_getAll.bind(this.stateManager),
|
|
250
|
+
this.stateManager.messages_get.bind(this.stateManager),
|
|
251
|
+
this.stateManager.getRoomList.bind(this.stateManager),
|
|
252
|
+
this.stateManager.getRoomMessages.bind(this.stateManager),
|
|
253
|
+
(roomId: string) => this.stateManager.getRoom(roomId)?.display_name ?? null
|
|
254
|
+
));
|
|
244
255
|
if (this.isTUI) {
|
|
245
256
|
await registerFileReadExecutor();
|
|
246
257
|
}
|
|
@@ -294,13 +305,12 @@ export class Processor {
|
|
|
294
305
|
}
|
|
295
306
|
return;
|
|
296
307
|
}
|
|
297
|
-
const readMemoryTool = this.stateManager.tools_getByName("read_memory");
|
|
298
308
|
const emmettEntity: PersonaEntity = {
|
|
299
309
|
...EMMETT_PERSONA_DEFINITION,
|
|
300
310
|
id: "emmet",
|
|
301
311
|
display_name: "Emmett",
|
|
302
312
|
last_updated: new Date().toISOString(),
|
|
303
|
-
tools:
|
|
313
|
+
tools: [],
|
|
304
314
|
};
|
|
305
315
|
this.stateManager.persona_add(emmettEntity);
|
|
306
316
|
this.interface.onPersonaAdded?.();
|
|
@@ -326,6 +336,140 @@ export class Processor {
|
|
|
326
336
|
return executeUnsource(preview, this.stateManager);
|
|
327
337
|
}
|
|
328
338
|
|
|
339
|
+
async generateDocument(subject: string): Promise<{ slug: string }> {
|
|
340
|
+
this.bootstrapEmmett();
|
|
341
|
+
const slugBase = subject
|
|
342
|
+
.toLowerCase()
|
|
343
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
344
|
+
.slice(0, 40);
|
|
345
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
346
|
+
const slug = `${slugBase}_${timestamp}`;
|
|
347
|
+
|
|
348
|
+
const primary = await this.searchHumanData(subject, { limit: 20 });
|
|
349
|
+
if (
|
|
350
|
+
primary.facts.length === 0 &&
|
|
351
|
+
primary.topics.length === 0 &&
|
|
352
|
+
primary.people.length === 0 &&
|
|
353
|
+
primary.quotes.length === 0
|
|
354
|
+
) {
|
|
355
|
+
throw new Error(`No knowledge found about '${subject}'`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const seenQuoteIds = new Set<string>();
|
|
359
|
+
const seenItemIds = new Set<string>(
|
|
360
|
+
[...primary.topics, ...primary.people, ...primary.facts].map(i => i.id)
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const MAX_QUOTES_PER_ENTITY = 3;
|
|
364
|
+
|
|
365
|
+
const enrichTopic = (topic: import("../prompts/synthesis/types.js").EnrichedTopic["topic"]) => {
|
|
366
|
+
const linked = this.stateManager.human_quote_getForDataItem(topic.id)
|
|
367
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
368
|
+
.slice(0, MAX_QUOTES_PER_ENTITY);
|
|
369
|
+
linked.forEach(q => seenQuoteIds.add(q.id));
|
|
370
|
+
return { topic, quotes: linked };
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const enrichPerson = (person: import("../prompts/synthesis/types.js").EnrichedPerson["person"]) => {
|
|
374
|
+
const linked = this.stateManager.human_quote_getForDataItem(person.id)
|
|
375
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
376
|
+
.slice(0, MAX_QUOTES_PER_ENTITY);
|
|
377
|
+
linked.forEach(q => seenQuoteIds.add(q.id));
|
|
378
|
+
return { person, quotes: linked };
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const enrichedTopics = primary.topics.map(enrichTopic);
|
|
382
|
+
const enrichedPeople = primary.people.map(enrichPerson);
|
|
383
|
+
|
|
384
|
+
const human = this.stateManager.getHuman();
|
|
385
|
+
const allItems = [...human.facts, ...human.topics, ...human.people];
|
|
386
|
+
|
|
387
|
+
const MAX_SECONDARY_ENTITIES = 10;
|
|
388
|
+
|
|
389
|
+
const secondaryTopics: typeof enrichedTopics = [];
|
|
390
|
+
const secondaryPeople: typeof enrichedPeople = [];
|
|
391
|
+
const secondaryFacts: typeof primary.facts = [];
|
|
392
|
+
|
|
393
|
+
outer: for (const quote of [...enrichedTopics.flatMap(e => e.quotes), ...enrichedPeople.flatMap(e => e.quotes)]) {
|
|
394
|
+
for (const itemId of quote.data_item_ids) {
|
|
395
|
+
if (secondaryTopics.length + secondaryPeople.length + secondaryFacts.length >= MAX_SECONDARY_ENTITIES) break outer;
|
|
396
|
+
if (seenItemIds.has(itemId)) continue;
|
|
397
|
+
seenItemIds.add(itemId);
|
|
398
|
+
const item = allItems.find(i => i.id === itemId);
|
|
399
|
+
if (!item) continue;
|
|
400
|
+
if (human.topics.find(t => t.id === itemId)) {
|
|
401
|
+
secondaryTopics.push(enrichTopic(item as typeof primary.topics[0]));
|
|
402
|
+
} else if (human.people.find(p => p.id === itemId)) {
|
|
403
|
+
secondaryPeople.push(enrichPerson(item as typeof primary.people[0]));
|
|
404
|
+
} else if (human.facts.find(f => f.id === itemId)) {
|
|
405
|
+
secondaryFacts.push(item as typeof primary.facts[0]);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const standaloneQuotes = primary.quotes.filter(q => !seenQuoteIds.has(q.id));
|
|
411
|
+
|
|
412
|
+
const allLoadedFacts = [...primary.facts, ...secondaryFacts];
|
|
413
|
+
const allLoadedTopics = [...enrichedTopics, ...secondaryTopics];
|
|
414
|
+
const allLoadedPeople = [...enrichedPeople, ...secondaryPeople];
|
|
415
|
+
|
|
416
|
+
const loadedEntityNames = new Map<string, string>();
|
|
417
|
+
for (const f of allLoadedFacts) loadedEntityNames.set(f.id, f.name);
|
|
418
|
+
for (const { topic } of allLoadedTopics) loadedEntityNames.set(topic.id, topic.name);
|
|
419
|
+
for (const { person } of allLoadedPeople) loadedEntityNames.set(person.id, person.name);
|
|
420
|
+
|
|
421
|
+
const prompt = buildSynthesisPrompt({
|
|
422
|
+
subject,
|
|
423
|
+
facts: allLoadedFacts,
|
|
424
|
+
topics: allLoadedTopics,
|
|
425
|
+
people: allLoadedPeople,
|
|
426
|
+
standaloneQuotes,
|
|
427
|
+
loadedEntityNames,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const model = this.stateManager.getHuman().settings?.rewrite_model
|
|
431
|
+
?? this.stateManager.getHuman().settings?.default_model;
|
|
432
|
+
|
|
433
|
+
this.stateManager.queue_enqueue({
|
|
434
|
+
type: LLMRequestType.Raw,
|
|
435
|
+
priority: LLMPriority.Normal,
|
|
436
|
+
system: prompt.system,
|
|
437
|
+
user: prompt.user,
|
|
438
|
+
next_step: LLMNextStep.HandleKnowledgeSynthesis,
|
|
439
|
+
model,
|
|
440
|
+
data: { slug, subject },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return { slug };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
checkGenerationModel(): { model: string; isRewriteModel: boolean } {
|
|
447
|
+
const settings = this.stateManager.getHuman().settings;
|
|
448
|
+
if (settings?.rewrite_model) {
|
|
449
|
+
return { model: settings.rewrite_model, isRewriteModel: true };
|
|
450
|
+
}
|
|
451
|
+
return { model: settings?.default_model ?? "unknown", isRewriteModel: false };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async getGeneratedDocumentContent(slug: string): Promise<string | null> {
|
|
455
|
+
const messages = this.stateManager.messages_get("emmet");
|
|
456
|
+
const target = `generate:document:${slug}`;
|
|
457
|
+
const message = messages.find(m => m.source_tag === target);
|
|
458
|
+
return message?.content ?? null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async reRunDocument(slug: string): Promise<{ slug: string }> {
|
|
462
|
+
const docs = this.stateManager.getHuman().settings?.document?.processed_documents ?? {};
|
|
463
|
+
const entry = docs[slug];
|
|
464
|
+
if (!entry || entry.type !== "generated" || !entry.subject) {
|
|
465
|
+
throw new Error(`No generated document found for slug "${slug}"`);
|
|
466
|
+
}
|
|
467
|
+
const subject = entry.subject;
|
|
468
|
+
const preview = this.getUnsourcePreview(`generate:document:${slug}`);
|
|
469
|
+
await this.executeUnsource(preview);
|
|
470
|
+
return this.generateDocument(subject);
|
|
471
|
+
}
|
|
472
|
+
|
|
329
473
|
/**
|
|
330
474
|
* Seed built-in tool providers and tools if they don't exist yet.
|
|
331
475
|
* Called on every startup (after state load/restore) — safe to call repeatedly.
|
|
@@ -334,6 +478,11 @@ export class Processor {
|
|
|
334
478
|
private bootstrapTools(): void {
|
|
335
479
|
const now = new Date().toISOString();
|
|
336
480
|
|
|
481
|
+
for (const name of ["find_memory", "fetch_memory", "fetch_message", "read_memory"]) {
|
|
482
|
+
const tool = this.stateManager.tools_getByName(name);
|
|
483
|
+
if (tool) this.stateManager.tools_remove(tool.id);
|
|
484
|
+
}
|
|
485
|
+
|
|
337
486
|
// --- Ei built-in provider ---
|
|
338
487
|
if (!this.stateManager.tools_getProviderById("ei")) {
|
|
339
488
|
const eiProvider: ToolProvider = {
|
|
@@ -349,35 +498,6 @@ export class Processor {
|
|
|
349
498
|
this.stateManager.tools_addProvider(eiProvider);
|
|
350
499
|
}
|
|
351
500
|
|
|
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
501
|
// file_read tool (TUI only)
|
|
382
502
|
this.stateManager.tools_upsertBuiltin({
|
|
383
503
|
id: crypto.randomUUID(),
|
|
@@ -817,6 +937,20 @@ export class Processor {
|
|
|
817
937
|
max_calls_per_interaction: 1,
|
|
818
938
|
created_at: now,
|
|
819
939
|
});
|
|
940
|
+
|
|
941
|
+
// --- Reconcile pass: prune stale tool references from persona tool lists ---
|
|
942
|
+
// Build manifest of all tool IDs currently in state (everything seeded above).
|
|
943
|
+
const manifestIds = new Set(this.stateManager.tools_getAll().map(t => t.id));
|
|
944
|
+
|
|
945
|
+
for (const persona of this.stateManager.persona_getAll()) {
|
|
946
|
+
if (!persona.tools?.length) continue;
|
|
947
|
+
const pruned = persona.tools.filter(id => manifestIds.has(id));
|
|
948
|
+
if (pruned.length !== persona.tools.length) {
|
|
949
|
+
const removed = persona.tools.length - pruned.length;
|
|
950
|
+
this.stateManager.persona_update(persona.id, { tools: pruned });
|
|
951
|
+
console.log(`[Processor] Pruned ${removed} stale tool reference(s) from persona "${persona.display_name}"`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
820
954
|
}
|
|
821
955
|
|
|
822
956
|
/**
|
|
@@ -1088,28 +1222,34 @@ const toolNextSteps = new Set([
|
|
|
1088
1222
|
LLMNextStep.HandleEiHeartbeat,
|
|
1089
1223
|
LLMNextStep.HandleToolContinuation,
|
|
1090
1224
|
LLMNextStep.HandleDedupCurate,
|
|
1225
|
+
LLMNextStep.HandleKnowledgeSynthesis,
|
|
1091
1226
|
]);
|
|
1092
1227
|
const toolPersonaId =
|
|
1093
1228
|
personaId ??
|
|
1094
1229
|
(request.next_step === LLMNextStep.HandleEiHeartbeat ? "ei" : undefined);
|
|
1095
1230
|
|
|
1096
|
-
// Dedup operates on Human data, not persona data
|
|
1231
|
+
// Dedup operates on Human data, not persona data — provide find_memory from SYSTEM_TOOLS directly.
|
|
1097
1232
|
// Also covers HandleToolContinuation originating from a dedup request: the
|
|
1098
1233
|
// continuation rebuilds tool lists from scratch and has no personaId, so without
|
|
1099
|
-
// this check Opus loses
|
|
1234
|
+
// this check Opus loses find_memory access after round 1.
|
|
1100
1235
|
const isDedupRequest =
|
|
1101
1236
|
request.next_step === LLMNextStep.HandleDedupCurate ||
|
|
1102
1237
|
(request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
1103
1238
|
request.data.originalNextStep === LLMNextStep.HandleDedupCurate);
|
|
1104
1239
|
|
|
1240
|
+
const isSynthesisRequest = request.next_step === LLMNextStep.HandleKnowledgeSynthesis ||
|
|
1241
|
+
(request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
1242
|
+
request.data.originalNextStep === LLMNextStep.HandleKnowledgeSynthesis);
|
|
1243
|
+
|
|
1105
1244
|
let tools: ToolDefinition[] = [];
|
|
1106
1245
|
if (isDedupRequest) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1246
|
+
tools = SYSTEM_TOOLS.filter(t => t.name === "find_memory");
|
|
1247
|
+
} else if (isSynthesisRequest) {
|
|
1248
|
+
tools = SYSTEM_TOOLS.filter(t =>
|
|
1249
|
+
t.name === "find_memory" || t.name === "fetch_memory" || t.name === "fetch_message"
|
|
1250
|
+
);
|
|
1111
1251
|
} else if (toolNextSteps.has(request.next_step) && toolPersonaId) {
|
|
1112
|
-
tools = this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI);
|
|
1252
|
+
tools = [...SYSTEM_TOOLS, ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
|
|
1113
1253
|
}
|
|
1114
1254
|
|
|
1115
1255
|
// Auto-inject each handler's dedicated submit tool — infrastructure, not user-visible.
|
|
@@ -1764,6 +1904,17 @@ const toolNextSteps = new Set([
|
|
|
1764
1904
|
this.interface.onHumanUpdated?.();
|
|
1765
1905
|
}
|
|
1766
1906
|
}
|
|
1907
|
+
|
|
1908
|
+
const isSynthesisCompletion =
|
|
1909
|
+
response.request.next_step === LLMNextStep.HandleKnowledgeSynthesis ||
|
|
1910
|
+
(response.request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
1911
|
+
response.request.data.originalNextStep === LLMNextStep.HandleKnowledgeSynthesis);
|
|
1912
|
+
if (isSynthesisCompletion) {
|
|
1913
|
+
const slug = response.request.data.slug as string;
|
|
1914
|
+
const hasContent = slug && this.stateManager.messages_get("emmet")
|
|
1915
|
+
.some(m => m.source_tag === `generate:document:${slug}`);
|
|
1916
|
+
if (hasContent) this.interface.onDocumentGenerated?.(slug);
|
|
1917
|
+
}
|
|
1767
1918
|
} catch (err) {
|
|
1768
1919
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1769
1920
|
const result = this.stateManager.queue_fail(response.request.id, errorMsg);
|
|
@@ -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,
|
|
@@ -51,6 +51,15 @@ export async function getQueueStatus(sm: StateManager): Promise<QueueStatus> {
|
|
|
51
51
|
}
|
|
52
52
|
const extracting_documents = extractingSet.size > 0 ? Array.from(extractingSet) : undefined;
|
|
53
53
|
|
|
54
|
+
const generatingSet: string[] = [];
|
|
55
|
+
for (const item of activeItems) {
|
|
56
|
+
if (item.next_step === LLMNextStep.HandleKnowledgeSynthesis) {
|
|
57
|
+
const slug = (item.data as { slug?: string }).slug;
|
|
58
|
+
if (slug) generatingSet.push(slug);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const generating_documents = generatingSet.length > 0 ? generatingSet : undefined;
|
|
62
|
+
|
|
54
63
|
return {
|
|
55
64
|
state: sm.queue_isPaused()
|
|
56
65
|
? "paused"
|
|
@@ -62,6 +71,7 @@ export async function getQueueStatus(sm: StateManager): Promise<QueueStatus> {
|
|
|
62
71
|
embedding_warning: sm.embedding_getWarning() || undefined,
|
|
63
72
|
pending_documents,
|
|
64
73
|
extracting_documents,
|
|
74
|
+
generating_documents,
|
|
65
75
|
};
|
|
66
76
|
}
|
|
67
77
|
|
|
@@ -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`);
|
|
@@ -71,6 +71,7 @@ export class StateManager {
|
|
|
71
71
|
this.migrateProviderModel();
|
|
72
72
|
this.migrateThemes();
|
|
73
73
|
this.migrateFfaParentIds();
|
|
74
|
+
this.migrateDocumentSettings();
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/**
|
|
@@ -586,6 +587,40 @@ export class StateManager {
|
|
|
586
587
|
}
|
|
587
588
|
}
|
|
588
589
|
|
|
590
|
+
private migrateDocumentSettings(): void {
|
|
591
|
+
const human = this.humanState.get();
|
|
592
|
+
const doc = human.settings?.document;
|
|
593
|
+
if (!doc) return;
|
|
594
|
+
|
|
595
|
+
let migrated = false;
|
|
596
|
+
|
|
597
|
+
const existing = doc.processed_documents ?? {};
|
|
598
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
599
|
+
if (typeof value === "string") {
|
|
600
|
+
(existing as Record<string, unknown>)[key] = { created_at: value, type: "imported" };
|
|
601
|
+
migrated = true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const legacy = (doc as Record<string, unknown>).generated_documents as
|
|
606
|
+
| Record<string, { subject: string; created_at: string }>
|
|
607
|
+
| undefined;
|
|
608
|
+
if (legacy) {
|
|
609
|
+
for (const [slug, record] of Object.entries(legacy)) {
|
|
610
|
+
existing[slug] = { created_at: record.created_at, type: "generated", subject: record.subject };
|
|
611
|
+
}
|
|
612
|
+
delete (doc as Record<string, unknown>).generated_documents;
|
|
613
|
+
migrated = true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (migrated) {
|
|
617
|
+
doc.processed_documents = existing as import("./types/entities.js").DocumentSettings["processed_documents"];
|
|
618
|
+
this.humanState.set(human);
|
|
619
|
+
this.scheduleSave();
|
|
620
|
+
console.log("[StateManager] Migrated document settings to unified processed_documents schema");
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
589
624
|
getHuman(): HumanEntity {
|
|
590
625
|
return this.humanState.get();
|
|
591
626
|
}
|