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.
- package/README.md +31 -34
- package/package.json +5 -1
- package/src/README.md +85 -1
- package/src/cli/README.md +29 -21
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +91 -158
- package/src/core/orchestrators/ceremony.ts +1 -1
- package/src/core/processor.ts +172 -45
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/queue.ts +1 -10
- package/src/core/state-manager.ts +1 -7
- package/src/core/types.ts +4 -5
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +117 -690
- package/src/prompts/heartbeat/ei.ts +61 -133
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/index.ts +2 -5
- package/tui/README.md +77 -3
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/PromptInput.tsx +111 -29
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/context/ei.tsx +10 -0
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +12 -4
- 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,7 @@ import type {
|
|
|
22
23
|
PersonaTopicMatchResult,
|
|
23
24
|
PersonaTopicUpdateResult,
|
|
24
25
|
} from "../../prompts/persona/types.js";
|
|
25
|
-
|
|
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
|
-
|
|
54
|
-
import { LLMRequestType, LLMPriority
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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:
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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/
|
|
3
|
+
import { applyDecayToValue } from "../utils/index.js";
|
|
4
4
|
import {
|
|
5
5
|
queueFactScan,
|
|
6
6
|
queueTraitScan,
|