ei-tui 0.4.3 → 0.5.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/README.md +14 -0
- package/package.json +1 -1
- package/src/cli/README.md +17 -12
- package/src/cli/commands/personas.ts +12 -0
- package/src/cli/mcp.ts +2 -2
- package/src/cli/retrieval.ts +86 -8
- package/src/cli.ts +8 -5
- package/src/core/constants/seed-traits.ts +29 -0
- package/src/core/context-utils.ts +1 -0
- package/src/core/handlers/human-matching.ts +86 -56
- package/src/core/handlers/index.ts +5 -0
- package/src/core/handlers/persona-preview.ts +7 -0
- package/src/core/handlers/persona-topics.ts +3 -2
- package/src/core/handlers/rooms.ts +176 -0
- package/src/core/handlers/utils.ts +55 -3
- package/src/core/heartbeat-manager.ts +3 -1
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +10 -8
- package/src/core/orchestrators/human-extraction.ts +15 -2
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/orchestrators/persona-generation.ts +4 -0
- package/src/core/orchestrators/persona-topics.ts +2 -1
- package/src/core/orchestrators/room-extraction.ts +318 -0
- package/src/core/persona-manager.ts +16 -5
- package/src/core/personas/opencode-agent.ts +12 -2
- package/src/core/processor.ts +520 -4
- package/src/core/prompt-context-builder.ts +89 -5
- package/src/core/queue-processor.ts +68 -8
- package/src/core/room-manager.ts +408 -0
- package/src/core/state/index.ts +1 -0
- package/src/core/state/personas.ts +12 -2
- package/src/core/state/queue.ts +2 -2
- package/src/core/state/rooms.ts +182 -0
- package/src/core/state-manager.ts +124 -2
- package/src/core/tool-manager.ts +1 -1
- package/src/core/tools/index.ts +15 -0
- package/src/core/types/data-items.ts +3 -1
- package/src/core/types/enums.ts +11 -0
- package/src/core/types/integrations.ts +10 -2
- package/src/core/types/llm.ts +3 -0
- package/src/core/types/rooms.ts +59 -0
- package/src/core/types.ts +1 -0
- package/src/core/utils/decay.ts +14 -8
- package/src/core/utils/exposure.ts +14 -0
- package/src/integrations/claude-code/importer.ts +23 -10
- package/src/integrations/cursor/importer.ts +22 -10
- package/src/integrations/opencode/importer.ts +30 -13
- package/src/prompts/ceremony/dedup.ts +2 -2
- package/src/prompts/generation/from-person.ts +85 -0
- package/src/prompts/generation/index.ts +2 -0
- package/src/prompts/generation/persona.ts +14 -10
- package/src/prompts/generation/seeds.ts +4 -29
- package/src/prompts/generation/types.ts +13 -0
- package/src/prompts/heartbeat/check.ts +1 -1
- package/src/prompts/heartbeat/ei.ts +4 -4
- package/src/prompts/heartbeat/types.ts +1 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/message-utils.ts +2 -2
- package/src/prompts/persona/topics-match.ts +7 -6
- package/src/prompts/persona/topics-update.ts +8 -11
- package/src/prompts/persona/types.ts +2 -1
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +20 -8
- package/src/prompts/response/types.ts +6 -0
- package/src/prompts/room/index.ts +115 -0
- package/src/prompts/room/sections.ts +150 -0
- package/src/prompts/room/types.ts +93 -0
- package/tui/README.md +20 -0
- package/tui/src/app.tsx +3 -2
- package/tui/src/commands/activate.tsx +98 -0
- package/tui/src/commands/archive.tsx +54 -25
- package/tui/src/commands/capture.tsx +50 -0
- package/tui/src/commands/dedupe.tsx +2 -7
- package/tui/src/commands/delete.tsx +48 -0
- package/tui/src/commands/details.tsx +7 -0
- package/tui/src/commands/persona.tsx +271 -9
- package/tui/src/commands/room.tsx +261 -0
- package/tui/src/commands/silence.tsx +29 -0
- package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
- package/tui/src/components/ConfirmOverlay.tsx +6 -0
- package/tui/src/components/ConflictOverlay.tsx +6 -0
- package/tui/src/components/HelpOverlay.tsx +6 -1
- package/tui/src/components/LoadingOverlay.tsx +51 -0
- package/tui/src/components/MessageList.tsx +1 -18
- package/tui/src/components/PersonPickerOverlay.tsx +121 -0
- package/tui/src/components/PersonaListOverlay.tsx +6 -1
- package/tui/src/components/PromptInput.tsx +141 -8
- package/tui/src/components/ProviderListOverlay.tsx +5 -1
- package/tui/src/components/QuotesOverlay.tsx +5 -1
- package/tui/src/components/RoomMessageList.tsx +179 -0
- package/tui/src/components/Sidebar.tsx +54 -2
- package/tui/src/components/StatusBar.tsx +99 -8
- package/tui/src/components/ToolkitListOverlay.tsx +5 -1
- package/tui/src/components/WelcomeOverlay.tsx +6 -0
- package/tui/src/context/ei.tsx +252 -1
- package/tui/src/context/keyboard.tsx +48 -12
- package/tui/src/util/cyp-editor.tsx +152 -0
- package/tui/src/util/quote-utils.ts +19 -0
- package/tui/src/util/room-editor.tsx +164 -0
- package/tui/src/util/room-logic.ts +8 -0
- package/tui/src/util/room-parser.ts +70 -0
- package/tui/src/util/yaml-serializers.ts +151 -0
|
@@ -9,15 +9,10 @@ import type { StateManager } from "../state-manager.js";
|
|
|
9
9
|
import type { ItemMatchResult, ExposureImpact, TopicUpdateResult, PersonUpdateResult } from "../../prompts/human/types.js";
|
|
10
10
|
import { queueTopicUpdate, queuePersonUpdate, type ExtractionContext } from "../orchestrators/index.js";
|
|
11
11
|
import { getEmbeddingService, getTopicEmbeddingText, getPersonEmbeddingText } from "../embedding-service.js";
|
|
12
|
+
import { calculateExposureCurrent } from "../utils/exposure.js";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (isNewItem) return [personaGroup];
|
|
16
|
-
const groups = new Set(existing ?? []);
|
|
17
|
-
groups.add(personaGroup);
|
|
18
|
-
return Array.from(groups);
|
|
19
|
-
}
|
|
20
|
-
import { resolveMessageWindow, getMessageText } from "./utils.js";
|
|
14
|
+
|
|
15
|
+
import { resolveMessageWindow, getMessageText, normalizeRoomMessages } from "./utils.js";
|
|
21
16
|
|
|
22
17
|
export function handleTopicMatch(response: LLMResponse, state: StateManager): void {
|
|
23
18
|
const result = response.parsed as ItemMatchResult | undefined;
|
|
@@ -28,6 +23,7 @@ export function handleTopicMatch(response: LLMResponse, state: StateManager): vo
|
|
|
28
23
|
|
|
29
24
|
const personaId = response.request.data.personaId as string;
|
|
30
25
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
26
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
31
27
|
const { messages_context, messages_analyze } = resolveMessageWindow(response, state);
|
|
32
28
|
|
|
33
29
|
let matched_guid = result.matched_guid;
|
|
@@ -51,6 +47,7 @@ export function handleTopicMatch(response: LLMResponse, state: StateManager): vo
|
|
|
51
47
|
} = {
|
|
52
48
|
personaId,
|
|
53
49
|
personaDisplayName,
|
|
50
|
+
roomId,
|
|
54
51
|
messages_context,
|
|
55
52
|
messages_analyze,
|
|
56
53
|
candidateName: response.request.data.candidateName as string,
|
|
@@ -73,6 +70,7 @@ export function handlePersonMatch(response: LLMResponse, state: StateManager): v
|
|
|
73
70
|
|
|
74
71
|
const personaId = response.request.data.personaId as string;
|
|
75
72
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
73
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
76
74
|
const { messages_context, messages_analyze } = resolveMessageWindow(response, state);
|
|
77
75
|
|
|
78
76
|
let matched_guid = result.matched_guid;
|
|
@@ -96,6 +94,7 @@ export function handlePersonMatch(response: LLMResponse, state: StateManager): v
|
|
|
96
94
|
} = {
|
|
97
95
|
personaId,
|
|
98
96
|
personaDisplayName,
|
|
97
|
+
roomId,
|
|
99
98
|
messages_context,
|
|
100
99
|
messages_analyze,
|
|
101
100
|
candidateName: response.request.data.candidateName as string,
|
|
@@ -121,6 +120,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
121
120
|
const existingItemId = response.request.data.existingItemId as string | undefined;
|
|
122
121
|
const personaId = response.request.data.personaId as string;
|
|
123
122
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
123
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
124
124
|
const candidateCategory = response.request.data.candidateCategory as string | undefined;
|
|
125
125
|
|
|
126
126
|
if (!result.name || !result.description || result.sentiment === undefined) {
|
|
@@ -128,6 +128,9 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const personaIds = personaId.split("|").filter(Boolean);
|
|
132
|
+
const primaryId = personaIds[0] ?? personaId;
|
|
133
|
+
|
|
131
134
|
const now = new Date().toISOString();
|
|
132
135
|
const human = state.getHuman();
|
|
133
136
|
|
|
@@ -137,8 +140,11 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
137
140
|
};
|
|
138
141
|
const itemId = resolveItemId();
|
|
139
142
|
|
|
140
|
-
const persona = state.persona_getById(
|
|
143
|
+
const persona = state.persona_getById(primaryId);
|
|
141
144
|
const personaGroup = persona?.group_primary ?? null;
|
|
145
|
+
const allPersonaGroups = personaIds
|
|
146
|
+
.map(id => state.persona_getById(id)?.group_primary)
|
|
147
|
+
.filter((g): g is string => g != null);
|
|
142
148
|
|
|
143
149
|
const existingTopic = isNewItem ? undefined : human.topics.find(t => t.id === existingItemId);
|
|
144
150
|
|
|
@@ -153,27 +159,34 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
const exposureImpact = result.exposure_impact as ExposureImpact | undefined;
|
|
162
|
+
const interestedPersonas = isNewItem
|
|
163
|
+
? personaIds
|
|
164
|
+
: [...new Set([...(existingTopic?.interested_personas ?? []), ...personaIds])];
|
|
165
|
+
const personaGroupsMerged = isNewItem
|
|
166
|
+
? (allPersonaGroups.length > 0 ? allPersonaGroups : existingTopic?.persona_groups)
|
|
167
|
+
: [...new Set([...(existingTopic?.persona_groups ?? []), ...allPersonaGroups])];
|
|
168
|
+
|
|
156
169
|
const topic: Topic = {
|
|
157
170
|
id: itemId,
|
|
158
171
|
name: result.name,
|
|
159
172
|
description: result.description,
|
|
160
173
|
sentiment: result.sentiment,
|
|
161
174
|
category: result.category ?? candidateCategory ?? existingTopic?.category,
|
|
162
|
-
exposure_current: calculateExposureCurrent(exposureImpact),
|
|
175
|
+
exposure_current: calculateExposureCurrent(exposureImpact, existingTopic?.exposure_current ?? 0),
|
|
163
176
|
exposure_desired: result.exposure_desired ?? 0.5,
|
|
164
177
|
last_updated: now,
|
|
165
178
|
last_mentioned: now,
|
|
166
|
-
learned_by: isNewItem ?
|
|
167
|
-
last_changed_by:
|
|
168
|
-
interested_personas:
|
|
169
|
-
|
|
170
|
-
: [...new Set([...(existingTopic?.interested_personas ?? []), personaId])],
|
|
171
|
-
persona_groups: mergeGroups(personaGroup, isNewItem, existingTopic?.persona_groups),
|
|
179
|
+
learned_by: isNewItem ? primaryId : existingTopic?.learned_by,
|
|
180
|
+
last_changed_by: primaryId,
|
|
181
|
+
interested_personas: interestedPersonas,
|
|
182
|
+
persona_groups: personaGroupsMerged,
|
|
172
183
|
embedding,
|
|
173
184
|
};
|
|
174
185
|
state.human_topic_upsert(topic);
|
|
175
186
|
|
|
176
|
-
const allMessages =
|
|
187
|
+
const allMessages = roomId
|
|
188
|
+
? normalizeRoomMessages(state.getRoomMessages(roomId), state)
|
|
189
|
+
: state.messages_get(personaId);
|
|
177
190
|
await validateAndStoreQuotes(result.quotes, allMessages, itemId, personaDisplayName, personaGroup, state);
|
|
178
191
|
|
|
179
192
|
console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${result.name}"`);
|
|
@@ -191,6 +204,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
191
204
|
const existingItemId = response.request.data.existingItemId as string | undefined;
|
|
192
205
|
const personaId = response.request.data.personaId as string;
|
|
193
206
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
207
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
194
208
|
const candidateRelationship = response.request.data.candidateRelationship as string | undefined;
|
|
195
209
|
|
|
196
210
|
if (!result.name || !result.description || result.sentiment === undefined) {
|
|
@@ -198,6 +212,9 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
198
212
|
return;
|
|
199
213
|
}
|
|
200
214
|
|
|
215
|
+
const personaIds = personaId.split("|").filter(Boolean);
|
|
216
|
+
const primaryId = personaIds[0] ?? personaId;
|
|
217
|
+
|
|
201
218
|
const now = new Date().toISOString();
|
|
202
219
|
const human = state.getHuman();
|
|
203
220
|
|
|
@@ -207,8 +224,11 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
207
224
|
};
|
|
208
225
|
const itemId = resolveItemId();
|
|
209
226
|
|
|
210
|
-
const persona = state.persona_getById(
|
|
227
|
+
const persona = state.persona_getById(primaryId);
|
|
211
228
|
const personaGroup = persona?.group_primary ?? null;
|
|
229
|
+
const allPersonaGroups = personaIds
|
|
230
|
+
.map(id => state.persona_getById(id)?.group_primary)
|
|
231
|
+
.filter((g): g is string => g != null);
|
|
212
232
|
|
|
213
233
|
const existingPerson = isNewItem ? undefined : human.people.find(p => p.id === existingItemId);
|
|
214
234
|
|
|
@@ -223,27 +243,34 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
const exposureImpact = result.exposure_impact as ExposureImpact | undefined;
|
|
246
|
+
const interestedPersonas = isNewItem
|
|
247
|
+
? personaIds
|
|
248
|
+
: [...new Set([...(existingPerson?.interested_personas ?? []), ...personaIds])];
|
|
249
|
+
const personaGroupsMerged = isNewItem
|
|
250
|
+
? (allPersonaGroups.length > 0 ? allPersonaGroups : existingPerson?.persona_groups)
|
|
251
|
+
: [...new Set([...(existingPerson?.persona_groups ?? []), ...allPersonaGroups])];
|
|
252
|
+
|
|
226
253
|
const person: Person = {
|
|
227
254
|
id: itemId,
|
|
228
255
|
name: result.name,
|
|
229
256
|
description: result.description,
|
|
230
257
|
sentiment: result.sentiment,
|
|
231
258
|
relationship: result.relationship ?? candidateRelationship ?? existingPerson?.relationship ?? "Unknown",
|
|
232
|
-
exposure_current: calculateExposureCurrent(exposureImpact),
|
|
259
|
+
exposure_current: calculateExposureCurrent(exposureImpact, existingPerson?.exposure_current ?? 0),
|
|
233
260
|
exposure_desired: result.exposure_desired ?? 0.5,
|
|
234
261
|
last_updated: now,
|
|
235
262
|
last_mentioned: now,
|
|
236
|
-
learned_by: isNewItem ?
|
|
237
|
-
last_changed_by:
|
|
238
|
-
interested_personas:
|
|
239
|
-
|
|
240
|
-
: [...new Set([...(existingPerson?.interested_personas ?? []), personaId])],
|
|
241
|
-
persona_groups: mergeGroups(personaGroup, isNewItem, existingPerson?.persona_groups),
|
|
263
|
+
learned_by: isNewItem ? primaryId : existingPerson?.learned_by,
|
|
264
|
+
last_changed_by: primaryId,
|
|
265
|
+
interested_personas: interestedPersonas,
|
|
266
|
+
persona_groups: personaGroupsMerged,
|
|
242
267
|
embedding,
|
|
243
268
|
};
|
|
244
269
|
state.human_person_upsert(person);
|
|
245
270
|
|
|
246
|
-
const allMessages =
|
|
271
|
+
const allMessages = roomId
|
|
272
|
+
? normalizeRoomMessages(state.getRoomMessages(roomId), state)
|
|
273
|
+
: state.messages_get(personaId);
|
|
247
274
|
await validateAndStoreQuotes(result.quotes, allMessages, itemId, personaDisplayName, personaGroup, state);
|
|
248
275
|
|
|
249
276
|
console.log(`[handlePersonUpdate] ${isNewItem ? "Created" : "Updated"} person "${result.name}"`);
|
|
@@ -255,8 +282,7 @@ function normalizeText(text: string): string {
|
|
|
255
282
|
.replace(/[\u2018\u2019\u0060\u00B4]/g, "'") // curly single, backtick, acute accent
|
|
256
283
|
.replace(/[\u2014\u2013\u2012]/g, '-') // em-dash, en-dash, figure dash
|
|
257
284
|
.replace(/\u00A0/g, ' ') // non-breaking space
|
|
258
|
-
.replace(/[\u2000-\u200F]/g, ' ') // unicode space variants
|
|
259
|
-
.replace(/\u2026|\.\.\./g, '\u2026'); // normalize both ellipsis forms → unicode ellipsis (1:1)
|
|
285
|
+
.replace(/[\u2000-\u200F]/g, ' '); // unicode space variants
|
|
260
286
|
}
|
|
261
287
|
|
|
262
288
|
function stripPunctuation(text: string): string {
|
|
@@ -270,31 +296,46 @@ function stripPunctuation(text: string): string {
|
|
|
270
296
|
.toLowerCase();
|
|
271
297
|
}
|
|
272
298
|
|
|
273
|
-
interface WordBoundaryMatch {
|
|
299
|
+
export interface WordBoundaryMatch {
|
|
274
300
|
start: number;
|
|
275
301
|
end: number;
|
|
276
302
|
text: string;
|
|
277
303
|
}
|
|
278
304
|
|
|
279
|
-
function
|
|
305
|
+
export function expandToWordBoundaries(text: string, start: number, end: number): WordBoundaryMatch {
|
|
306
|
+
// Only walk backward if start is mid-word (not already at a word boundary)
|
|
307
|
+
if (start > 0 && !/\s/.test(text[start]))
|
|
308
|
+
while (start > 0 && !/\s/.test(text[start - 1])) start--;
|
|
309
|
+
// Only walk forward if end is mid-word
|
|
310
|
+
if (end > 0 && !/\s/.test(text[end - 1]))
|
|
311
|
+
while (end < text.length && !/\s/.test(text[end])) end++;
|
|
312
|
+
return { start, end, text: text.slice(start, end) };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function findQuoteByWords(quoteText: string, msgText: string): WordBoundaryMatch | null {
|
|
280
316
|
const strippedQuote = stripPunctuation(quoteText);
|
|
281
317
|
const quoteWords = strippedQuote.split(' ').filter(w => w.length > 0);
|
|
282
318
|
|
|
283
|
-
if (quoteWords.length <
|
|
319
|
+
if (quoteWords.length < 2) return null; // Too short to trust — require at least 2 words
|
|
284
320
|
|
|
285
|
-
// Build word token list from original message with original positions
|
|
321
|
+
// Build word token list from original message with original positions.
|
|
322
|
+
// Each \S+ token is re-split into sub-tokens (sharing the parent's start/end)
|
|
323
|
+
// so that contractions stripped by stripPunctuation (e.g. don't → "don t")
|
|
324
|
+
// align correctly with quoteWords which is also split on spaces.
|
|
286
325
|
const wordTokens: Array<{ word: string; start: number; end: number }> = [];
|
|
287
326
|
const wordRegex = /\S+/g;
|
|
288
327
|
let match: RegExpExecArray | null;
|
|
289
328
|
while ((match = wordRegex.exec(msgText)) !== null) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
329
|
+
const tokenStart = match.index;
|
|
330
|
+
const tokenEnd = match.index + match[0].length;
|
|
331
|
+
const stripped = stripPunctuation(match[0]);
|
|
332
|
+
const subWords = stripped.split(' ').filter(w => w.length > 0);
|
|
333
|
+
for (const sub of subWords) {
|
|
334
|
+
wordTokens.push({ word: sub, start: tokenStart, end: tokenEnd });
|
|
335
|
+
}
|
|
295
336
|
}
|
|
296
337
|
|
|
297
|
-
// Find contiguous sequence of
|
|
338
|
+
// Find contiguous sequence of word tokens matching the quote words
|
|
298
339
|
for (let i = 0; i <= wordTokens.length - quoteWords.length; i++) {
|
|
299
340
|
let allMatch = true;
|
|
300
341
|
for (let j = 0; j < quoteWords.length; j++) {
|
|
@@ -306,11 +347,7 @@ function findQuoteByWords(quoteText: string, msgText: string): WordBoundaryMatch
|
|
|
306
347
|
if (allMatch) {
|
|
307
348
|
const startToken = wordTokens[i];
|
|
308
349
|
const endToken = wordTokens[i + quoteWords.length - 1];
|
|
309
|
-
return
|
|
310
|
-
start: startToken.start,
|
|
311
|
-
end: endToken.end,
|
|
312
|
-
text: msgText.slice(startToken.start, endToken.end),
|
|
313
|
-
};
|
|
350
|
+
return expandToWordBoundaries(msgText, startToken.start, endToken.end);
|
|
314
351
|
}
|
|
315
352
|
}
|
|
316
353
|
|
|
@@ -343,9 +380,10 @@ async function validateAndStoreQuotes(
|
|
|
343
380
|
let matchLevel: string;
|
|
344
381
|
|
|
345
382
|
if (start !== -1) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
383
|
+
const expanded = expandToWordBoundaries(msgText, start, start + candidate.text.length);
|
|
384
|
+
matchStart = expanded.start;
|
|
385
|
+
matchEnd = expanded.end;
|
|
386
|
+
matchText = expanded.text;
|
|
349
387
|
matchLevel = "exact";
|
|
350
388
|
} else {
|
|
351
389
|
// Level 2: word-boundary fallback
|
|
@@ -413,7 +451,8 @@ async function validateAndStoreQuotes(
|
|
|
413
451
|
data_item_ids: [dataItemId],
|
|
414
452
|
persona_groups: [personaGroup || "General"],
|
|
415
453
|
text: matchText,
|
|
416
|
-
speaker: message.role === "human" ? "human" : personaName,
|
|
454
|
+
speaker: message.role === "human" ? "human" : (message.speaker_name ?? personaName),
|
|
455
|
+
channel: personaName,
|
|
417
456
|
timestamp: message.timestamp,
|
|
418
457
|
start: matchStart,
|
|
419
458
|
end: matchEnd,
|
|
@@ -436,14 +475,5 @@ async function validateAndStoreQuotes(
|
|
|
436
475
|
}
|
|
437
476
|
}
|
|
438
477
|
|
|
439
|
-
function calculateExposureCurrent(impact: ExposureImpact | undefined): number {
|
|
440
|
-
switch (impact) {
|
|
441
|
-
case "high": return 0.9;
|
|
442
|
-
case "medium": return 0.6;
|
|
443
|
-
case "low": return 0.3;
|
|
444
|
-
case "none": return 0.1;
|
|
445
|
-
default: return 0.5;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
478
|
|
|
449
479
|
|
|
@@ -18,6 +18,8 @@ import { handleFactFind, handleHumanTopicScan, handleHumanPersonScan, handleEven
|
|
|
18
18
|
import { handleTopicMatch, handleTopicUpdate, handlePersonMatch, handlePersonUpdate } from "./human-matching.js";
|
|
19
19
|
import { handleRewriteScan, handleRewriteRewrite } from "./rewrite.js";
|
|
20
20
|
import { handleDedupCurate } from "./dedup.js";
|
|
21
|
+
import { handleRoomResponse, handleRoomJudge } from "./rooms.js";
|
|
22
|
+
import { handlePersonaPreview } from "./persona-preview.js";
|
|
21
23
|
|
|
22
24
|
export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
23
25
|
handlePersonaResponse,
|
|
@@ -45,4 +47,7 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
|
45
47
|
handleRewriteRewrite,
|
|
46
48
|
handleDedupCurate,
|
|
47
49
|
handleEventScan,
|
|
50
|
+
handleRoomResponse,
|
|
51
|
+
handleRoomJudge,
|
|
52
|
+
handlePersonaPreview,
|
|
48
53
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LLMResponse } from "../types.js";
|
|
2
|
+
import type { StateManager } from "../state-manager.js";
|
|
3
|
+
|
|
4
|
+
export function handlePersonaPreview(_response: LLMResponse, _state: StateManager): void {
|
|
5
|
+
// Intentionally empty — state writes are not needed for preview generation.
|
|
6
|
+
// The Processor post-dispatch block handles: completeness validation, re-queue, and Promise resolution.
|
|
7
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../orchestrators/index.js";
|
|
23
23
|
import { buildPersonaDescriptionsPrompt } from "../../prompts/generation/index.js";
|
|
24
24
|
import { splitMessagesByTimestamp } from "./utils.js";
|
|
25
|
+
import { calculateExposureCurrent } from "../utils/exposure.js";
|
|
25
26
|
|
|
26
27
|
export const MIN_MESSAGE_COUNT_FOR_CREATE = 2;
|
|
27
28
|
|
|
@@ -272,7 +273,7 @@ export function handlePersonaTopicUpdate(response: LLMResponse, state: StateMana
|
|
|
272
273
|
approach: result.approach || "",
|
|
273
274
|
personal_stake: result.personal_stake || "",
|
|
274
275
|
sentiment: result.sentiment,
|
|
275
|
-
exposure_current: result.
|
|
276
|
+
exposure_current: calculateExposureCurrent(result.exposure_impact, 0),
|
|
276
277
|
exposure_desired: result.exposure_desired,
|
|
277
278
|
last_updated: now,
|
|
278
279
|
};
|
|
@@ -284,7 +285,7 @@ export function handlePersonaTopicUpdate(response: LLMResponse, state: StateMana
|
|
|
284
285
|
const updatedTopics = persona.topics.map((t: PersonaTopic) => {
|
|
285
286
|
if (t.id !== existingTopicId) return t;
|
|
286
287
|
|
|
287
|
-
const newExposure = Math.
|
|
288
|
+
const newExposure = Math.max(calculateExposureCurrent(result.exposure_impact, t.exposure_current), t.exposure_current);
|
|
288
289
|
|
|
289
290
|
return {
|
|
290
291
|
...t,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Room Response Handlers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ContextStatus, LLMNextStep, LLMPriority, LLMRequestType } from "../types.js";
|
|
6
|
+
import type { LLMResponse, RoomMessage } from "../types.js";
|
|
7
|
+
import type { StateManager } from "../state-manager.js";
|
|
8
|
+
import type { PersonaResponseResult } from "../../prompts/response/index.js";
|
|
9
|
+
import type { RoomJudgeResult } from "../../prompts/room/index.js";
|
|
10
|
+
import { buildRoomResponsePromptData } from "../prompt-context-builder.js";
|
|
11
|
+
|
|
12
|
+
export function handleRoomResponse(response: LLMResponse, state: StateManager): void {
|
|
13
|
+
const roomId = response.request.data.roomId as string;
|
|
14
|
+
const personaId = response.request.data.personaId as string;
|
|
15
|
+
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
16
|
+
const parentMessageId = response.request.data.parentMessageId as string | null ?? null;
|
|
17
|
+
|
|
18
|
+
if (!roomId || !personaId) {
|
|
19
|
+
console.error("[handleRoomResponse] Missing roomId or personaId in request data");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const now = new Date().toISOString();
|
|
24
|
+
|
|
25
|
+
if (response.parsed !== undefined) {
|
|
26
|
+
const result = response.parsed as PersonaResponseResult;
|
|
27
|
+
|
|
28
|
+
if (!result.should_respond) {
|
|
29
|
+
const reason = result.reason;
|
|
30
|
+
console.log(`[handleRoomResponse] ${personaDisplayName} chose silence in room ${roomId}: ${reason ?? "(no reason)"}`);
|
|
31
|
+
if (reason) {
|
|
32
|
+
const msg: RoomMessage = {
|
|
33
|
+
id: crypto.randomUUID(),
|
|
34
|
+
parent_id: parentMessageId,
|
|
35
|
+
role: "persona",
|
|
36
|
+
persona_id: personaId,
|
|
37
|
+
silence_reason: reason,
|
|
38
|
+
timestamp: now,
|
|
39
|
+
read: false,
|
|
40
|
+
context_status: ContextStatus.Default,
|
|
41
|
+
};
|
|
42
|
+
state.appendRoomMessage(roomId, msg);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const verbal = result.verbal_response || undefined;
|
|
48
|
+
const action = result.action_response || undefined;
|
|
49
|
+
|
|
50
|
+
if (!verbal && !action) {
|
|
51
|
+
console.log(`[handleRoomResponse] ${personaDisplayName} returned should_respond=true but no content`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const msg: RoomMessage = {
|
|
56
|
+
id: crypto.randomUUID(),
|
|
57
|
+
parent_id: parentMessageId,
|
|
58
|
+
role: "persona",
|
|
59
|
+
persona_id: personaId,
|
|
60
|
+
verbal_response: verbal,
|
|
61
|
+
action_response: action,
|
|
62
|
+
timestamp: now,
|
|
63
|
+
read: false,
|
|
64
|
+
context_status: ContextStatus.Default,
|
|
65
|
+
};
|
|
66
|
+
state.appendRoomMessage(roomId, msg);
|
|
67
|
+
console.log(`[handleRoomResponse] Appended response from ${personaDisplayName} to room ${roomId}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!response.content) {
|
|
72
|
+
console.log(`[handleRoomResponse] ${personaDisplayName} no response (empty content)`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const msg: RoomMessage = {
|
|
77
|
+
id: crypto.randomUUID(),
|
|
78
|
+
parent_id: parentMessageId,
|
|
79
|
+
role: "persona",
|
|
80
|
+
persona_id: personaId,
|
|
81
|
+
verbal_response: response.content,
|
|
82
|
+
timestamp: now,
|
|
83
|
+
read: false,
|
|
84
|
+
context_status: ContextStatus.Default,
|
|
85
|
+
};
|
|
86
|
+
state.appendRoomMessage(roomId, msg);
|
|
87
|
+
console.log(`[handleRoomResponse] Appended plain-text response from ${personaDisplayName} to room ${roomId}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function handleRoomJudge(response: LLMResponse, state: StateManager): Promise<void> {
|
|
91
|
+
const roomId = response.request.data.roomId as string;
|
|
92
|
+
const judgeDisplayName = response.request.data.judgePersonaDisplayName as string;
|
|
93
|
+
|
|
94
|
+
if (!roomId) {
|
|
95
|
+
console.error("[handleRoomJudge] Missing roomId in request data");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!response.parsed) {
|
|
100
|
+
console.error(`[handleRoomJudge] No parsed result from judge ${judgeDisplayName}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = response.parsed as RoomJudgeResult;
|
|
105
|
+
if (!result.winner_message_id) {
|
|
106
|
+
console.error(`[handleRoomJudge] Judge ${judgeDisplayName} returned no winner_message_id`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const judgePersonaId = response.request.data.judgePersonaId as string;
|
|
111
|
+
|
|
112
|
+
const allMessages = state.getRoomMessages(roomId);
|
|
113
|
+
const winner = allMessages.find(m => m.id === result.winner_message_id);
|
|
114
|
+
if (!winner) {
|
|
115
|
+
console.error(`[handleRoomJudge] Winner message ${result.winner_message_id} not found in room ${roomId}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const verdictParentId = winner.parent_id;
|
|
120
|
+
|
|
121
|
+
const ok = state.setRoomActiveNode(roomId, result.winner_message_id);
|
|
122
|
+
if (!ok) {
|
|
123
|
+
console.error(`[handleRoomJudge] Could not set active node ${result.winner_message_id} in room ${roomId}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const losers = allMessages
|
|
128
|
+
.filter(m => m.parent_id === verdictParentId && m.id !== winner.id)
|
|
129
|
+
.map(m => m.id);
|
|
130
|
+
if (losers.length > 0) {
|
|
131
|
+
state.removeRoomMessages(roomId, losers);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (result.reason) {
|
|
135
|
+
console.log(`[handleRoomJudge] ${judgeDisplayName} verdict: ${result.reason}`);
|
|
136
|
+
const verdictMsg = {
|
|
137
|
+
id: crypto.randomUUID(),
|
|
138
|
+
parent_id: verdictParentId,
|
|
139
|
+
role: "persona" as const,
|
|
140
|
+
persona_id: judgePersonaId,
|
|
141
|
+
silence_reason: result.reason,
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
read: false,
|
|
144
|
+
context_status: "default" as import("../types.js").ContextStatus,
|
|
145
|
+
};
|
|
146
|
+
state.appendRoomMessage(roomId, verdictMsg);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const room = state.getRoom(roomId);
|
|
150
|
+
if (!room) return;
|
|
151
|
+
|
|
152
|
+
for (const personaId of room.persona_ids) {
|
|
153
|
+
if (room.judge_persona_id === personaId) continue;
|
|
154
|
+
const persona = state.persona_getById(personaId);
|
|
155
|
+
if (!persona || persona.is_archived || persona.is_paused) continue;
|
|
156
|
+
|
|
157
|
+
const isTUI = false;
|
|
158
|
+
const promptData = await buildRoomResponsePromptData(state, room, persona, isTUI);
|
|
159
|
+
const model = persona.model ?? state.getHuman().settings?.default_model ?? "";
|
|
160
|
+
|
|
161
|
+
state.queue_enqueue({
|
|
162
|
+
type: LLMRequestType.JSON,
|
|
163
|
+
priority: LLMPriority.Room,
|
|
164
|
+
system: promptData.system,
|
|
165
|
+
user: promptData.user,
|
|
166
|
+
next_step: LLMNextStep.HandleRoomResponse,
|
|
167
|
+
model,
|
|
168
|
+
data: {
|
|
169
|
+
roomId,
|
|
170
|
+
personaId,
|
|
171
|
+
personaDisplayName: persona.display_name,
|
|
172
|
+
parentMessageId: result.winner_message_id,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,12 +1,53 @@
|
|
|
1
|
-
import type { Message, LLMResponse } from "../types.js";
|
|
1
|
+
import type { Message, RoomMessage, LLMResponse } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
|
|
4
|
+
export function normalizeRoomMessages(messages: RoomMessage[], state: StateManager): Message[] {
|
|
5
|
+
const human = state.getHuman();
|
|
6
|
+
const humanName = human.settings?.name_display ?? "Human";
|
|
7
|
+
return messages.map(m => {
|
|
8
|
+
const speakerName = m.role === "human"
|
|
9
|
+
? humanName
|
|
10
|
+
: (state.persona_getById(m.persona_id ?? "")?.display_name ?? "Participant");
|
|
11
|
+
return {
|
|
12
|
+
id: m.id,
|
|
13
|
+
role: m.role === "human" ? "human" as const : "system" as const,
|
|
14
|
+
speaker_name: speakerName,
|
|
15
|
+
verbal_response: m.verbal_response,
|
|
16
|
+
action_response: m.action_response,
|
|
17
|
+
silence_reason: m.silence_reason,
|
|
18
|
+
timestamp: m.timestamp,
|
|
19
|
+
read: m.read,
|
|
20
|
+
context_status: m.context_status,
|
|
21
|
+
f: m.f,
|
|
22
|
+
t: m.t,
|
|
23
|
+
p: m.p,
|
|
24
|
+
e: m.e,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
4
29
|
export function resolveMessageWindow(
|
|
5
30
|
response: LLMResponse,
|
|
6
31
|
state: StateManager
|
|
7
32
|
): { messages_context: Message[]; messages_analyze: Message[] } {
|
|
8
|
-
const
|
|
33
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
9
34
|
const messageIdsToMark = response.request.data.message_ids_to_mark as string[] | undefined;
|
|
35
|
+
|
|
36
|
+
if (roomId) {
|
|
37
|
+
const allRoomMessages = normalizeRoomMessages(state.getRoomMessages(roomId), state);
|
|
38
|
+
if (messageIdsToMark && messageIdsToMark.length > 0) {
|
|
39
|
+
const idSet = new Set(messageIdsToMark);
|
|
40
|
+
const messages_analyze = allRoomMessages.filter(m => idSet.has(m.id));
|
|
41
|
+
const analyzeStartTime = messages_analyze[0]?.timestamp ?? '9999';
|
|
42
|
+
const messages_context = allRoomMessages.filter(m =>
|
|
43
|
+
!idSet.has(m.id) && new Date(m.timestamp).getTime() < new Date(analyzeStartTime).getTime()
|
|
44
|
+
);
|
|
45
|
+
return { messages_context, messages_analyze };
|
|
46
|
+
}
|
|
47
|
+
return { messages_context: [], messages_analyze: allRoomMessages };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const personaId = response.request.data.personaId as string;
|
|
10
51
|
const allMessages = state.messages_get(personaId);
|
|
11
52
|
|
|
12
53
|
if (messageIdsToMark && messageIdsToMark.length > 0) {
|
|
@@ -48,10 +89,21 @@ export function markMessagesExtracted(
|
|
|
48
89
|
state: StateManager,
|
|
49
90
|
flag: ExtractionFlag
|
|
50
91
|
): void {
|
|
92
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
51
93
|
const personaId = response.request.data.personaId as string | undefined;
|
|
52
94
|
const messageIds = response.request.data.message_ids_to_mark as string[] | undefined;
|
|
53
95
|
|
|
54
|
-
if (!
|
|
96
|
+
if (!messageIds?.length) return;
|
|
97
|
+
|
|
98
|
+
if (roomId) {
|
|
99
|
+
const count = state.markRoomMessagesExtracted(roomId, messageIds, flag);
|
|
100
|
+
if (count > 0) {
|
|
101
|
+
console.log(`[markMessagesExtracted] Marked ${count} room messages with flag '${flag}' for room ${roomId}`);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!personaId) return;
|
|
55
107
|
|
|
56
108
|
const count = state.messages_markExtracted(personaId, messageIds, flag);
|
|
57
109
|
if (count > 0) {
|
|
@@ -158,9 +158,11 @@ export async function queueEiHeartbeat(
|
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
const recentHistory = history.slice(-10);
|
|
161
162
|
const promptData: EiHeartbeatPromptData = {
|
|
162
163
|
items,
|
|
163
|
-
recent_history:
|
|
164
|
+
recent_history: recentHistory,
|
|
165
|
+
system_messages: recentHistory.filter(m => m.role === "system"),
|
|
164
166
|
};
|
|
165
167
|
|
|
166
168
|
const prompt = buildEiHeartbeatPrompt(promptData);
|
package/src/core/llm-client.ts
CHANGED
|
@@ -159,7 +159,7 @@ export async function callLLMRaw(
|
|
|
159
159
|
const chatMessages: ChatMessage[] = [
|
|
160
160
|
{ role: "system", content: systemPrompt },
|
|
161
161
|
...messages,
|
|
162
|
-
{ role: "user", content: userPrompt },
|
|
162
|
+
...(userPrompt ? [{ role: "user" as const, content: userPrompt }] : []),
|
|
163
163
|
];
|
|
164
164
|
|
|
165
165
|
const finalMessages = ensureUserFirst(chatMessages);
|