ei-tui 0.1.3 → 0.1.5
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 +36 -35
- package/package.json +6 -2
- package/src/README.md +85 -1
- package/src/cli/README.md +30 -20
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +195 -172
- package/src/core/orchestrators/ceremony.ts +4 -4
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +245 -77
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +80 -23
- package/src/core/state-manager.ts +36 -10
- package/src/core/types.ts +23 -11
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +118 -691
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +65 -136
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/index.ts +2 -5
- package/src/prompts/message-utils.ts +42 -3
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +65 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +89 -4
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +118 -30
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +43 -3
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +81 -11
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- package/src/prompts/validation/types.ts +0 -22
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ContextStatus,
|
|
3
|
-
LLMNextStep,
|
|
1
|
+
import {
|
|
2
|
+
ContextStatus,
|
|
3
|
+
LLMNextStep,
|
|
4
4
|
ValidationLevel,
|
|
5
|
-
type LLMResponse,
|
|
6
|
-
type Message,
|
|
7
|
-
type Trait,
|
|
5
|
+
type LLMResponse,
|
|
6
|
+
type Message,
|
|
7
|
+
type Trait,
|
|
8
8
|
type Topic,
|
|
9
9
|
type PersonaTopic,
|
|
10
10
|
type Fact,
|
|
11
11
|
type Person,
|
|
12
12
|
type Quote,
|
|
13
13
|
type DataItemType,
|
|
14
|
+
type DataItemBase,
|
|
14
15
|
} from "../types.js";
|
|
15
16
|
import type { StateManager } from "../state-manager.js";
|
|
16
17
|
import type { HeartbeatCheckResult, EiHeartbeatResult } from "../../prompts/heartbeat/types.js";
|
|
@@ -22,7 +23,8 @@ import type {
|
|
|
22
23
|
PersonaTopicMatchResult,
|
|
23
24
|
PersonaTopicUpdateResult,
|
|
24
25
|
} from "../../prompts/persona/types.js";
|
|
25
|
-
import type {
|
|
26
|
+
import type { PersonaResponseResult } from "../../prompts/response/index.js";
|
|
27
|
+
|
|
26
28
|
import type {
|
|
27
29
|
PersonaExpireResult,
|
|
28
30
|
PersonaExploreResult,
|
|
@@ -50,9 +52,10 @@ import type {
|
|
|
50
52
|
ItemUpdateResult,
|
|
51
53
|
ExposureImpact,
|
|
52
54
|
} from "../../prompts/human/types.js";
|
|
53
|
-
|
|
54
|
-
import { LLMRequestType, LLMPriority
|
|
55
|
+
|
|
56
|
+
import { LLMRequestType, LLMPriority } from "../types.js";
|
|
55
57
|
import { getEmbeddingService, getItemEmbeddingText } from "../embedding-service.js";
|
|
58
|
+
import { crossFind } from "../utils/index.js";
|
|
56
59
|
|
|
57
60
|
export type ResponseHandler = (response: LLMResponse, state: StateManager) => void | Promise<void>;
|
|
58
61
|
|
|
@@ -104,20 +107,66 @@ function handlePersonaResponse(response: LLMResponse, state: StateManager): void
|
|
|
104
107
|
// the messages were "seen" and processed
|
|
105
108
|
state.messages_markPendingAsRead(personaId);
|
|
106
109
|
|
|
110
|
+
// Structured JSON path: queue-processor parsed valid JSON into `parsed`
|
|
111
|
+
if (response.parsed !== undefined) {
|
|
112
|
+
const result = response.parsed as PersonaResponseResult;
|
|
113
|
+
|
|
114
|
+
if (!result.should_respond) {
|
|
115
|
+
const reason = result.reason;
|
|
116
|
+
if (reason) {
|
|
117
|
+
console.log(`[handlePersonaResponse] ${personaDisplayName} chose silence: ${reason}`);
|
|
118
|
+
const silentMessage: Message = {
|
|
119
|
+
id: crypto.randomUUID(),
|
|
120
|
+
role: "system",
|
|
121
|
+
silence_reason: reason,
|
|
122
|
+
timestamp: new Date().toISOString(),
|
|
123
|
+
read: false,
|
|
124
|
+
context_status: ContextStatus.Never,
|
|
125
|
+
};
|
|
126
|
+
state.messages_append(personaId, silentMessage);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`[handlePersonaResponse] ${personaDisplayName} chose not to respond (no reason given)`);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Build message with structured fields
|
|
134
|
+
const verbal = result.verbal_response || undefined;
|
|
135
|
+
const action = result.action_response || undefined;
|
|
136
|
+
|
|
137
|
+
if (!verbal && !action) {
|
|
138
|
+
console.log(`[handlePersonaResponse] ${personaDisplayName} JSON had should_respond=true but no content fields`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const message: Message = {
|
|
143
|
+
id: crypto.randomUUID(),
|
|
144
|
+
role: "system",
|
|
145
|
+
verbal_response: verbal,
|
|
146
|
+
action_response: action,
|
|
147
|
+
timestamp: new Date().toISOString(),
|
|
148
|
+
read: false,
|
|
149
|
+
context_status: ContextStatus.Default,
|
|
150
|
+
};
|
|
151
|
+
state.messages_append(personaId, message);
|
|
152
|
+
console.log(`[handlePersonaResponse] Appended structured response to ${personaDisplayName}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Legacy plain-text fallback
|
|
107
157
|
if (!response.content) {
|
|
108
|
-
console.log(`[handlePersonaResponse]
|
|
158
|
+
console.log(`[handlePersonaResponse] ${personaDisplayName} chose not to respond (no reason given)`);
|
|
109
159
|
return;
|
|
110
160
|
}
|
|
111
161
|
|
|
112
162
|
const message: Message = {
|
|
113
163
|
id: crypto.randomUUID(),
|
|
114
164
|
role: "system",
|
|
115
|
-
|
|
165
|
+
verbal_response: response.content ?? undefined,
|
|
116
166
|
timestamp: new Date().toISOString(),
|
|
117
167
|
read: false,
|
|
118
168
|
context_status: ContextStatus.Default,
|
|
119
169
|
};
|
|
120
|
-
|
|
121
170
|
state.messages_append(personaId, message);
|
|
122
171
|
console.log(`[handlePersonaResponse] Appended response to ${personaDisplayName}`);
|
|
123
172
|
}
|
|
@@ -148,7 +197,7 @@ function handleHeartbeatCheck(response: LLMResponse, state: StateManager): void
|
|
|
148
197
|
const message: Message = {
|
|
149
198
|
id: crypto.randomUUID(),
|
|
150
199
|
role: "system",
|
|
151
|
-
|
|
200
|
+
verbal_response: result.message,
|
|
152
201
|
timestamp: now,
|
|
153
202
|
read: false,
|
|
154
203
|
context_status: ContextStatus.Default,
|
|
@@ -164,29 +213,52 @@ function handleEiHeartbeat(response: LLMResponse, state: StateManager): void {
|
|
|
164
213
|
console.error("[handleEiHeartbeat] No parsed result");
|
|
165
214
|
return;
|
|
166
215
|
}
|
|
167
|
-
|
|
168
216
|
const now = new Date().toISOString();
|
|
169
217
|
state.persona_update("ei", { last_heartbeat: now });
|
|
170
|
-
|
|
171
|
-
if (!result.should_respond) {
|
|
218
|
+
if (!result.should_respond || !result.id) {
|
|
172
219
|
console.log("[handleEiHeartbeat] Ei chose not to reach out");
|
|
173
220
|
return;
|
|
174
221
|
}
|
|
222
|
+
const isTUI = response.request.data.isTUI as boolean;
|
|
223
|
+
const found = crossFind(result.id, state.getHuman(), state.persona_getAll());
|
|
224
|
+
if (!found) {
|
|
225
|
+
console.warn(`[handleEiHeartbeat] Could not find item with id "${result.id}"`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
175
228
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
229
|
+
const sendMessage = (verbal_response: string) => state.messages_append("ei", {
|
|
230
|
+
id: crypto.randomUUID(),
|
|
231
|
+
role: "system",
|
|
232
|
+
verbal_response,
|
|
233
|
+
timestamp: now,
|
|
234
|
+
read: false,
|
|
235
|
+
context_status: ContextStatus.Default,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (found.type === "fact") {
|
|
239
|
+
const factsNav = isTUI ? "using /me facts" : "using \u2630 \u2192 My Data";
|
|
240
|
+
sendMessage(`Another persona updated a fact called "${found.name}" to "${found.description}". If that's right, you can lock it from further changes by ${factsNav}.`);
|
|
241
|
+
state.human_fact_upsert({ ...found, validated: ValidationLevel.Ei, validated_date: now });
|
|
242
|
+
console.log(`[handleEiHeartbeat] Notified about fact "${found.name}"`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (result.my_response) sendMessage(result.my_response);
|
|
247
|
+
|
|
248
|
+
switch (found.type) {
|
|
249
|
+
case "person":
|
|
250
|
+
state.human_person_upsert({ ...found, last_ei_asked: now });
|
|
251
|
+
console.log(`[handleEiHeartbeat] Reached out about person "${found.name}"`);
|
|
252
|
+
break;
|
|
253
|
+
case "topic":
|
|
254
|
+
state.human_topic_upsert({ ...found, last_ei_asked: now });
|
|
255
|
+
console.log(`[handleEiHeartbeat] Reached out about topic "${found.name}"`);
|
|
256
|
+
break;
|
|
257
|
+
case "persona":
|
|
258
|
+
console.log(`[handleEiHeartbeat] Reached out about persona "${found.display_name}"`);
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
console.warn(`[handleEiHeartbeat] Unexpected item type "${found.type}" for id "${result.id}"`);
|
|
190
262
|
}
|
|
191
263
|
}
|
|
192
264
|
|
|
@@ -291,59 +363,7 @@ function handlePersonaTraitExtraction(response: LLMResponse, state: StateManager
|
|
|
291
363
|
console.log(`[handlePersonaTraitExtraction] Updated ${traits.length} traits for ${personaDisplayName}`);
|
|
292
364
|
}
|
|
293
365
|
|
|
294
|
-
function handleEiValidation(response: LLMResponse, state: StateManager): void {
|
|
295
|
-
const validationId = response.request.data.validationId as string;
|
|
296
|
-
const dataType = response.request.data.dataType as string;
|
|
297
|
-
const itemName = response.request.data.itemName as string;
|
|
298
|
-
|
|
299
|
-
const result = response.parsed as EiValidationResult | undefined;
|
|
300
|
-
if (!result) {
|
|
301
|
-
console.error("[handleEiValidation] No parsed result");
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
console.log(`[handleEiValidation] Decision for ${dataType} "${itemName}": ${result.decision} - ${result.reason}`);
|
|
306
|
-
|
|
307
|
-
if (result.decision === "reject") {
|
|
308
|
-
if (validationId) {
|
|
309
|
-
state.queue_clearValidations([validationId]);
|
|
310
|
-
}
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
366
|
|
|
314
|
-
const itemToApply = result.decision === "modify" && result.modified_item
|
|
315
|
-
? result.modified_item
|
|
316
|
-
: response.request.data.proposedItem;
|
|
317
|
-
|
|
318
|
-
if (itemToApply && dataType) {
|
|
319
|
-
const now = new Date().toISOString();
|
|
320
|
-
const item = { ...itemToApply, last_updated: now };
|
|
321
|
-
|
|
322
|
-
switch (dataType) {
|
|
323
|
-
case "fact":
|
|
324
|
-
state.human_fact_upsert({
|
|
325
|
-
...item,
|
|
326
|
-
validated: ValidationLevel.Ei,
|
|
327
|
-
validated_date: now,
|
|
328
|
-
} as Fact);
|
|
329
|
-
break;
|
|
330
|
-
case "trait":
|
|
331
|
-
state.human_trait_upsert(item as Trait);
|
|
332
|
-
break;
|
|
333
|
-
case "topic":
|
|
334
|
-
state.human_topic_upsert(item as Topic);
|
|
335
|
-
break;
|
|
336
|
-
case "person":
|
|
337
|
-
state.human_person_upsert(item as Person);
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
console.log(`[handleEiValidation] Applied ${result.decision} for ${dataType} "${itemName}"`);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (validationId) {
|
|
344
|
-
state.queue_clearValidations([validationId]);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
367
|
|
|
348
368
|
function handleOneShot(_response: LLMResponse, _state: StateManager): void {
|
|
349
369
|
// One-shot is handled specially in Processor to fire onOneShotReturned
|
|
@@ -573,43 +593,48 @@ function handleHumanItemMatch(response: LLMResponse, state: StateManager): void
|
|
|
573
593
|
console.error("[handleHumanItemMatch] No parsed result");
|
|
574
594
|
return;
|
|
575
595
|
}
|
|
576
|
-
// "new" isn't a valid guid and is used as a marker for LLMs to signify "no match" - update for processing
|
|
577
|
-
if (result?.matched_guid === "new") {
|
|
578
|
-
result.matched_guid = null;
|
|
579
|
-
}
|
|
580
596
|
|
|
581
597
|
const candidateType = response.request.data.candidateType as DataItemType;
|
|
582
|
-
const itemName = response.request.data.itemName as string;
|
|
583
|
-
const itemValue = response.request.data.itemValue as string;
|
|
584
598
|
const personaId = response.request.data.personaId as string;
|
|
585
599
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
586
600
|
const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
|
|
587
|
-
|
|
588
601
|
const allMessages = state.messages_get(personaId);
|
|
589
602
|
const { messages_context, messages_analyze } = splitMessagesByTimestamp(allMessages, analyzeFrom);
|
|
590
|
-
|
|
591
|
-
if (result.matched_guid) {
|
|
592
|
-
const human = state.getHuman();
|
|
593
|
-
const matchedFact = human.facts.find(f => f.id === result.matched_guid);
|
|
594
|
-
if (matchedFact?.validated === ValidationLevel.Human) {
|
|
595
|
-
console.log(`[handleHumanItemMatch] Skipping locked fact "${matchedFact.name}" (human-validated)`);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
603
|
const context: ExtractionContext & { itemName: string; itemValue: string; itemCategory?: string } = {
|
|
601
604
|
personaId,
|
|
602
605
|
personaDisplayName,
|
|
603
606
|
messages_context,
|
|
604
607
|
messages_analyze,
|
|
605
|
-
itemName,
|
|
606
|
-
itemValue,
|
|
607
|
-
itemCategory:
|
|
608
|
+
itemName: response.request.data.itemName as string,
|
|
609
|
+
itemValue: response.request.data.itemValue as string,
|
|
610
|
+
itemCategory: response.request.data.itemCategory as string | undefined,
|
|
608
611
|
};
|
|
609
612
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
+
let resolvedType: DataItemType = candidateType;
|
|
614
|
+
let matched_guid = result.matched_guid;
|
|
615
|
+
if (matched_guid === "new") {
|
|
616
|
+
matched_guid = null;
|
|
617
|
+
} else if (matched_guid) {
|
|
618
|
+
const found = crossFind(matched_guid, state.getHuman());
|
|
619
|
+
if (!found) {
|
|
620
|
+
console.warn(`[handleHumanItemMatch] matched_guid "${matched_guid}" not found in human data — treating as new item`);
|
|
621
|
+
matched_guid = null;
|
|
622
|
+
} else if (found.type === "fact" && found.validated === ValidationLevel.Human) {
|
|
623
|
+
console.log(`[handleHumanItemMatch] Skipping locked fact "${found.name}" (human-validated)`);
|
|
624
|
+
return;
|
|
625
|
+
} else if (!(found.type === "fact" || found.type === "trait" || found.type === "topic" || found.type === "person")) {
|
|
626
|
+
console.warn(`[handleHumanItemMatch] matched_guid "${matched_guid}" resolved to non-human type "${found.type}" - Ignoring`);
|
|
627
|
+
return;
|
|
628
|
+
} else {
|
|
629
|
+
resolvedType = found.type;
|
|
630
|
+
context.itemName = found.name || context.itemName;
|
|
631
|
+
context.itemValue = found.description || context.itemValue;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
result.matched_guid = matched_guid;
|
|
635
|
+
queueItemUpdate(resolvedType, result, context, state);
|
|
636
|
+
const matched = matched_guid ? `matched GUID "${matched_guid}"` : "no match (new item)";
|
|
637
|
+
console.log(`[handleHumanItemMatch] ${resolvedType} "${context.itemName}": ${matched}`);
|
|
613
638
|
}
|
|
614
639
|
|
|
615
640
|
async function handleHumanItemUpdate(response: LLMResponse, state: StateManager): Promise<void> {
|
|
@@ -632,7 +657,15 @@ async function handleHumanItemUpdate(response: LLMResponse, state: StateManager)
|
|
|
632
657
|
}
|
|
633
658
|
|
|
634
659
|
const now = new Date().toISOString();
|
|
635
|
-
const
|
|
660
|
+
const resolveItemId = (): string => {
|
|
661
|
+
if (isNewItem || !existingItemId) return crypto.randomUUID();
|
|
662
|
+
const h = state.getHuman();
|
|
663
|
+
const arr = candidateType === "fact" ? h.facts : candidateType === "trait" ? h.traits : candidateType === "topic" ? h.topics : h.people;
|
|
664
|
+
// Guard: if existingItemId isn't in the correct type array, treat as new
|
|
665
|
+
// (prevents cross-type ID reuse when LLM matches against a different type's UUID)
|
|
666
|
+
return arr.find((x: DataItemBase) => x.id === existingItemId) ? existingItemId : crypto.randomUUID();
|
|
667
|
+
};
|
|
668
|
+
const itemId = resolveItemId();
|
|
636
669
|
|
|
637
670
|
const persona = state.persona_getById(personaId);
|
|
638
671
|
const personaGroup = persona?.group_primary ?? null;
|
|
@@ -745,6 +778,18 @@ async function handleHumanItemUpdate(response: LLMResponse, state: StateManager)
|
|
|
745
778
|
console.log(`[handleHumanItemUpdate] ${isNewItem ? "Created" : "Updated"} ${candidateType} "${result.name}"`);
|
|
746
779
|
}
|
|
747
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Returns the combined display text of a message for quote indexing.
|
|
783
|
+
* Mirrors the rendering logic used in the frontends.
|
|
784
|
+
*/
|
|
785
|
+
function getMessageText(message: Message): string {
|
|
786
|
+
const parts: string[] = [];
|
|
787
|
+
if (message.action_response) parts.push(`_${message.action_response}_`);
|
|
788
|
+
if (message.verbal_response) parts.push(message.verbal_response);
|
|
789
|
+
return parts.join('\n\n');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
|
|
748
793
|
async function validateAndStoreQuotes(
|
|
749
794
|
candidates: Array<{ text: string; reason: string }> | undefined,
|
|
750
795
|
messages: Message[],
|
|
@@ -758,22 +803,53 @@ async function validateAndStoreQuotes(
|
|
|
758
803
|
for (const candidate of candidates) {
|
|
759
804
|
let found = false;
|
|
760
805
|
for (const message of messages) {
|
|
761
|
-
const
|
|
806
|
+
const msgText = getMessageText(message);
|
|
807
|
+
const start = msgText.indexOf(candidate.text);
|
|
762
808
|
if (start !== -1) {
|
|
763
809
|
const end = start + candidate.text.length;
|
|
764
810
|
|
|
811
|
+
// Check for ANY overlapping quote in this message (not just exact match)
|
|
765
812
|
const existing = state.human_quote_getForMessage(message.id);
|
|
766
|
-
const
|
|
813
|
+
const overlapping = existing.find(q =>
|
|
814
|
+
q.start !== null && q.end !== null &&
|
|
815
|
+
start < q.end && end > q.start // ranges overlap
|
|
816
|
+
);
|
|
767
817
|
|
|
768
|
-
if (
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
818
|
+
if (overlapping) {
|
|
819
|
+
// Merge: expand to the union of both ranges
|
|
820
|
+
const mergedStart = Math.min(start, overlapping.start!);
|
|
821
|
+
const mergedEnd = Math.max(end, overlapping.end!);
|
|
822
|
+
const mergedText = msgText.slice(mergedStart, mergedEnd);
|
|
823
|
+
|
|
824
|
+
// Merge data_item_ids and persona_groups (deduplicated)
|
|
825
|
+
const mergedDataItemIds = overlapping.data_item_ids.includes(dataItemId)
|
|
826
|
+
? overlapping.data_item_ids
|
|
827
|
+
: [...overlapping.data_item_ids, dataItemId];
|
|
828
|
+
const group = personaGroup || "General";
|
|
829
|
+
const mergedGroups = overlapping.persona_groups.includes(group)
|
|
830
|
+
? overlapping.persona_groups
|
|
831
|
+
: [...overlapping.persona_groups, group];
|
|
832
|
+
|
|
833
|
+
// Only recompute embedding if the text actually changed
|
|
834
|
+
let embedding = overlapping.embedding;
|
|
835
|
+
if (mergedText !== overlapping.text) {
|
|
836
|
+
try {
|
|
837
|
+
const embeddingService = getEmbeddingService();
|
|
838
|
+
embedding = await embeddingService.embed(mergedText);
|
|
839
|
+
} catch (err) {
|
|
840
|
+
console.warn(`[extraction] Failed to recompute embedding for merged quote: "${mergedText.slice(0, 30)}..."`, err);
|
|
841
|
+
}
|
|
776
842
|
}
|
|
843
|
+
|
|
844
|
+
state.human_quote_update(overlapping.id, {
|
|
845
|
+
start: mergedStart,
|
|
846
|
+
end: mergedEnd,
|
|
847
|
+
text: mergedText,
|
|
848
|
+
data_item_ids: mergedDataItemIds,
|
|
849
|
+
persona_groups: mergedGroups,
|
|
850
|
+
embedding,
|
|
851
|
+
});
|
|
852
|
+
console.log(`[extraction] Merged overlapping quote: "${mergedText.slice(0, 50)}..." (${mergedStart}-${mergedEnd})`);
|
|
777
853
|
found = true;
|
|
778
854
|
break;
|
|
779
855
|
}
|
|
@@ -826,23 +902,16 @@ function applyOrValidate(
|
|
|
826
902
|
state: StateManager,
|
|
827
903
|
dataType: DataItemType,
|
|
828
904
|
item: Fact | Trait | Topic | Person,
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
905
|
+
_personaName: string,
|
|
906
|
+
_isEi: boolean,
|
|
907
|
+
_personaGroup: string | null
|
|
832
908
|
): void {
|
|
833
|
-
const isGeneralGroup = !personaGroup || personaGroup.toLowerCase() === "general";
|
|
834
|
-
const needsValidation = !isEi && isGeneralGroup;
|
|
835
|
-
|
|
836
909
|
switch (dataType) {
|
|
837
910
|
case "fact": state.human_fact_upsert(item as Fact); break;
|
|
838
911
|
case "trait": state.human_trait_upsert(item as Trait); break;
|
|
839
912
|
case "topic": state.human_topic_upsert(item as Topic); break;
|
|
840
913
|
case "person": state.human_person_upsert(item as Person); break;
|
|
841
914
|
}
|
|
842
|
-
|
|
843
|
-
if (needsValidation) {
|
|
844
|
-
queueEiValidation(state, dataType, item, personaName);
|
|
845
|
-
}
|
|
846
915
|
}
|
|
847
916
|
|
|
848
917
|
const MIN_MESSAGE_COUNT_FOR_CREATE = 2;
|
|
@@ -986,52 +1055,7 @@ function handlePersonaTopicUpdate(response: LLMResponse, state: StateManager): v
|
|
|
986
1055
|
}
|
|
987
1056
|
}
|
|
988
1057
|
|
|
989
|
-
function queueEiValidation(
|
|
990
|
-
state: StateManager,
|
|
991
|
-
dataType: DataItemType,
|
|
992
|
-
item: Fact | Trait | Topic | Person,
|
|
993
|
-
sourcePersona: string
|
|
994
|
-
): void {
|
|
995
|
-
const human = state.getHuman();
|
|
996
|
-
let existingItem: Fact | Trait | Topic | Person | undefined;
|
|
997
1058
|
|
|
998
|
-
switch (dataType) {
|
|
999
|
-
case "fact": existingItem = human.facts.find(f => f.id === item.id); break;
|
|
1000
|
-
case "trait": existingItem = human.traits.find(t => t.id === item.id); break;
|
|
1001
|
-
case "topic": existingItem = human.topics.find(t => t.id === item.id); break;
|
|
1002
|
-
case "person": existingItem = human.people.find(p => p.id === item.id); break;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const prompt = buildEiValidationPrompt({
|
|
1006
|
-
validation_type: "cross_persona",
|
|
1007
|
-
item_name: item.name,
|
|
1008
|
-
data_type: dataType,
|
|
1009
|
-
context: `Learned from conversation with ${sourcePersona}`,
|
|
1010
|
-
source_persona: sourcePersona,
|
|
1011
|
-
current_item: existingItem,
|
|
1012
|
-
proposed_item: item,
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
// Cross-persona validation is always normal priority
|
|
1016
|
-
const priority = LLMPriority.Normal;
|
|
1017
|
-
|
|
1018
|
-
state.queue_enqueue({
|
|
1019
|
-
type: LLMRequestType.JSON,
|
|
1020
|
-
priority,
|
|
1021
|
-
system: prompt.system,
|
|
1022
|
-
user: prompt.user,
|
|
1023
|
-
next_step: NextStep.HandleEiValidation,
|
|
1024
|
-
data: {
|
|
1025
|
-
validationId: crypto.randomUUID(),
|
|
1026
|
-
dataType,
|
|
1027
|
-
itemName: item.name,
|
|
1028
|
-
proposedItem: item,
|
|
1029
|
-
sourcePersona,
|
|
1030
|
-
},
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
console.log(`[queueEiValidation] Queued ${dataType} "${item.name}" from ${sourcePersona} for Ei validation`);
|
|
1034
|
-
}
|
|
1035
1059
|
|
|
1036
1060
|
export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
1037
1061
|
handlePersonaResponse,
|
|
@@ -1049,7 +1073,6 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
|
1049
1073
|
handlePersonaTopicUpdate,
|
|
1050
1074
|
handleHeartbeatCheck,
|
|
1051
1075
|
handleEiHeartbeat,
|
|
1052
|
-
handleEiValidation,
|
|
1053
1076
|
handleOneShot,
|
|
1054
1077
|
handlePersonaExpire,
|
|
1055
1078
|
handlePersonaExplore,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { LLMRequestType, LLMPriority, LLMNextStep, MESSAGE_MIN_COUNT, MESSAGE_MAX_AGE_DAYS, type CeremonyConfig, type PersonaTopic, type Topic } from "../types.js";
|
|
1
|
+
import { LLMRequestType, LLMPriority, LLMNextStep, MESSAGE_MIN_COUNT, MESSAGE_MAX_AGE_DAYS, type CeremonyConfig, type PersonaTopic, type Topic, type Message } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
|
-
import { applyDecayToValue } from "../utils/
|
|
3
|
+
import { applyDecayToValue } from "../utils/index.js";
|
|
4
4
|
import {
|
|
5
5
|
queueFactScan,
|
|
6
6
|
queueTraitScan,
|
|
@@ -384,12 +384,12 @@ export function queueExplorePhase(personaId: string, state: StateManager): void
|
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
function extractConversationThemes(messages:
|
|
387
|
+
function extractConversationThemes(messages: Message[]): string[] {
|
|
388
388
|
const humanMessages = messages.filter(m => m.role === "human");
|
|
389
389
|
if (humanMessages.length === 0) return [];
|
|
390
390
|
|
|
391
391
|
const words = humanMessages
|
|
392
|
-
.map(m => m.
|
|
392
|
+
.map(m => (m.verbal_response ?? '').toLowerCase())
|
|
393
393
|
.join(" ")
|
|
394
394
|
.split(/\s+/)
|
|
395
395
|
.filter(w => w.length > 4);
|
|
@@ -12,7 +12,7 @@ function estimateTokens(text: string): number {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function estimateMessageTokens(messages: Message[]): number {
|
|
15
|
-
return messages.reduce((sum, msg) => sum + estimateTokens(msg.
|
|
15
|
+
return messages.reduce((sum, msg) => sum + estimateTokens(msg.verbal_response ?? '') + 4, 0);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function fitMessagesFromEnd(messages: Message[], maxTokens: number): Message[] {
|
|
@@ -20,7 +20,7 @@ function fitMessagesFromEnd(messages: Message[], maxTokens: number): Message[] {
|
|
|
20
20
|
let tokens = 0;
|
|
21
21
|
|
|
22
22
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
23
|
-
const msgTokens = estimateTokens(messages[i].
|
|
23
|
+
const msgTokens = estimateTokens(messages[i].verbal_response ?? '') + 4;
|
|
24
24
|
if (tokens + msgTokens > maxTokens) break;
|
|
25
25
|
result.unshift(messages[i]);
|
|
26
26
|
tokens += msgTokens;
|
|
@@ -39,7 +39,7 @@ function pullMessagesFromStart(
|
|
|
39
39
|
let i = startIndex;
|
|
40
40
|
|
|
41
41
|
while (i < messages.length) {
|
|
42
|
-
const msgTokens = estimateTokens(messages[i].
|
|
42
|
+
const msgTokens = estimateTokens(messages[i].verbal_response ?? '') + 4;
|
|
43
43
|
if (tokens + msgTokens > maxTokens && pulled.length > 0) break;
|
|
44
44
|
pulled.push(messages[i]);
|
|
45
45
|
tokens += msgTokens;
|