ei-tui 1.1.0 → 1.3.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 +16 -0
- package/package.json +2 -23
- package/src/cli/README.md +12 -2
- package/src/cli/mcp.ts +12 -4
- package/src/cli/retrieval.ts +162 -0
- package/src/cli.ts +7 -1
- package/src/core/handlers/dedup.ts +4 -15
- package/src/core/handlers/document-segmentation.ts +5 -7
- package/src/core/handlers/heartbeat.ts +5 -10
- package/src/core/handlers/human-matching.ts +8 -0
- package/src/core/handlers/index.ts +2 -0
- package/src/core/handlers/knowledge-synthesis.ts +48 -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/heartbeat-manager.ts +10 -0
- package/src/core/llm-client.ts +13 -3
- package/src/core/message-manager.ts +2 -4
- package/src/core/orchestrators/ceremony.ts +45 -22
- package/src/core/orchestrators/human-extraction.ts +10 -1
- package/src/core/processor.ts +275 -7
- package/src/core/queue-manager.ts +10 -0
- package/src/core/state-manager.ts +35 -0
- package/src/core/tools/builtin/fetch-memory.ts +6 -6
- package/src/core/tools/builtin/fetch-message.ts +27 -1
- package/src/core/tools/builtin/find-memory.ts +11 -3
- package/src/core/tools/index.ts +3 -3
- 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/core/types/llm.ts +0 -9
- package/src/core/utils/message-id.ts +114 -0
- package/src/integrations/claude-code/importer.ts +12 -5
- package/src/integrations/cursor/importer.ts +12 -5
- package/src/integrations/document/importer.ts +1 -1
- package/src/integrations/document/unsource.ts +11 -14
- package/src/integrations/opencode/importer.ts +19 -6
- package/src/integrations/opencode/json-reader.ts +65 -0
- package/src/integrations/opencode/sqlite-reader.ts +33 -0
- package/src/integrations/opencode/types.ts +8 -0
- package/src/integrations/persona-history/importer.ts +9 -0
- package/src/prompts/ceremony/people-rewrite.ts +2 -2
- package/src/prompts/ceremony/topic-rewrite.ts +2 -2
- package/src/prompts/heartbeat/check.ts +5 -2
- package/src/prompts/heartbeat/ei.ts +7 -0
- package/src/prompts/heartbeat/types.ts +5 -0
- package/src/prompts/index.ts +3 -0
- package/src/prompts/response/sections.ts +30 -16
- package/src/prompts/room/sections.ts +28 -6
- package/src/prompts/synthesis/index.ts +101 -0
- package/src/prompts/synthesis/types.ts +26 -0
- package/src/prompts/trait-utils.ts +33 -0
- package/tui/README.md +2 -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/help-content.ts +11 -0
- 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/processor.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.
|
|
|
44
44
|
import { EMMETT_PERSONA_DEFINITION } from "../templates/emmett.js";
|
|
45
45
|
import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueReflectionDrain, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction, queueTargetedPersonUpdate, queueTargetedTopicUpdate } from "./orchestrators/index.js";
|
|
46
46
|
import { finishDocumentBatch } from "./handlers/document-segmentation.js";
|
|
47
|
+
import { buildSynthesisPrompt } from "../prompts/synthesis/index.js";
|
|
47
48
|
import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
|
|
48
49
|
import { DEFAULT_SEED_TRAITS } from "./constants/seed-traits.js";
|
|
49
50
|
|
|
@@ -138,6 +139,9 @@ import {
|
|
|
138
139
|
import type { RoomCreationInput, RoomEntity, RoomMessage, RoomSummary } from "./types.js";
|
|
139
140
|
import { previewUnsource as _previewUnsource } from "../integrations/document/unsource.js";
|
|
140
141
|
import type { UnsourcePreview, UnsourceResult } from "../integrations/document/unsource.js";
|
|
142
|
+
import { isQualifiedMessageId, qualifyEiMessage, qualifyOpenCodeMessage } from "./utils/message-id.js";
|
|
143
|
+
|
|
144
|
+
import type { IOpenCodeReader } from "../integrations/opencode/types.js";
|
|
141
145
|
|
|
142
146
|
const DEFAULT_LOOP_INTERVAL_MS = 100;
|
|
143
147
|
const DEFAULT_OPENCODE_POLLING_MS = 60000;
|
|
@@ -241,18 +245,30 @@ export class Processor {
|
|
|
241
245
|
this.bootstrapTools();
|
|
242
246
|
this.seedBuiltinFacts();
|
|
243
247
|
this.migrateLearnedOn();
|
|
248
|
+
await this.migrateMessageIds();
|
|
244
249
|
this.seedSettings();
|
|
245
250
|
registerFindMemoryExecutor(createFindMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this), this.stateManager.getHuman.bind(this.stateManager)));
|
|
246
251
|
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
|
-
));
|
|
254
252
|
if (this.isTUI) {
|
|
255
253
|
await registerFileReadExecutor();
|
|
254
|
+
const { createOpenCodeReader } = await import("../integrations/opencode/reader-factory.js");
|
|
255
|
+
const openCodeReader = await createOpenCodeReader().catch(() => null);
|
|
256
|
+
registerFetchMessageExecutor(createFetchMessageExecutor(
|
|
257
|
+
this.stateManager.persona_getAll.bind(this.stateManager),
|
|
258
|
+
this.stateManager.messages_get.bind(this.stateManager),
|
|
259
|
+
this.stateManager.getRoomList.bind(this.stateManager),
|
|
260
|
+
this.stateManager.getRoomMessages.bind(this.stateManager),
|
|
261
|
+
(roomId: string) => this.stateManager.getRoom(roomId)?.display_name ?? null,
|
|
262
|
+
openCodeReader ? (id, before, after) => openCodeReader.getMessageById(id, before, after) : undefined
|
|
263
|
+
));
|
|
264
|
+
} else {
|
|
265
|
+
registerFetchMessageExecutor(createFetchMessageExecutor(
|
|
266
|
+
this.stateManager.persona_getAll.bind(this.stateManager),
|
|
267
|
+
this.stateManager.messages_get.bind(this.stateManager),
|
|
268
|
+
this.stateManager.getRoomList.bind(this.stateManager),
|
|
269
|
+
this.stateManager.getRoomMessages.bind(this.stateManager),
|
|
270
|
+
(roomId: string) => this.stateManager.getRoom(roomId)?.display_name ?? null
|
|
271
|
+
));
|
|
256
272
|
}
|
|
257
273
|
this.running = true;
|
|
258
274
|
console.log(`[Processor ${this.instanceId}] initialized, starting loop`);
|
|
@@ -335,6 +351,140 @@ export class Processor {
|
|
|
335
351
|
return executeUnsource(preview, this.stateManager);
|
|
336
352
|
}
|
|
337
353
|
|
|
354
|
+
async generateDocument(subject: string): Promise<{ slug: string }> {
|
|
355
|
+
this.bootstrapEmmett();
|
|
356
|
+
const slugBase = subject
|
|
357
|
+
.toLowerCase()
|
|
358
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
359
|
+
.slice(0, 40);
|
|
360
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
361
|
+
const slug = `${slugBase}_${timestamp}`;
|
|
362
|
+
|
|
363
|
+
const primary = await this.searchHumanData(subject, { limit: 20 });
|
|
364
|
+
if (
|
|
365
|
+
primary.facts.length === 0 &&
|
|
366
|
+
primary.topics.length === 0 &&
|
|
367
|
+
primary.people.length === 0 &&
|
|
368
|
+
primary.quotes.length === 0
|
|
369
|
+
) {
|
|
370
|
+
throw new Error(`No knowledge found about '${subject}'`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const seenQuoteIds = new Set<string>();
|
|
374
|
+
const seenItemIds = new Set<string>(
|
|
375
|
+
[...primary.topics, ...primary.people, ...primary.facts].map(i => i.id)
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const MAX_QUOTES_PER_ENTITY = 3;
|
|
379
|
+
|
|
380
|
+
const enrichTopic = (topic: import("../prompts/synthesis/types.js").EnrichedTopic["topic"]) => {
|
|
381
|
+
const linked = this.stateManager.human_quote_getForDataItem(topic.id)
|
|
382
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
383
|
+
.slice(0, MAX_QUOTES_PER_ENTITY);
|
|
384
|
+
linked.forEach(q => seenQuoteIds.add(q.id));
|
|
385
|
+
return { topic, quotes: linked };
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const enrichPerson = (person: import("../prompts/synthesis/types.js").EnrichedPerson["person"]) => {
|
|
389
|
+
const linked = this.stateManager.human_quote_getForDataItem(person.id)
|
|
390
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
391
|
+
.slice(0, MAX_QUOTES_PER_ENTITY);
|
|
392
|
+
linked.forEach(q => seenQuoteIds.add(q.id));
|
|
393
|
+
return { person, quotes: linked };
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const enrichedTopics = primary.topics.map(enrichTopic);
|
|
397
|
+
const enrichedPeople = primary.people.map(enrichPerson);
|
|
398
|
+
|
|
399
|
+
const human = this.stateManager.getHuman();
|
|
400
|
+
const allItems = [...human.facts, ...human.topics, ...human.people];
|
|
401
|
+
|
|
402
|
+
const MAX_SECONDARY_ENTITIES = 10;
|
|
403
|
+
|
|
404
|
+
const secondaryTopics: typeof enrichedTopics = [];
|
|
405
|
+
const secondaryPeople: typeof enrichedPeople = [];
|
|
406
|
+
const secondaryFacts: typeof primary.facts = [];
|
|
407
|
+
|
|
408
|
+
outer: for (const quote of [...enrichedTopics.flatMap(e => e.quotes), ...enrichedPeople.flatMap(e => e.quotes)]) {
|
|
409
|
+
for (const itemId of quote.data_item_ids) {
|
|
410
|
+
if (secondaryTopics.length + secondaryPeople.length + secondaryFacts.length >= MAX_SECONDARY_ENTITIES) break outer;
|
|
411
|
+
if (seenItemIds.has(itemId)) continue;
|
|
412
|
+
seenItemIds.add(itemId);
|
|
413
|
+
const item = allItems.find(i => i.id === itemId);
|
|
414
|
+
if (!item) continue;
|
|
415
|
+
if (human.topics.find(t => t.id === itemId)) {
|
|
416
|
+
secondaryTopics.push(enrichTopic(item as typeof primary.topics[0]));
|
|
417
|
+
} else if (human.people.find(p => p.id === itemId)) {
|
|
418
|
+
secondaryPeople.push(enrichPerson(item as typeof primary.people[0]));
|
|
419
|
+
} else if (human.facts.find(f => f.id === itemId)) {
|
|
420
|
+
secondaryFacts.push(item as typeof primary.facts[0]);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const standaloneQuotes = primary.quotes.filter(q => !seenQuoteIds.has(q.id));
|
|
426
|
+
|
|
427
|
+
const allLoadedFacts = [...primary.facts, ...secondaryFacts];
|
|
428
|
+
const allLoadedTopics = [...enrichedTopics, ...secondaryTopics];
|
|
429
|
+
const allLoadedPeople = [...enrichedPeople, ...secondaryPeople];
|
|
430
|
+
|
|
431
|
+
const loadedEntityNames = new Map<string, string>();
|
|
432
|
+
for (const f of allLoadedFacts) loadedEntityNames.set(f.id, f.name);
|
|
433
|
+
for (const { topic } of allLoadedTopics) loadedEntityNames.set(topic.id, topic.name);
|
|
434
|
+
for (const { person } of allLoadedPeople) loadedEntityNames.set(person.id, person.name);
|
|
435
|
+
|
|
436
|
+
const prompt = buildSynthesisPrompt({
|
|
437
|
+
subject,
|
|
438
|
+
facts: allLoadedFacts,
|
|
439
|
+
topics: allLoadedTopics,
|
|
440
|
+
people: allLoadedPeople,
|
|
441
|
+
standaloneQuotes,
|
|
442
|
+
loadedEntityNames,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const model = this.stateManager.getHuman().settings?.rewrite_model
|
|
446
|
+
?? this.stateManager.getHuman().settings?.default_model;
|
|
447
|
+
|
|
448
|
+
this.stateManager.queue_enqueue({
|
|
449
|
+
type: LLMRequestType.Raw,
|
|
450
|
+
priority: LLMPriority.Normal,
|
|
451
|
+
system: prompt.system,
|
|
452
|
+
user: prompt.user,
|
|
453
|
+
next_step: LLMNextStep.HandleKnowledgeSynthesis,
|
|
454
|
+
model,
|
|
455
|
+
data: { slug, subject },
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return { slug };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
checkGenerationModel(): { model: string; isRewriteModel: boolean } {
|
|
462
|
+
const settings = this.stateManager.getHuman().settings;
|
|
463
|
+
if (settings?.rewrite_model) {
|
|
464
|
+
return { model: settings.rewrite_model, isRewriteModel: true };
|
|
465
|
+
}
|
|
466
|
+
return { model: settings?.default_model ?? "unknown", isRewriteModel: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async getGeneratedDocumentContent(slug: string): Promise<string | null> {
|
|
470
|
+
const messages = this.stateManager.messages_get("emmet");
|
|
471
|
+
const target = `generate:document:${slug}`;
|
|
472
|
+
const message = messages.find(m => m.id.startsWith(`${target}:`));
|
|
473
|
+
return message?.content ?? null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async reRunDocument(slug: string): Promise<{ slug: string }> {
|
|
477
|
+
const docs = this.stateManager.getHuman().settings?.document?.processed_documents ?? {};
|
|
478
|
+
const entry = docs[slug];
|
|
479
|
+
if (!entry || entry.type !== "generated" || !entry.subject) {
|
|
480
|
+
throw new Error(`No generated document found for slug "${slug}"`);
|
|
481
|
+
}
|
|
482
|
+
const subject = entry.subject;
|
|
483
|
+
const preview = this.getUnsourcePreview(`generate:document:${slug}`);
|
|
484
|
+
await this.executeUnsource(preview);
|
|
485
|
+
return this.generateDocument(subject);
|
|
486
|
+
}
|
|
487
|
+
|
|
338
488
|
/**
|
|
339
489
|
* Seed built-in tool providers and tools if they don't exist yet.
|
|
340
490
|
* Called on every startup (after state load/restore) — safe to call repeatedly.
|
|
@@ -874,6 +1024,104 @@ export class Processor {
|
|
|
874
1024
|
}
|
|
875
1025
|
}
|
|
876
1026
|
|
|
1027
|
+
private async migrateMessageIds(): Promise<void> {
|
|
1028
|
+
try {
|
|
1029
|
+
let msgRewrites = 0;
|
|
1030
|
+
let quoteRewrites = 0;
|
|
1031
|
+
|
|
1032
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1033
|
+
|
|
1034
|
+
const personas = this.stateManager.persona_getAll();
|
|
1035
|
+
for (const persona of personas) {
|
|
1036
|
+
for (const msg of this.stateManager.messages_get(persona.id)) {
|
|
1037
|
+
if (!msg.external && UUID_PATTERN.test(msg.id)) {
|
|
1038
|
+
this.stateManager.messages_update(persona.id, msg.id, { id: qualifyEiMessage(msg.id) });
|
|
1039
|
+
msgRewrites++;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const rooms = this.stateManager.getRoomList();
|
|
1045
|
+
for (const room of rooms) {
|
|
1046
|
+
for (const msg of this.stateManager.getRoomMessages(room.id).slice()) {
|
|
1047
|
+
if (UUID_PATTERN.test(msg.id)) {
|
|
1048
|
+
this.stateManager.updateRoomMessage(room.id, msg.id, { id: qualifyEiMessage(msg.id) });
|
|
1049
|
+
msgRewrites++;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const human = this.stateManager.getHuman();
|
|
1055
|
+
const quotes = human.quotes ?? [];
|
|
1056
|
+
|
|
1057
|
+
const eiUuidMap = new Map<string, string>();
|
|
1058
|
+
for (const persona of personas) {
|
|
1059
|
+
for (const msg of this.stateManager.messages_get(persona.id)) {
|
|
1060
|
+
if (msg.id.startsWith("ei:")) eiUuidMap.set(msg.id.slice(3), msg.id);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
for (const room of rooms) {
|
|
1064
|
+
for (const msg of this.stateManager.getRoomMessages(room.id)) {
|
|
1065
|
+
if (msg.id.startsWith("ei:")) eiUuidMap.set(msg.id.slice(3), msg.id);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const MSG_PATTERN = /^msg_[a-zA-Z0-9]+$/;
|
|
1070
|
+
|
|
1071
|
+
let openCodeReader: IOpenCodeReader | null = null;
|
|
1072
|
+
if (this.isTUI) {
|
|
1073
|
+
const { createOpenCodeReader } = await import("../integrations/opencode/reader-factory.js");
|
|
1074
|
+
openCodeReader = await createOpenCodeReader().catch(() => null);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const updatedQuotes: typeof quotes = [];
|
|
1078
|
+
for (const quote of quotes) {
|
|
1079
|
+
const mid = quote.message_id;
|
|
1080
|
+
if (!mid || isQualifiedMessageId(mid)) {
|
|
1081
|
+
updatedQuotes.push(quote);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (MSG_PATTERN.test(mid)) {
|
|
1086
|
+
if (openCodeReader) {
|
|
1087
|
+
const ocWindow = await openCodeReader.getMessageById(mid).catch(() => null);
|
|
1088
|
+
if (ocWindow) {
|
|
1089
|
+
const { getMachineId } = await import("../integrations/machine-id.js");
|
|
1090
|
+
updatedQuotes.push({ ...quote, message_id: qualifyOpenCodeMessage(getMachineId(), ocWindow.session.id, mid) });
|
|
1091
|
+
quoteRewrites++;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
updatedQuotes.push(quote);
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (UUID_PATTERN.test(mid)) {
|
|
1100
|
+
const fqId = eiUuidMap.get(mid);
|
|
1101
|
+
if (fqId) {
|
|
1102
|
+
updatedQuotes.push({ ...quote, message_id: fqId });
|
|
1103
|
+
quoteRewrites++;
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
updatedQuotes.push(quote);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
updatedQuotes.push(quote);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (quoteRewrites > 0) {
|
|
1114
|
+
this.stateManager.setHuman({ ...human, quotes: updatedQuotes });
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (msgRewrites > 0 || quoteRewrites > 0) {
|
|
1118
|
+
console.log(`[Processor] migrateMessageIds: rewrote ${msgRewrites} message IDs, ${quoteRewrites} quote message_ids`);
|
|
1119
|
+
}
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
console.error("[Processor] migrateMessageIds failed, continuing:", err);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
877
1125
|
private seedSettings(): void {
|
|
878
1126
|
const human = this.stateManager.getHuman();
|
|
879
1127
|
let modified = false;
|
|
@@ -1087,6 +1335,7 @@ const toolNextSteps = new Set([
|
|
|
1087
1335
|
LLMNextStep.HandleEiHeartbeat,
|
|
1088
1336
|
LLMNextStep.HandleToolContinuation,
|
|
1089
1337
|
LLMNextStep.HandleDedupCurate,
|
|
1338
|
+
LLMNextStep.HandleKnowledgeSynthesis,
|
|
1090
1339
|
]);
|
|
1091
1340
|
const toolPersonaId =
|
|
1092
1341
|
personaId ??
|
|
@@ -1101,9 +1350,17 @@ const toolNextSteps = new Set([
|
|
|
1101
1350
|
(request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
1102
1351
|
request.data.originalNextStep === LLMNextStep.HandleDedupCurate);
|
|
1103
1352
|
|
|
1353
|
+
const isSynthesisRequest = request.next_step === LLMNextStep.HandleKnowledgeSynthesis ||
|
|
1354
|
+
(request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
1355
|
+
request.data.originalNextStep === LLMNextStep.HandleKnowledgeSynthesis);
|
|
1356
|
+
|
|
1104
1357
|
let tools: ToolDefinition[] = [];
|
|
1105
1358
|
if (isDedupRequest) {
|
|
1106
1359
|
tools = SYSTEM_TOOLS.filter(t => t.name === "find_memory");
|
|
1360
|
+
} else if (isSynthesisRequest) {
|
|
1361
|
+
tools = SYSTEM_TOOLS.filter(t =>
|
|
1362
|
+
t.name === "find_memory" || t.name === "fetch_memory" || t.name === "fetch_message"
|
|
1363
|
+
);
|
|
1107
1364
|
} else if (toolNextSteps.has(request.next_step) && toolPersonaId) {
|
|
1108
1365
|
tools = [...SYSTEM_TOOLS, ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
|
|
1109
1366
|
}
|
|
@@ -1760,6 +2017,17 @@ const toolNextSteps = new Set([
|
|
|
1760
2017
|
this.interface.onHumanUpdated?.();
|
|
1761
2018
|
}
|
|
1762
2019
|
}
|
|
2020
|
+
|
|
2021
|
+
const isSynthesisCompletion =
|
|
2022
|
+
response.request.next_step === LLMNextStep.HandleKnowledgeSynthesis ||
|
|
2023
|
+
(response.request.next_step === LLMNextStep.HandleToolContinuation &&
|
|
2024
|
+
response.request.data.originalNextStep === LLMNextStep.HandleKnowledgeSynthesis);
|
|
2025
|
+
if (isSynthesisCompletion) {
|
|
2026
|
+
const slug = response.request.data.slug as string;
|
|
2027
|
+
const hasContent = slug && this.stateManager.messages_get("emmet")
|
|
2028
|
+
.some(m => m.id.startsWith(`generate:document:${slug}:`));
|
|
2029
|
+
if (hasContent) this.interface.onDocumentGenerated?.(slug);
|
|
2030
|
+
}
|
|
1763
2031
|
} catch (err) {
|
|
1764
2032
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1765
2033
|
const result = this.stateManager.queue_fail(response.request.id, errorMsg);
|
|
@@ -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
|
|
|
@@ -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
|
}
|
|
@@ -4,20 +4,20 @@ import type { Fact, Topic, Person, Quote, HumanEntity } from "../../types.js";
|
|
|
4
4
|
type GetHuman = () => HumanEntity;
|
|
5
5
|
|
|
6
6
|
function cleanFact(f: Fact): Record<string, unknown> {
|
|
7
|
-
const { embedding,
|
|
8
|
-
void embedding; void
|
|
7
|
+
const { embedding, persona_groups, ...rest } = f;
|
|
8
|
+
void embedding; void persona_groups;
|
|
9
9
|
return rest;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function cleanTopic(t: Topic): Record<string, unknown> {
|
|
13
|
-
const { embedding,
|
|
14
|
-
void embedding; void
|
|
13
|
+
const { embedding, rewrite_length_floor, persona_groups, last_ei_asked, ...rest } = t;
|
|
14
|
+
void embedding; void rewrite_length_floor; void persona_groups; void last_ei_asked;
|
|
15
15
|
return rest;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function cleanPerson(p: Person): Record<string, unknown> {
|
|
19
|
-
const { embedding,
|
|
20
|
-
void embedding; void
|
|
19
|
+
const { embedding, rewrite_length_floor, persona_groups, last_ei_asked, ...rest } = p;
|
|
20
|
+
void embedding; void rewrite_length_floor; void persona_groups; void last_ei_asked;
|
|
21
21
|
return rest;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -2,6 +2,7 @@ import type { ToolExecutor } from "../types.js";
|
|
|
2
2
|
import type { Message } from "../../types.js";
|
|
3
3
|
import type { RoomMessage, RoomSummary } from "../../types/rooms.js";
|
|
4
4
|
import type { PersonaEntity } from "../../types/entities.js";
|
|
5
|
+
import type { OpenCodeMessageWindow } from "../../../integrations/opencode/types.js";
|
|
5
6
|
|
|
6
7
|
interface CleanMessage {
|
|
7
8
|
id: string;
|
|
@@ -17,6 +18,9 @@ type GetPersonaMessages = (personaId: string) => Message[];
|
|
|
17
18
|
type GetRoomList = () => RoomSummary[];
|
|
18
19
|
type GetRoomMessages = (roomId: string) => RoomMessage[];
|
|
19
20
|
type GetRoomDisplayName = (roomId: string) => string | null;
|
|
21
|
+
type GetOpenCodeMessage = (id: string, before: number, after: number) => Promise<OpenCodeMessageWindow | null>;
|
|
22
|
+
|
|
23
|
+
const OPENCODE_MESSAGE_ID = /^msg_[a-zA-Z0-9]+$/;
|
|
20
24
|
|
|
21
25
|
function stripMessage(m: Message): CleanMessage {
|
|
22
26
|
return {
|
|
@@ -45,7 +49,8 @@ export function createFetchMessageExecutor(
|
|
|
45
49
|
getPersonaMessages: GetPersonaMessages,
|
|
46
50
|
getRoomList: GetRoomList,
|
|
47
51
|
getRoomMessages: GetRoomMessages,
|
|
48
|
-
getRoomDisplayName: GetRoomDisplayName
|
|
52
|
+
getRoomDisplayName: GetRoomDisplayName,
|
|
53
|
+
getOpenCodeMessage?: GetOpenCodeMessage
|
|
49
54
|
): ToolExecutor {
|
|
50
55
|
return {
|
|
51
56
|
name: "fetch_message",
|
|
@@ -62,6 +67,27 @@ export function createFetchMessageExecutor(
|
|
|
62
67
|
return JSON.stringify({ error: "Missing required argument: id" });
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
if (OPENCODE_MESSAGE_ID.test(id)) {
|
|
71
|
+
if (!getOpenCodeMessage) {
|
|
72
|
+
return JSON.stringify({ error: "OpenCode message lookup not available in this runtime", id });
|
|
73
|
+
}
|
|
74
|
+
const window = await getOpenCodeMessage(id, before, after);
|
|
75
|
+
if (!window) {
|
|
76
|
+
return JSON.stringify({
|
|
77
|
+
error: "OpenCode message not found on this machine. It may exist on another device.",
|
|
78
|
+
id,
|
|
79
|
+
hint: "Check the linked topic's sources for the originating machine and session.",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return JSON.stringify({
|
|
83
|
+
message: { id: window.message.id, role: window.message.role, content: window.message.content, timestamp: window.message.timestamp, agent: window.message.agent },
|
|
84
|
+
before: window.before.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp, agent: m.agent })),
|
|
85
|
+
after: window.after.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp, agent: m.agent })),
|
|
86
|
+
session: { id: window.session.id, title: window.session.title, directory: window.session.directory },
|
|
87
|
+
source: "opencode",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
const personas = getAllPersonas();
|
|
66
92
|
|
|
67
93
|
// TODO: add persona access gate when calling context is available —
|
|
@@ -15,6 +15,14 @@ type GetPersonaList = () => Promise<PersonaSummary[]>;
|
|
|
15
15
|
|
|
16
16
|
type GetHuman = () => HumanEntity;
|
|
17
17
|
|
|
18
|
+
function formatSentiment(s: number): string {
|
|
19
|
+
const pct = Math.round(Math.abs(s) * 100);
|
|
20
|
+
const direction = s > 0.2 ? "positive" : s < -0.2 ? "negative" : "neutral";
|
|
21
|
+
if (direction === "neutral") return "neutral";
|
|
22
|
+
const intensity = pct >= 80 ? "strongly " : pct >= 50 ? "" : "slightly ";
|
|
23
|
+
return `${pct}% ${intensity}${direction}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
export function createFindMemoryExecutor(searchHumanData: SearchHumanData, getPersonaList?: GetPersonaList, getHuman?: GetHuman): ToolExecutor {
|
|
19
27
|
return {
|
|
20
28
|
name: "find_memory",
|
|
@@ -68,8 +76,8 @@ export function createFindMemoryExecutor(searchHumanData: SearchHumanData, getPe
|
|
|
68
76
|
|
|
69
77
|
const output: Record<string, unknown[]> = {};
|
|
70
78
|
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 ?? [] }));
|
|
79
|
+
if (results.topics.length > 0) output.topics = results.topics.map(t => ({ id: t.id, name: t.name, description: t.description, sentiment: formatSentiment(t.sentiment) }));
|
|
80
|
+
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 ?? [], sentiment: formatSentiment(p.sentiment) }));
|
|
73
81
|
|
|
74
82
|
if (results.quotes.length > 0) {
|
|
75
83
|
const human = getHuman ? getHuman() : null;
|
|
@@ -85,7 +93,7 @@ export function createFindMemoryExecutor(searchHumanData: SearchHumanData, getPe
|
|
|
85
93
|
if (person) { linked_items.push({ id: person.id, name: person.name, type: "person" }); }
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
|
-
return { id: q.id, text: q.text, speaker: q.speaker, linked_items };
|
|
96
|
+
return { id: q.id, text: q.text, speaker: q.speaker, message_id: q.message_id, linked_items };
|
|
89
97
|
});
|
|
90
98
|
}
|
|
91
99
|
|
package/src/core/tools/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const SYSTEM_TOOLS: ToolDefinition[] = [
|
|
|
29
29
|
provider_id: "ei",
|
|
30
30
|
name: "find_memory",
|
|
31
31
|
display_name: "Find Memory",
|
|
32
|
-
description: "Semantic search of your personal memory — facts, topics, people, and quotes learned across ALL conversations over time, not just this one. Use when the human references something from the past, mentions a person, or asks about a topic you might have learned about. Supports optional filters: types (array of 'facts', 'topics', 'people', 'quotes'), limit (1-20, default 10), recent (true = sort by recency), persona (filter to what a specific persona has learned — use display name).",
|
|
32
|
+
description: "Semantic search of your personal memory — facts, topics, people, and quotes learned across ALL conversations over time, not just this one. Use when the human references something from the past, mentions a person, or asks about a topic you might have learned about. People and topic results include a sentiment field (e.g. '72% positive', 'neutral', '45% slightly negative') indicating how the human generally feels about that person or subject. Supports optional filters: types (array of 'facts', 'topics', 'people', 'quotes'), limit (1-20, default 10), recent (true = sort by recency), persona (filter to what a specific persona has learned — use display name).",
|
|
33
33
|
input_schema: {
|
|
34
34
|
type: "object",
|
|
35
35
|
properties: {
|
|
@@ -52,7 +52,7 @@ export const SYSTEM_TOOLS: ToolDefinition[] = [
|
|
|
52
52
|
provider_id: "ei",
|
|
53
53
|
name: "fetch_memory",
|
|
54
54
|
display_name: "Fetch Memory",
|
|
55
|
-
description: "Retrieve the full record for a specific memory by its ID.
|
|
55
|
+
description: "Retrieve the full record for a specific memory by its ID. For most conversational use, find_memory results are sufficient. Use fetch_memory when you need provenance details (which sessions or documents the memory came from) or the raw sentiment score. Returns the complete Fact, Topic, Person, or Quote record including all fields.",
|
|
56
56
|
input_schema: {
|
|
57
57
|
type: "object",
|
|
58
58
|
properties: {
|
|
@@ -64,7 +64,7 @@ export const SYSTEM_TOOLS: ToolDefinition[] = [
|
|
|
64
64
|
builtin: true,
|
|
65
65
|
enabled: true,
|
|
66
66
|
created_at: new Date(0).toISOString(),
|
|
67
|
-
max_calls_per_interaction:
|
|
67
|
+
max_calls_per_interaction: 10,
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
id: "builtin-fetch-message",
|
package/src/core/tools/types.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/** A single tool call the LLM wants to make (from the API response). */
|
|
7
7
|
export interface ToolCall {
|
|
8
8
|
id: string; // call_abc123 — must be echoed back in tool result message
|
|
9
|
-
name: string; // snake_case tool name ("web_search", "
|
|
9
|
+
name: string; // snake_case tool name ("web_search", "find_memory")
|
|
10
10
|
arguments: Record<string, unknown>; // Parsed from JSON string in the API response
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ export interface DataItemBase {
|
|
|
18
18
|
sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId"). Grow-only union.
|
|
19
19
|
persona_groups?: string[];
|
|
20
20
|
embedding?: number[];
|
|
21
|
-
|
|
21
|
+
rewrite_length_floor?: number; // Set after every rewrite scan: ceil(description.length * 1.1). Item is skipped by ceremony until description grows past this floor. Preserved across extraction upserts — only cleared when description exceeds it.
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface Fact extends DataItemBase {
|
|
@@ -20,9 +20,15 @@ export interface OpenCodeSettings {
|
|
|
20
20
|
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface DocumentRecord {
|
|
24
|
+
created_at: string;
|
|
25
|
+
type: "imported" | "generated";
|
|
26
|
+
subject?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export interface DocumentSettings {
|
|
24
30
|
extraction_model?: string;
|
|
25
|
-
processed_documents?: Record<string,
|
|
31
|
+
processed_documents?: Record<string, DocumentRecord>;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export interface CeremonyConfig {
|
package/src/core/types/enums.ts
CHANGED
|
@@ -53,6 +53,7 @@ export enum LLMNextStep {
|
|
|
53
53
|
HandleTopicValidate = "handleTopicValidate",
|
|
54
54
|
HandleReflectionCritic = "handleReflectionCritic",
|
|
55
55
|
HandleDocumentSegmentation = "handleDocumentSegmentation",
|
|
56
|
+
HandleKnowledgeSynthesis = "handleKnowledgeSynthesis",
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export enum ProviderType {
|
|
@@ -29,7 +29,7 @@ export interface ToolProvider {
|
|
|
29
29
|
export interface ToolDefinition {
|
|
30
30
|
id: string; // UUID
|
|
31
31
|
provider_id: string; // FK → ToolProvider.id (required)
|
|
32
|
-
name: string; // Snake_case machine name ("web_search", "
|
|
32
|
+
name: string; // Snake_case machine name ("web_search", "find_memory")
|
|
33
33
|
display_name: string; // Human label
|
|
34
34
|
description: string; // What the LLM reads to decide whether to call this tool
|
|
35
35
|
input_schema: Record<string, unknown>; // JSON Schema for parameters the LLM can pass
|
|
@@ -77,6 +77,7 @@ export interface QueueStatus {
|
|
|
77
77
|
embedding_warning?: boolean;
|
|
78
78
|
pending_documents?: Array<{ batchId: string; filename: string; count: number }>;
|
|
79
79
|
extracting_documents?: string[];
|
|
80
|
+
generating_documents?: string[];
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export interface EiError {
|
|
@@ -121,6 +122,7 @@ export interface Ei_Interface {
|
|
|
121
122
|
onRoomMessageAdded?: (roomId: string) => void;
|
|
122
123
|
onRoomMessageQueued?: (roomId: string) => void;
|
|
123
124
|
onRoomMessageProcessing?: (roomId: string) => void;
|
|
125
|
+
onDocumentGenerated?: (slug: string) => void;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// =============================================================================
|
package/src/core/types/llm.ts
CHANGED
|
@@ -27,15 +27,6 @@ export interface Message {
|
|
|
27
27
|
|
|
28
28
|
external?: boolean; // Set by integration importers (OpenCode, Cursor, Claude Code); invisible to LLM context
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* Integration source tag. Set ONLY on external: true messages by importers (document, Slack, etc.)
|
|
32
|
-
* to identify which external source this synthetic message came from.
|
|
33
|
-
* Format: "import:document:filename" | "slack:channelId" | etc.
|
|
34
|
-
* Enables quote provenance tracing: quote.message_id → message.source_tag → original source.
|
|
35
|
-
* Never set on conversational messages.
|
|
36
|
-
*/
|
|
37
|
-
source_tag?: string;
|
|
38
|
-
|
|
39
30
|
}
|
|
40
31
|
|
|
41
32
|
export interface ChatMessage {
|