ei-tui 0.1.3 → 0.1.4

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.
@@ -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,7 @@ import type {
22
23
  PersonaTopicMatchResult,
23
24
  PersonaTopicUpdateResult,
24
25
  } from "../../prompts/persona/types.js";
25
- import type { EiValidationResult } from "../../prompts/validation/types.js";
26
+
26
27
  import type {
27
28
  PersonaExpireResult,
28
29
  PersonaExploreResult,
@@ -50,9 +51,10 @@ import type {
50
51
  ItemUpdateResult,
51
52
  ExposureImpact,
52
53
  } from "../../prompts/human/types.js";
53
- import { buildEiValidationPrompt } from "../../prompts/validation/index.js";
54
- import { LLMRequestType, LLMPriority, LLMNextStep as NextStep } from "../types.js";
54
+
55
+ import { LLMRequestType, LLMPriority } from "../types.js";
55
56
  import { getEmbeddingService, getItemEmbeddingText } from "../embedding-service.js";
57
+ import { crossFind } from "../utils/index.js";
56
58
 
57
59
  export type ResponseHandler = (response: LLMResponse, state: StateManager) => void | Promise<void>;
58
60
 
@@ -164,29 +166,52 @@ function handleEiHeartbeat(response: LLMResponse, state: StateManager): void {
164
166
  console.error("[handleEiHeartbeat] No parsed result");
165
167
  return;
166
168
  }
167
-
168
169
  const now = new Date().toISOString();
169
170
  state.persona_update("ei", { last_heartbeat: now });
170
-
171
- if (!result.should_respond) {
171
+ if (!result.should_respond || !result.id) {
172
172
  console.log("[handleEiHeartbeat] Ei chose not to reach out");
173
173
  return;
174
174
  }
175
+ const isTUI = response.request.data.isTUI as boolean;
176
+ const found = crossFind(result.id, state.getHuman(), state.persona_getAll());
177
+ if (!found) {
178
+ console.warn(`[handleEiHeartbeat] Could not find item with id "${result.id}"`);
179
+ return;
180
+ }
175
181
 
176
- if (result.message) {
177
- const message: Message = {
178
- id: crypto.randomUUID(),
179
- role: "system",
180
- content: result.message,
181
- timestamp: now,
182
- read: false,
183
- context_status: ContextStatus.Default,
184
- };
185
- state.messages_append("ei", message);
186
- console.log("[handleEiHeartbeat] Ei proactively messaged");
187
- if (result.priorities) {
188
- console.log("[handleEiHeartbeat] Priorities:", result.priorities.map(p => p.name).join(", "));
189
- }
182
+ const sendMessage = (content: string) => state.messages_append("ei", {
183
+ id: crypto.randomUUID(),
184
+ role: "system",
185
+ content,
186
+ timestamp: now,
187
+ read: false,
188
+ context_status: ContextStatus.Default,
189
+ });
190
+
191
+ if (found.type === "fact") {
192
+ const factsNav = isTUI ? "using /me facts" : "using \u2630 \u2192 My Data";
193
+ 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}.`);
194
+ state.human_fact_upsert({ ...found, validated: ValidationLevel.Ei, validated_date: now });
195
+ console.log(`[handleEiHeartbeat] Notified about fact "${found.name}"`);
196
+ return;
197
+ }
198
+
199
+ if (result.my_response) sendMessage(result.my_response);
200
+
201
+ switch (found.type) {
202
+ case "person":
203
+ state.human_person_upsert({ ...found, last_ei_asked: now });
204
+ console.log(`[handleEiHeartbeat] Reached out about person "${found.name}"`);
205
+ break;
206
+ case "topic":
207
+ state.human_topic_upsert({ ...found, last_ei_asked: now });
208
+ console.log(`[handleEiHeartbeat] Reached out about topic "${found.name}"`);
209
+ break;
210
+ case "persona":
211
+ console.log(`[handleEiHeartbeat] Reached out about persona "${found.display_name}"`);
212
+ break;
213
+ default:
214
+ console.warn(`[handleEiHeartbeat] Unexpected item type "${found.type}" for id "${result.id}"`);
190
215
  }
191
216
  }
192
217
 
@@ -291,59 +316,7 @@ function handlePersonaTraitExtraction(response: LLMResponse, state: StateManager
291
316
  console.log(`[handlePersonaTraitExtraction] Updated ${traits.length} traits for ${personaDisplayName}`);
292
317
  }
293
318
 
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
-
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
319
 
343
- if (validationId) {
344
- state.queue_clearValidations([validationId]);
345
- }
346
- }
347
320
 
348
321
  function handleOneShot(_response: LLMResponse, _state: StateManager): void {
349
322
  // One-shot is handled specially in Processor to fire onOneShotReturned
@@ -573,43 +546,48 @@ function handleHumanItemMatch(response: LLMResponse, state: StateManager): void
573
546
  console.error("[handleHumanItemMatch] No parsed result");
574
547
  return;
575
548
  }
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
549
 
581
550
  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
551
  const personaId = response.request.data.personaId as string;
585
552
  const personaDisplayName = response.request.data.personaDisplayName as string;
586
553
  const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
587
-
588
554
  const allMessages = state.messages_get(personaId);
589
555
  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
556
  const context: ExtractionContext & { itemName: string; itemValue: string; itemCategory?: string } = {
601
557
  personaId,
602
558
  personaDisplayName,
603
559
  messages_context,
604
560
  messages_analyze,
605
- itemName,
606
- itemValue,
607
- itemCategory: candidateType === "topic" ? itemValue : undefined,
561
+ itemName: response.request.data.itemName as string,
562
+ itemValue: response.request.data.itemValue as string,
563
+ itemCategory: response.request.data.itemCategory as string | undefined,
608
564
  };
609
565
 
610
- queueItemUpdate(candidateType, result, context, state);
611
- const matched = result.matched_guid ? `matched GUID "${result.matched_guid}"` : "no match (new item)";
612
- console.log(`[handleHumanItemMatch] ${candidateType} "${itemName}": ${matched}`);
566
+ let resolvedType: DataItemType = candidateType;
567
+ let matched_guid = result.matched_guid;
568
+ if (matched_guid === "new") {
569
+ matched_guid = null;
570
+ } else if (matched_guid) {
571
+ const found = crossFind(matched_guid, state.getHuman());
572
+ if (!found) {
573
+ console.warn(`[handleHumanItemMatch] matched_guid "${matched_guid}" not found in human data — treating as new item`);
574
+ matched_guid = null;
575
+ } else if (found.type === "fact" && found.validated === ValidationLevel.Human) {
576
+ console.log(`[handleHumanItemMatch] Skipping locked fact "${found.name}" (human-validated)`);
577
+ return;
578
+ } else if (!(found.type === "fact" || found.type === "trait" || found.type === "topic" || found.type === "person")) {
579
+ console.warn(`[handleHumanItemMatch] matched_guid "${matched_guid}" resolved to non-human type "${found.type}" - Ignoring`);
580
+ return;
581
+ } else {
582
+ resolvedType = found.type;
583
+ context.itemName = found.name || context.itemName;
584
+ context.itemValue = found.description || context.itemValue;
585
+ }
586
+ }
587
+ result.matched_guid = matched_guid;
588
+ queueItemUpdate(resolvedType, result, context, state);
589
+ const matched = matched_guid ? `matched GUID "${matched_guid}"` : "no match (new item)";
590
+ console.log(`[handleHumanItemMatch] ${resolvedType} "${context.itemName}": ${matched}`);
613
591
  }
614
592
 
615
593
  async function handleHumanItemUpdate(response: LLMResponse, state: StateManager): Promise<void> {
@@ -632,7 +610,15 @@ async function handleHumanItemUpdate(response: LLMResponse, state: StateManager)
632
610
  }
633
611
 
634
612
  const now = new Date().toISOString();
635
- const itemId = isNewItem ? crypto.randomUUID() : (existingItemId ?? crypto.randomUUID());
613
+ const resolveItemId = (): string => {
614
+ if (isNewItem || !existingItemId) return crypto.randomUUID();
615
+ const h = state.getHuman();
616
+ const arr = candidateType === "fact" ? h.facts : candidateType === "trait" ? h.traits : candidateType === "topic" ? h.topics : h.people;
617
+ // Guard: if existingItemId isn't in the correct type array, treat as new
618
+ // (prevents cross-type ID reuse when LLM matches against a different type's UUID)
619
+ return arr.find((x: DataItemBase) => x.id === existingItemId) ? existingItemId : crypto.randomUUID();
620
+ };
621
+ const itemId = resolveItemId();
636
622
 
637
623
  const persona = state.persona_getById(personaId);
638
624
  const personaGroup = persona?.group_primary ?? null;
@@ -826,23 +812,16 @@ function applyOrValidate(
826
812
  state: StateManager,
827
813
  dataType: DataItemType,
828
814
  item: Fact | Trait | Topic | Person,
829
- personaName: string,
830
- isEi: boolean,
831
- personaGroup: string | null
815
+ _personaName: string,
816
+ _isEi: boolean,
817
+ _personaGroup: string | null
832
818
  ): void {
833
- const isGeneralGroup = !personaGroup || personaGroup.toLowerCase() === "general";
834
- const needsValidation = !isEi && isGeneralGroup;
835
-
836
819
  switch (dataType) {
837
820
  case "fact": state.human_fact_upsert(item as Fact); break;
838
821
  case "trait": state.human_trait_upsert(item as Trait); break;
839
822
  case "topic": state.human_topic_upsert(item as Topic); break;
840
823
  case "person": state.human_person_upsert(item as Person); break;
841
824
  }
842
-
843
- if (needsValidation) {
844
- queueEiValidation(state, dataType, item, personaName);
845
- }
846
825
  }
847
826
 
848
827
  const MIN_MESSAGE_COUNT_FOR_CREATE = 2;
@@ -986,52 +965,7 @@ function handlePersonaTopicUpdate(response: LLMResponse, state: StateManager): v
986
965
  }
987
966
  }
988
967
 
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
-
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
968
 
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
969
 
1036
970
  export const handlers: Record<LLMNextStep, ResponseHandler> = {
1037
971
  handlePersonaResponse,
@@ -1049,7 +983,6 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
1049
983
  handlePersonaTopicUpdate,
1050
984
  handleHeartbeatCheck,
1051
985
  handleEiHeartbeat,
1052
- handleEiValidation,
1053
986
  handleOneShot,
1054
987
  handlePersonaExpire,
1055
988
  handlePersonaExplore,
@@ -1,6 +1,6 @@
1
1
  import { LLMRequestType, LLMPriority, LLMNextStep, MESSAGE_MIN_COUNT, MESSAGE_MAX_AGE_DAYS, type CeremonyConfig, type PersonaTopic, type Topic } from "../types.js";
2
2
  import type { StateManager } from "../state-manager.js";
3
- import { applyDecayToValue } from "../utils/decay.js";
3
+ import { applyDecayToValue } from "../utils/index.js";
4
4
  import {
5
5
  queueFactScan,
6
6
  queueTraitScan,