ei-tui 0.5.4 → 0.6.1
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/package.json +1 -1
- package/src/core/constants/built-in-identifier-types.ts +24 -0
- package/src/core/embedding-service.ts +24 -1
- package/src/core/handlers/dedup.ts +34 -4
- package/src/core/handlers/heartbeat.ts +16 -0
- package/src/core/handlers/human-extraction.ts +201 -7
- package/src/core/handlers/human-matching.ts +71 -22
- package/src/core/handlers/index.ts +52 -14
- package/src/core/handlers/persona-generation.ts +2 -0
- package/src/core/handlers/persona-response.ts +37 -22
- package/src/core/handlers/persona-topics.ts +35 -271
- package/src/core/handlers/rewrite.ts +3 -0
- package/src/core/handlers/rooms.ts +41 -20
- package/src/core/handlers/utils.ts +10 -8
- package/src/core/heartbeat-manager.ts +60 -2
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +3 -2
- package/src/core/orchestrators/ceremony.ts +54 -144
- package/src/core/orchestrators/dedup-phase.ts +0 -199
- package/src/core/orchestrators/extraction-chunker.ts +8 -3
- package/src/core/orchestrators/human-extraction.ts +37 -85
- package/src/core/orchestrators/index.ts +4 -8
- package/src/core/orchestrators/person-migration.ts +55 -0
- package/src/core/orchestrators/persona-topics.ts +64 -89
- package/src/core/orchestrators/room-extraction.ts +34 -0
- package/src/core/persona-manager.ts +21 -2
- package/src/core/personas/opencode-agent.ts +1 -0
- package/src/core/processor.ts +51 -14
- package/src/core/prompt-context-builder.ts +38 -5
- package/src/core/queue-processor.ts +4 -2
- package/src/core/room-manager.ts +6 -7
- package/src/core/state/human.ts +6 -0
- package/src/core/state/personas.ts +35 -10
- package/src/core/state/rooms.ts +21 -0
- package/src/core/state-manager.ts +61 -0
- package/src/core/types/data-items.ts +12 -0
- package/src/core/types/entities.ts +3 -0
- package/src/core/types/enums.ts +2 -7
- package/src/core/types/llm.ts +2 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/core/utils/identifier-utils.ts +19 -0
- package/src/core/utils/index.ts +2 -1
- package/src/core/utils/levenshtein.ts +18 -0
- package/src/integrations/claude-code/importer.ts +1 -0
- package/src/integrations/cursor/importer.ts +1 -0
- package/src/prompts/ceremony/index.ts +1 -0
- package/src/prompts/ceremony/person-migration.ts +77 -0
- package/src/prompts/ceremony/rewrite.ts +1 -1
- package/src/prompts/ceremony/user-dedup.ts +15 -1
- package/src/prompts/heartbeat/check.ts +28 -12
- package/src/prompts/heartbeat/ei.ts +2 -0
- package/src/prompts/heartbeat/types.ts +12 -0
- package/src/prompts/human/index.ts +0 -2
- package/src/prompts/human/person-scan.ts +58 -14
- package/src/prompts/human/person-update.ts +171 -96
- package/src/prompts/human/topic-update.ts +1 -1
- package/src/prompts/human/types.ts +5 -1
- package/src/prompts/index.ts +3 -10
- package/src/prompts/message-utils.ts +9 -23
- package/src/prompts/persona/index.ts +3 -10
- package/src/prompts/persona/topics-rate.ts +95 -0
- package/src/prompts/persona/types.ts +8 -48
- package/src/prompts/response/index.ts +3 -7
- package/src/prompts/response/sections.ts +7 -57
- package/src/prompts/room/index.ts +1 -1
- package/src/prompts/room/sections.ts +8 -31
- package/tui/src/commands/me.tsx +14 -7
- package/tui/src/commands/persona.tsx +120 -83
- package/tui/src/components/MessageList.tsx +9 -4
- package/tui/src/components/RoomMessageList.tsx +10 -5
- package/tui/src/context/keyboard.tsx +2 -2
- package/tui/src/util/cyp-editor.tsx +13 -8
- package/tui/src/util/yaml-context.ts +66 -0
- package/tui/src/util/yaml-human.ts +274 -0
- package/tui/src/util/yaml-persona.ts +479 -0
- package/tui/src/util/yaml-provider.ts +215 -0
- package/tui/src/util/yaml-queue.ts +81 -0
- package/tui/src/util/yaml-quotes.ts +46 -0
- package/tui/src/util/yaml-serializers.ts +9 -1417
- package/tui/src/util/yaml-settings.ts +223 -0
- package/tui/src/util/yaml-shared.ts +32 -0
- package/tui/src/util/yaml-toolkit.ts +55 -0
- package/src/prompts/human/person-match.ts +0 -65
- package/src/prompts/persona/topics-match.ts +0 -70
- package/src/prompts/persona/topics-scan.ts +0 -98
- package/src/prompts/persona/topics-update.ts +0 -154
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Message, RoomMessage, LLMResponse } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
|
|
4
|
+
export function getMessageContent(msg: { content?: string; verbal_response?: string; action_response?: string }): string {
|
|
5
|
+
if (msg.content) return msg.content;
|
|
6
|
+
const parts: string[] = [];
|
|
7
|
+
if (msg.action_response) parts.push(`_${msg.action_response}_`);
|
|
8
|
+
if (msg.verbal_response) parts.push(msg.verbal_response);
|
|
9
|
+
return parts.join('\n\n');
|
|
10
|
+
}
|
|
11
|
+
|
|
4
12
|
export function normalizeRoomMessages(messages: RoomMessage[], state: StateManager): Message[] {
|
|
5
13
|
const human = state.getHuman();
|
|
6
14
|
const humanName = human.settings?.name_display ?? "Human";
|
|
@@ -12,6 +20,7 @@ export function normalizeRoomMessages(messages: RoomMessage[], state: StateManag
|
|
|
12
20
|
id: m.id,
|
|
13
21
|
role: m.role === "human" ? "human" as const : "system" as const,
|
|
14
22
|
speaker_name: speakerName,
|
|
23
|
+
content: m.content,
|
|
15
24
|
verbal_response: m.verbal_response,
|
|
16
25
|
action_response: m.action_response,
|
|
17
26
|
silence_reason: m.silence_reason,
|
|
@@ -111,13 +120,6 @@ export function markMessagesExtracted(
|
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
/**
|
|
115
|
-
* Returns the combined display text of a message for quote indexing.
|
|
116
|
-
* Mirrors the rendering logic used in the frontends.
|
|
117
|
-
*/
|
|
118
123
|
export function getMessageText(message: Message): string {
|
|
119
|
-
|
|
120
|
-
if (message.action_response) parts.push(`_${message.action_response}_`);
|
|
121
|
-
if (message.verbal_response) parts.push(message.verbal_response);
|
|
122
|
-
return parts.join('\n\n');
|
|
124
|
+
return getMessageContent(message);
|
|
123
125
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type Message,
|
|
7
7
|
} from "./types.js";
|
|
8
8
|
import { StateManager } from "./state-manager.js";
|
|
9
|
+
import { getMessageContent } from "./handlers/utils.js";
|
|
9
10
|
import {
|
|
10
11
|
buildHeartbeatCheckPrompt,
|
|
11
12
|
buildEiHeartbeatPrompt,
|
|
@@ -15,6 +16,10 @@ import {
|
|
|
15
16
|
} from "../prompts/index.js";
|
|
16
17
|
import { filterMessagesForContext } from "./context-utils.js";
|
|
17
18
|
import { filterHumanDataByVisibility } from "./prompt-context-builder.js";
|
|
19
|
+
import { cosineSimilarity, computePersonaDescriptionEmbedding } from "./embedding-service.js";
|
|
20
|
+
|
|
21
|
+
const REFLECTION_SIMILARITY_THRESHOLD = 0.80;
|
|
22
|
+
const REFLECTION_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 1 week between drift prompts
|
|
18
23
|
|
|
19
24
|
// =============================================================================
|
|
20
25
|
// MODEL HELPERS
|
|
@@ -43,7 +48,7 @@ export function countTrailingPersonaMessages(history: Message[]): number {
|
|
|
43
48
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
44
49
|
const msg = history[i];
|
|
45
50
|
if (msg.role === "human") break;
|
|
46
|
-
if (msg.role === "system" && msg
|
|
51
|
+
if (msg.role === "system" && getMessageContent(msg) && msg.silence_reason === undefined) {
|
|
47
52
|
count++;
|
|
48
53
|
}
|
|
49
54
|
}
|
|
@@ -84,6 +89,20 @@ export async function queueEiHeartbeat(
|
|
|
84
89
|
});
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
const newPeople = human.people
|
|
93
|
+
.filter(p => !p.validated_date)
|
|
94
|
+
.slice(0, 3);
|
|
95
|
+
for (const person of newPeople) {
|
|
96
|
+
const quote = human.quotes.find((q) => q.data_item_ids.includes(person.id));
|
|
97
|
+
items.push({
|
|
98
|
+
id: person.id,
|
|
99
|
+
type: "New Person",
|
|
100
|
+
name: person.name,
|
|
101
|
+
description: person.description ?? '',
|
|
102
|
+
quote: quote?.text,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
const underEngagedPeople = human.people
|
|
88
107
|
.filter(
|
|
89
108
|
(p) =>
|
|
@@ -174,7 +193,7 @@ export async function queueEiHeartbeat(
|
|
|
174
193
|
user: prompt.user,
|
|
175
194
|
next_step: LLMNextStep.HandleEiHeartbeat,
|
|
176
195
|
model: getModelForPersona(sm, "ei"),
|
|
177
|
-
data: { personaId: "ei", isTUI },
|
|
196
|
+
data: { personaId: "ei", isTUI, newPersonIds: newPeople.map(p => p.id) },
|
|
178
197
|
});
|
|
179
198
|
}
|
|
180
199
|
|
|
@@ -213,6 +232,44 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
|
|
|
213
232
|
b.exposure_desired - b.exposure_current - (a.exposure_desired - a.exposure_current)
|
|
214
233
|
);
|
|
215
234
|
|
|
235
|
+
let driftContext: HeartbeatCheckPromptData["drift_context"];
|
|
236
|
+
const personRecord = sm.human_person_getByIdentifier("ei_persona", personaId);
|
|
237
|
+
|
|
238
|
+
if (personRecord?.embedding) {
|
|
239
|
+
let currentPersona = persona;
|
|
240
|
+
|
|
241
|
+
if (!currentPersona.description_embedding) {
|
|
242
|
+
const embedding = await computePersonaDescriptionEmbedding(currentPersona);
|
|
243
|
+
if (embedding) {
|
|
244
|
+
sm.persona_update(personaId, { description_embedding: embedding });
|
|
245
|
+
currentPersona = { ...currentPersona, description_embedding: embedding };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (currentPersona.description_embedding) {
|
|
250
|
+
const lastAsked = currentPersona.reflection_last_asked
|
|
251
|
+
? new Date(currentPersona.reflection_last_asked).getTime()
|
|
252
|
+
: 0;
|
|
253
|
+
|
|
254
|
+
// Gate: person must have been updated at least 1 week AFTER reflection was last asked.
|
|
255
|
+
// This handles both the cooldown AND the extraction echo — the ceremony extraction
|
|
256
|
+
// that fires right after a persona surfaces drift updates last_updated by minutes,
|
|
257
|
+
// which can never satisfy the 1-week offset requirement.
|
|
258
|
+
if (new Date(personRecord.last_updated).getTime() > lastAsked + REFLECTION_COOLDOWN_MS) {
|
|
259
|
+
const similarity = cosineSimilarity(personRecord.embedding, currentPersona.description_embedding);
|
|
260
|
+
if (similarity < REFLECTION_SIMILARITY_THRESHOLD) {
|
|
261
|
+
driftContext = {
|
|
262
|
+
people_description: personRecord.description ?? '',
|
|
263
|
+
persona_description: currentPersona.long_description ?? '',
|
|
264
|
+
};
|
|
265
|
+
console.log(`[HeartbeatCheck ${persona.display_name}] Drift detected (similarity: ${similarity.toFixed(3)}) - including reflection context`);
|
|
266
|
+
} else {
|
|
267
|
+
console.log(`[HeartbeatCheck ${persona.display_name}] Person updated but no drift (similarity: ${similarity.toFixed(3)})`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
216
273
|
const promptData: HeartbeatCheckPromptData = {
|
|
217
274
|
persona: {
|
|
218
275
|
name: persona.display_name,
|
|
@@ -225,6 +282,7 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
|
|
|
225
282
|
},
|
|
226
283
|
recent_history: contextHistory.slice(-10),
|
|
227
284
|
inactive_days: inactiveDays,
|
|
285
|
+
drift_context: driftContext,
|
|
228
286
|
};
|
|
229
287
|
|
|
230
288
|
const prompt = buildHeartbeatCheckPrompt(promptData);
|
package/src/core/llm-client.ts
CHANGED
|
@@ -337,7 +337,7 @@ export async function callLLMRaw(
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
let finalToolCalls = rawToolCalls;
|
|
340
|
-
if ((!rawToolCalls || rawToolCalls.length === 0) && choice?.finish_reason === "stop" && typeof textContent === "string") {
|
|
340
|
+
if ((!rawToolCalls || rawToolCalls.length === 0) && choice?.finish_reason === "stop" && typeof textContent === "string" && textContent.trimStart().startsWith("<|tool_call>")) {
|
|
341
341
|
const rescued = rescueGemmaToolCalls(textContent);
|
|
342
342
|
if (rescued.length > 0) {
|
|
343
343
|
console.log(`[LLM] Rescued ${rescued.length} tool call(s) from content (Gemma native format)`);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type LLMRequest,
|
|
9
9
|
} from "./types.js";
|
|
10
10
|
import { formatTimestamp } from "./format-utils.js";
|
|
11
|
+
import { getMessageContent } from "./handlers/utils.js";
|
|
11
12
|
import { StateManager } from "./state-manager.js";
|
|
12
13
|
import { QueueProcessor } from "./queue-processor.js";
|
|
13
14
|
import {
|
|
@@ -113,7 +114,7 @@ export async function recallPendingMessages(
|
|
|
113
114
|
.map((m) => m.id);
|
|
114
115
|
if (pendingIds.length === 0) return "";
|
|
115
116
|
const removed = sm.messages_remove(personaId, pendingIds);
|
|
116
|
-
const recalledContent = removed.map((m) => m
|
|
117
|
+
const recalledContent = removed.map((m) => getMessageContent(m)).join("\n\n");
|
|
117
118
|
onMessageAdded(personaId);
|
|
118
119
|
onMessageRecalled(personaId, recalledContent);
|
|
119
120
|
return recalledContent;
|
|
@@ -164,7 +165,7 @@ export async function sendMessage(
|
|
|
164
165
|
const prompt = buildResponsePrompt(promptData);
|
|
165
166
|
|
|
166
167
|
sm.queue_enqueue({
|
|
167
|
-
type: LLMRequestType.
|
|
168
|
+
type: LLMRequestType.Raw,
|
|
168
169
|
priority: LLMPriority.High,
|
|
169
170
|
system: prompt.system,
|
|
170
171
|
user: prompt.user,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { LLMRequestType, LLMPriority, LLMNextStep, type CeremonyConfig, type PersonaTopic, type Topic, type
|
|
1
|
+
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, type CeremonyConfig, type PersonaTopic, type Topic, type DataItemBase } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
|
+
import { normalizeRoomMessages } from "../handlers/utils.js";
|
|
3
4
|
import { applyDecayToValue } from "../utils/index.js";
|
|
4
5
|
import {
|
|
5
6
|
queueFactFind,
|
|
@@ -9,9 +10,9 @@ import {
|
|
|
9
10
|
type ExtractionContext,
|
|
10
11
|
type ExtractionOptions,
|
|
11
12
|
} from "./human-extraction.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { queuePersonaTopicRating, type PersonaTopicContext, type PersonaTopicOptions } from "./persona-topics.js";
|
|
14
|
+
import { queuePersonMigration } from "./person-migration.js";
|
|
15
|
+
import { buildRewriteScanPrompt, type RewriteItemType } from "../../prompts/ceremony/index.js";
|
|
15
16
|
|
|
16
17
|
export function isNewDay(lastCeremony: string | undefined, now: Date): boolean {
|
|
17
18
|
if (!lastCeremony) return true;
|
|
@@ -70,14 +71,14 @@ export function startCeremony(state: StateManager): void {
|
|
|
70
71
|
},
|
|
71
72
|
});
|
|
72
73
|
|
|
73
|
-
// PHASE 1:
|
|
74
|
-
console.log("[ceremony] Starting Phase 1:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Check if
|
|
74
|
+
// PHASE 1: Person Migration
|
|
75
|
+
console.log("[ceremony] Starting Phase 1: Person Migration");
|
|
76
|
+
queuePersonMigration(state);
|
|
77
|
+
|
|
78
|
+
// Check if migration work was queued
|
|
78
79
|
if (!state.queue_hasPendingCeremonies()) {
|
|
79
|
-
// No
|
|
80
|
-
console.log("[ceremony] No
|
|
80
|
+
// No migration work found → immediately advance to Expose phase
|
|
81
|
+
console.log("[ceremony] No migration work, advancing to Expose phase");
|
|
81
82
|
handleCeremonyProgress(state, 1);
|
|
82
83
|
}
|
|
83
84
|
|
|
@@ -142,16 +143,21 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
|
|
|
142
143
|
console.log(`[ceremony:exposure] Queued human extraction scans (f:${unextractedFacts.length}, t:${unextractedTopics.length}, p:${unextractedPeople.length})`);
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
const
|
|
146
|
-
|
|
146
|
+
const human = state.getHuman();
|
|
147
|
+
const lastCeremony = human.settings?.ceremony?.last_ceremony;
|
|
148
|
+
const shortId = personaId.slice(0, 8);
|
|
149
|
+
const forPersonaTopics = state.messages_getUnextractedForPersona(personaId, shortId, lastCeremony ?? undefined);
|
|
150
|
+
if (forPersonaTopics.length > 0) {
|
|
147
151
|
const personaTopicContext: PersonaTopicContext = {
|
|
148
152
|
personaId,
|
|
149
153
|
personaDisplayName: persona.display_name,
|
|
150
|
-
messages_context: allMessages.filter(m => m.
|
|
151
|
-
messages_analyze:
|
|
154
|
+
messages_context: allMessages.filter(m => !!m.persona_extracted?.[shortId]),
|
|
155
|
+
messages_analyze: forPersonaTopics,
|
|
156
|
+
topics: persona.topics,
|
|
152
157
|
};
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
const personaTopicOptions: PersonaTopicOptions = { ceremony_progress: options?.ceremony_progress };
|
|
159
|
+
queuePersonaTopicRating(personaTopicContext, state, personaTopicOptions);
|
|
160
|
+
console.log(`[ceremony:exposure] Queued persona topic rating for ${persona.display_name} (${forPersonaTopics.length} messages)`);
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
|
|
@@ -194,6 +200,32 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
194
200
|
for (const persona of personasWithUnprocessed) {
|
|
195
201
|
queueExposurePhase(persona.id, state, options);
|
|
196
202
|
}
|
|
203
|
+
|
|
204
|
+
const rooms = state.getRoomList();
|
|
205
|
+
for (const room of rooms) {
|
|
206
|
+
if (room.mode === RoomMode.ChooseYourPath) continue;
|
|
207
|
+
for (const personaId of room.persona_ids) {
|
|
208
|
+
const shortId = personaId.slice(0, 8);
|
|
209
|
+
const unprocessedRaw = state.getRoomUnextractedMessagesForPersona(room.id, shortId);
|
|
210
|
+
if (unprocessedRaw.length === 0) continue;
|
|
211
|
+
const personaForRoom = state.persona_getById(personaId);
|
|
212
|
+
if (!personaForRoom) continue;
|
|
213
|
+
const allRoomMessagesRaw = state.getRoomActivePath(room.id);
|
|
214
|
+
const processedIds = new Set(allRoomMessagesRaw.filter(m => !!m.persona_extracted?.[shortId]).map(m => m.id));
|
|
215
|
+
const allNormalized = normalizeRoomMessages(allRoomMessagesRaw, state);
|
|
216
|
+
const unprocessedNormalized = normalizeRoomMessages(unprocessedRaw, state);
|
|
217
|
+
const personaTopicContext: PersonaTopicContext = {
|
|
218
|
+
personaId,
|
|
219
|
+
personaDisplayName: personaForRoom.display_name,
|
|
220
|
+
messages_context: allNormalized.filter(m => processedIds.has(m.id)),
|
|
221
|
+
messages_analyze: unprocessedNormalized,
|
|
222
|
+
topics: personaForRoom.topics,
|
|
223
|
+
};
|
|
224
|
+
const roomScanOptions: PersonaTopicOptions = { ceremony_progress: 2, roomId: room.id };
|
|
225
|
+
queuePersonaTopicRating(personaTopicContext, state, roomScanOptions);
|
|
226
|
+
console.log(`[ceremony:expose] Queued room persona topic rating: ${personaForRoom.display_name} in "${room.display_name}" (${unprocessedRaw.length} messages)`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
197
229
|
return;
|
|
198
230
|
}
|
|
199
231
|
|
|
@@ -238,16 +270,9 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
238
270
|
runHumanCeremony(state);
|
|
239
271
|
|
|
240
272
|
// Rewrite phase: fire-and-forget scans for bloated human data items
|
|
241
|
-
// No ceremony_progress gating — Expire/Explore only touch persona topics, zero overlap
|
|
242
273
|
queueRewritePhase(state);
|
|
243
274
|
|
|
244
|
-
|
|
245
|
-
// handlePersonaExpire already chains to Explore → DescriptionCheck
|
|
246
|
-
for (const persona of activePersonas) {
|
|
247
|
-
queueExpirePhase(persona.id, state);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
console.log("[ceremony:progress] Ceremony Decay complete, Expire queued");
|
|
275
|
+
console.log("[ceremony:progress] Ceremony Decay complete");
|
|
251
276
|
}
|
|
252
277
|
|
|
253
278
|
// =============================================================================
|
|
@@ -334,124 +359,6 @@ export function prunePersonaMessages(personaId: string, state: StateManager): vo
|
|
|
334
359
|
}
|
|
335
360
|
}
|
|
336
361
|
|
|
337
|
-
// =============================================================================
|
|
338
|
-
// EXPIRE PHASE (queues LLM calls)
|
|
339
|
-
// =============================================================================
|
|
340
|
-
|
|
341
|
-
export function queueExpirePhase(personaId: string, state: StateManager): void {
|
|
342
|
-
const persona = state.persona_getById(personaId);
|
|
343
|
-
if (!persona) {
|
|
344
|
-
console.error(`[ceremony:expire] Persona not found: ${personaId}`);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
console.log(`[ceremony:expire] Queueing for ${persona.display_name}`);
|
|
349
|
-
|
|
350
|
-
if (persona.topics.length === 0) {
|
|
351
|
-
console.log(`[ceremony:expire] ${persona.display_name} has no topics, skipping to description check`);
|
|
352
|
-
queueDescriptionCheck(personaId, state);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const prompt = buildPersonaExpirePrompt({
|
|
357
|
-
persona_name: persona.display_name,
|
|
358
|
-
topics: persona.topics,
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
state.queue_enqueue({
|
|
362
|
-
type: LLMRequestType.JSON,
|
|
363
|
-
priority: LLMPriority.Low,
|
|
364
|
-
system: prompt.system,
|
|
365
|
-
user: prompt.user,
|
|
366
|
-
next_step: LLMNextStep.HandlePersonaExpire,
|
|
367
|
-
data: { personaId, personaDisplayName: persona.display_name },
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// =============================================================================
|
|
372
|
-
// EXPLORE PHASE (queues LLM calls — chained from handlePersonaExpire in handlers)
|
|
373
|
-
// =============================================================================
|
|
374
|
-
|
|
375
|
-
export function queueExplorePhase(personaId: string, state: StateManager): void {
|
|
376
|
-
const persona = state.persona_getById(personaId);
|
|
377
|
-
if (!persona) {
|
|
378
|
-
console.error(`[ceremony:explore] Persona not found: ${personaId}`);
|
|
379
|
-
queueDescriptionCheck(personaId, state);
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
console.log(`[ceremony:explore] Queueing for ${persona.display_name}`);
|
|
384
|
-
|
|
385
|
-
const messages = state.messages_get(personaId);
|
|
386
|
-
const recentMessages = messages.slice(-20);
|
|
387
|
-
const themes = extractConversationThemes(recentMessages);
|
|
388
|
-
|
|
389
|
-
const prompt = buildPersonaExplorePrompt({
|
|
390
|
-
persona_name: persona.display_name,
|
|
391
|
-
traits: persona.traits,
|
|
392
|
-
remaining_topics: persona.topics,
|
|
393
|
-
recent_conversation_themes: themes,
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
state.queue_enqueue({
|
|
397
|
-
type: LLMRequestType.JSON,
|
|
398
|
-
priority: LLMPriority.Low,
|
|
399
|
-
system: prompt.system,
|
|
400
|
-
user: prompt.user,
|
|
401
|
-
next_step: LLMNextStep.HandlePersonaExplore,
|
|
402
|
-
data: { personaId, personaDisplayName: persona.display_name },
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function extractConversationThemes(messages: Message[]): string[] {
|
|
407
|
-
const humanMessages = messages.filter(m => m.role === "human");
|
|
408
|
-
if (humanMessages.length === 0) return [];
|
|
409
|
-
|
|
410
|
-
const words = humanMessages
|
|
411
|
-
.map(m => (m.verbal_response ?? '').toLowerCase())
|
|
412
|
-
.join(" ")
|
|
413
|
-
.split(/\s+/)
|
|
414
|
-
.filter(w => w.length > 4);
|
|
415
|
-
|
|
416
|
-
const frequency: Record<string, number> = {};
|
|
417
|
-
for (const word of words) {
|
|
418
|
-
frequency[word] = (frequency[word] || 0) + 1;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
return Object.entries(frequency)
|
|
422
|
-
.filter(([_, count]) => count >= 2)
|
|
423
|
-
.sort((a, b) => b[1] - a[1])
|
|
424
|
-
.slice(0, 5)
|
|
425
|
-
.map(([word]) => word);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export function queueDescriptionCheck(personaId: string, state: StateManager): void {
|
|
429
|
-
const persona = state.persona_getById(personaId);
|
|
430
|
-
if (!persona) {
|
|
431
|
-
console.error(`[ceremony:description] Persona not found: ${personaId}`);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
console.log(`[ceremony:description] Queueing for ${persona.display_name}`);
|
|
436
|
-
|
|
437
|
-
const prompt = buildDescriptionCheckPrompt({
|
|
438
|
-
persona_name: persona.display_name,
|
|
439
|
-
current_short_description: persona.short_description,
|
|
440
|
-
current_long_description: persona.long_description,
|
|
441
|
-
traits: persona.traits,
|
|
442
|
-
topics: persona.topics,
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
state.queue_enqueue({
|
|
446
|
-
type: LLMRequestType.JSON,
|
|
447
|
-
priority: LLMPriority.Low,
|
|
448
|
-
system: prompt.system,
|
|
449
|
-
user: prompt.user,
|
|
450
|
-
next_step: LLMNextStep.HandleDescriptionCheck,
|
|
451
|
-
data: { personaId, personaDisplayName: persona.display_name },
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
362
|
// =============================================================================
|
|
456
363
|
// HUMAN CEREMONY (synchronous — runs during Decay phase)
|
|
457
364
|
// =============================================================================
|
|
@@ -550,7 +457,10 @@ export function queueRewritePhase(state: StateManager): void {
|
|
|
550
457
|
}
|
|
551
458
|
}
|
|
552
459
|
for (const person of human.people) {
|
|
553
|
-
|
|
460
|
+
const isPersonaLinked = (person.identifiers ?? []).some(
|
|
461
|
+
i => i.type.toLowerCase() === 'ei persona'
|
|
462
|
+
);
|
|
463
|
+
if (!isPersonaLinked && (person.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD && !person.rewrite_checked) {
|
|
554
464
|
itemsToScan.push({ item: person, type: "person" });
|
|
555
465
|
}
|
|
556
466
|
}
|
|
@@ -1,208 +1,9 @@
|
|
|
1
1
|
import { StateManager } from "../state-manager.js";
|
|
2
2
|
import { LLMRequestType, LLMPriority, LLMNextStep, type DataItemBase } from "../types.js";
|
|
3
|
-
import type { DataItemType } from "../types/data-items.js";
|
|
4
|
-
import { buildDedupPrompt } from "../../prompts/ceremony/dedup.js";
|
|
5
3
|
import { buildUserDedupPrompt } from "../../prompts/ceremony/user-dedup.js";
|
|
6
4
|
|
|
7
|
-
// =============================================================================
|
|
8
|
-
// TYPES
|
|
9
|
-
// =============================================================================
|
|
10
|
-
|
|
11
5
|
type DedupableItem = DataItemBase & { relationship?: string };
|
|
12
6
|
|
|
13
|
-
interface Cluster {
|
|
14
|
-
ids: string[];
|
|
15
|
-
minSim: number;
|
|
16
|
-
maxSim: number;
|
|
17
|
-
size: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// =============================================================================
|
|
21
|
-
// DEDUP CANDIDATE FINDING (copied from ceremony.ts)
|
|
22
|
-
// =============================================================================
|
|
23
|
-
|
|
24
|
-
const DEDUP_DEFAULT_THRESHOLD = 0.95; // Raised from 0.90: Ei's topic corpus is a single dense project domain — mega-cluster persists all the way to 0.92. At 0.95, max cluster drops to 7 items.
|
|
25
|
-
|
|
26
|
-
function findDedupCandidates<T extends DedupableItem>(
|
|
27
|
-
items: T[],
|
|
28
|
-
threshold: number
|
|
29
|
-
): Array<{ a: T; b: T; similarity: number }> {
|
|
30
|
-
const withEmbeddings = items.filter(item =>
|
|
31
|
-
item.embedding && item.embedding.length > 0 &&
|
|
32
|
-
item.relationship !== "Persona"
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const candidates: Array<{ a: T; b: T; similarity: number }> = [];
|
|
36
|
-
|
|
37
|
-
for (let i = 0; i < withEmbeddings.length; i++) {
|
|
38
|
-
for (let j = i + 1; j < withEmbeddings.length; j++) {
|
|
39
|
-
const a = withEmbeddings[i];
|
|
40
|
-
const b = withEmbeddings[j];
|
|
41
|
-
const dot = a.embedding!.reduce((sum, v, k) => sum + v * b.embedding![k], 0);
|
|
42
|
-
const normA = Math.sqrt(a.embedding!.reduce((sum, v) => sum + v * v, 0));
|
|
43
|
-
const normB = Math.sqrt(b.embedding!.reduce((sum, v) => sum + v * v, 0));
|
|
44
|
-
const similarity = normA && normB ? dot / (normA * normB) : 0;
|
|
45
|
-
|
|
46
|
-
if (similarity >= threshold) {
|
|
47
|
-
candidates.push({ a, b, similarity });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return candidates.sort((x, y) => y.similarity - x.similarity);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// =============================================================================
|
|
56
|
-
// UNION-FIND CLUSTERING
|
|
57
|
-
// =============================================================================
|
|
58
|
-
|
|
59
|
-
function clusterPairs<T extends DedupableItem>(
|
|
60
|
-
pairs: Array<{ a: T; b: T; similarity: number }>
|
|
61
|
-
): Cluster[] {
|
|
62
|
-
const parent = new Map<string, string>();
|
|
63
|
-
|
|
64
|
-
function find(x: string): string {
|
|
65
|
-
if (!parent.has(x)) parent.set(x, x);
|
|
66
|
-
if (parent.get(x) !== x) parent.set(x, find(parent.get(x)!));
|
|
67
|
-
return parent.get(x)!;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function union(x: string, y: string): void {
|
|
71
|
-
const px = find(x), py = find(y);
|
|
72
|
-
if (px !== py) parent.set(px, py);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Union all pairs
|
|
76
|
-
for (const pair of pairs) {
|
|
77
|
-
union(pair.a.id, pair.b.id);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Group by root to create clusters
|
|
81
|
-
const clusters = new Map<string, { ids: string[]; sims: number[] }>();
|
|
82
|
-
for (const pair of pairs) {
|
|
83
|
-
const root = find(pair.a.id);
|
|
84
|
-
if (!clusters.has(root)) {
|
|
85
|
-
clusters.set(root, { ids: [], sims: [] });
|
|
86
|
-
}
|
|
87
|
-
const cluster = clusters.get(root)!;
|
|
88
|
-
if (!cluster.ids.includes(pair.a.id)) cluster.ids.push(pair.a.id);
|
|
89
|
-
if (!cluster.ids.includes(pair.b.id)) cluster.ids.push(pair.b.id);
|
|
90
|
-
cluster.sims.push(pair.similarity);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Convert to Cluster objects
|
|
94
|
-
return Array.from(clusters.values()).map(c => ({
|
|
95
|
-
ids: c.ids,
|
|
96
|
-
minSim: Math.min(...c.sims),
|
|
97
|
-
maxSim: Math.max(...c.sims),
|
|
98
|
-
size: c.ids.length
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// =============================================================================
|
|
103
|
-
// QUALITY GATES
|
|
104
|
-
// =============================================================================
|
|
105
|
-
|
|
106
|
-
function filterClusters(clusters: Cluster[]): Cluster[] {
|
|
107
|
-
return clusters
|
|
108
|
-
.filter(c => {
|
|
109
|
-
if (c.size > 50) {
|
|
110
|
-
console.warn(`[Dedup] Cluster rejected (size too large): ${c.size} items`);
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
return true;
|
|
114
|
-
})
|
|
115
|
-
.filter(c => {
|
|
116
|
-
const spread = c.maxSim - c.minSim;
|
|
117
|
-
if (spread > 0.10) { // 10% threshold
|
|
118
|
-
console.warn(`[Dedup] Cluster rejected (high spread): ${spread.toFixed(3)} range`);
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
return true;
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// =============================================================================
|
|
126
|
-
// MAIN QUEUEING FUNCTION
|
|
127
|
-
// =============================================================================
|
|
128
|
-
|
|
129
|
-
export function queueDedupPhase(state: StateManager): void {
|
|
130
|
-
const human = state.getHuman();
|
|
131
|
-
const rewriteModel = human.settings?.rewrite_model;
|
|
132
|
-
|
|
133
|
-
if (!rewriteModel) {
|
|
134
|
-
console.log("[Dedup] rewrite_model not set — skipping dedup phase");
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const threshold = human.settings?.ceremony?.dedup_threshold ?? DEDUP_DEFAULT_THRESHOLD;
|
|
139
|
-
|
|
140
|
-
console.log(`[Dedup] Starting deduplication phase (threshold: ${threshold})`);
|
|
141
|
-
|
|
142
|
-
const entityTypes: Array<{ type: DataItemType; items: DedupableItem[] }> = [
|
|
143
|
-
{ type: "topic", items: human.topics },
|
|
144
|
-
{ type: "person", items: human.people },
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
let totalClusters = 0;
|
|
148
|
-
|
|
149
|
-
for (const { type, items } of entityTypes) {
|
|
150
|
-
// Find dedup candidates
|
|
151
|
-
const pairs = findDedupCandidates(items, threshold);
|
|
152
|
-
|
|
153
|
-
if (pairs.length === 0) {
|
|
154
|
-
console.log(`[Dedup] ${type}: No duplicates found`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Cluster pairs via union-find
|
|
159
|
-
const clusters = clusterPairs(pairs);
|
|
160
|
-
|
|
161
|
-
// Apply quality gates
|
|
162
|
-
const vettedClusters = filterClusters(clusters);
|
|
163
|
-
|
|
164
|
-
console.log(`[Dedup] ${type}: ${pairs.length} pairs → ${clusters.length} clusters → ${vettedClusters.length} vetted`);
|
|
165
|
-
|
|
166
|
-
// Queue Opus curation for each vetted cluster
|
|
167
|
-
for (const cluster of vettedClusters) {
|
|
168
|
-
// Hydrate cluster with full entity data
|
|
169
|
-
const clusterEntities = cluster.ids
|
|
170
|
-
.map(id => items.find(item => item.id === id))
|
|
171
|
-
.filter((item): item is DedupableItem => item !== undefined);
|
|
172
|
-
|
|
173
|
-
if (clusterEntities.length === 0) {
|
|
174
|
-
console.warn(`[Dedup] Cluster hydration failed - no entities found`);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Build prompt
|
|
179
|
-
const prompt = buildDedupPrompt({
|
|
180
|
-
cluster: clusterEntities,
|
|
181
|
-
itemType: type as "topic" | "person",
|
|
182
|
-
similarityRange: { min: cluster.minSim, max: cluster.maxSim }
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Queue LLM request
|
|
186
|
-
state.queue_enqueue({
|
|
187
|
-
type: LLMRequestType.JSON,
|
|
188
|
-
priority: LLMPriority.Normal,
|
|
189
|
-
system: prompt.system,
|
|
190
|
-
user: prompt.user,
|
|
191
|
-
next_step: LLMNextStep.HandleDedupCurate,
|
|
192
|
-
model: rewriteModel,
|
|
193
|
-
data: {
|
|
194
|
-
entity_type: type,
|
|
195
|
-
entity_ids: cluster.ids,
|
|
196
|
-
ceremony_progress: 1
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
totalClusters++;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
console.log(`[Dedup] Queued ${totalClusters} clusters for curation`);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
7
|
// =============================================================================
|
|
207
8
|
// USER-TRIGGERED DEDUP
|
|
208
9
|
// =============================================================================
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Message } from "../types.js";
|
|
2
2
|
import type { ExtractionContext } from "./human-extraction.js";
|
|
3
|
+
import { getMessageContent } from "../handlers/utils.js";
|
|
4
|
+
import { getMessageDisplayText } from "../../prompts/message-utils.js";
|
|
3
5
|
|
|
4
6
|
const DEFAULT_MAX_TOKENS = 10000;
|
|
5
7
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -12,7 +14,10 @@ function estimateTokens(text: string): number {
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
function estimateMessageTokens(messages: Message[]): number {
|
|
15
|
-
return messages.reduce((sum, msg) =>
|
|
17
|
+
return messages.reduce((sum, msg) => {
|
|
18
|
+
const text = getMessageDisplayText(msg) ?? getMessageContent(msg);
|
|
19
|
+
return sum + estimateTokens(text) + 4;
|
|
20
|
+
}, 0);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
function fitMessagesFromEnd(messages: Message[], maxTokens: number): Message[] {
|
|
@@ -20,7 +25,7 @@ function fitMessagesFromEnd(messages: Message[], maxTokens: number): Message[] {
|
|
|
20
25
|
let tokens = 0;
|
|
21
26
|
|
|
22
27
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
23
|
-
const msgTokens = estimateTokens(messages[i]
|
|
28
|
+
const msgTokens = estimateTokens(getMessageContent(messages[i])) + 4;
|
|
24
29
|
if (tokens + msgTokens > maxTokens) break;
|
|
25
30
|
result.unshift(messages[i]);
|
|
26
31
|
tokens += msgTokens;
|
|
@@ -39,7 +44,7 @@ function pullMessagesFromStart(
|
|
|
39
44
|
let i = startIndex;
|
|
40
45
|
|
|
41
46
|
while (i < messages.length) {
|
|
42
|
-
const msgTokens = estimateTokens(messages[i]
|
|
47
|
+
const msgTokens = estimateTokens(getMessageContent(messages[i])) + 4;
|
|
43
48
|
if (tokens + msgTokens > maxTokens && pulled.length > 0) break;
|
|
44
49
|
pulled.push(messages[i]);
|
|
45
50
|
tokens += msgTokens;
|