ei-tui 1.0.1 → 1.2.0
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 +3 -1
- package/package.json +2 -21
- package/src/cli/README.md +42 -14
- package/src/cli/mcp.ts +237 -0
- package/src/cli.ts +17 -51
- package/src/core/handlers/dedup.ts +4 -15
- package/src/core/handlers/document-segmentation.ts +2 -3
- package/src/core/handlers/heartbeat.ts +5 -10
- package/src/core/handlers/human-extraction.ts +6 -0
- package/src/core/handlers/human-matching.ts +53 -10
- package/src/core/handlers/index.ts +2 -0
- package/src/core/handlers/knowledge-synthesis.ts +50 -0
- package/src/core/handlers/persona-generation.ts +4 -8
- package/src/core/handlers/persona-response.ts +3 -4
- package/src/core/handlers/persona-topics.ts +2 -4
- package/src/core/handlers/rewrite.ts +26 -9
- package/src/core/handlers/rooms.ts +6 -12
- package/src/core/llm-client.ts +53 -7
- package/src/core/message-manager.ts +2 -4
- package/src/core/orchestrators/ceremony.ts +44 -13
- package/src/core/orchestrators/human-extraction.ts +38 -1
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/processor.ts +192 -41
- package/src/core/prompt-context-builder.ts +1 -0
- package/src/core/queue-manager.ts +10 -0
- package/src/core/queue-processor.ts +13 -4
- package/src/core/state-manager.ts +35 -0
- package/src/core/tools/builtin/fetch-memory.ts +92 -0
- package/src/core/tools/builtin/fetch-message.ts +123 -0
- package/src/core/tools/builtin/find-memory.ts +99 -0
- package/src/core/tools/index.ts +88 -5
- package/src/core/tools/types.ts +1 -1
- package/src/core/types/data-items.ts +1 -1
- package/src/core/types/entities.ts +7 -1
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +3 -1
- package/src/integrations/claude-code/importer.ts +6 -0
- package/src/integrations/cursor/importer.ts +6 -0
- package/src/integrations/document/unsource.ts +5 -3
- package/src/integrations/opencode/importer.ts +13 -1
- package/src/integrations/persona-history/importer.ts +12 -1
- package/src/prompts/ceremony/dedup.ts +3 -3
- package/src/prompts/ceremony/people-rewrite.ts +2 -2
- package/src/prompts/ceremony/topic-rewrite.ts +2 -2
- package/src/prompts/ceremony/types.ts +1 -1
- package/src/prompts/human/person-scan.ts +17 -0
- package/src/prompts/human/types.ts +4 -0
- package/src/prompts/index.ts +3 -0
- package/src/prompts/response/sections.ts +14 -7
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/synthesis/index.ts +101 -0
- package/src/prompts/synthesis/types.ts +26 -0
- package/tui/src/commands/generate.tsx +98 -0
- package/tui/src/commands/unsource.tsx +17 -10
- package/tui/src/components/GeneratedDocsOverlay.tsx +136 -0
- package/tui/src/components/PromptInput.tsx +2 -0
- package/tui/src/context/ei.tsx +49 -2
- package/tui/src/util/logger.ts +22 -2
- package/tui/src/util/provider-detection.ts +5 -2
- package/tui/src/util/yaml-provider.ts +2 -8
- package/src/core/tools/builtin/read-memory.ts +0 -70
|
@@ -85,6 +85,10 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
85
85
|
const primaryId = personaIds[0] ?? personaId;
|
|
86
86
|
|
|
87
87
|
const now = new Date().toISOString();
|
|
88
|
+
const { messages_analyze } = resolveMessageWindow(response, state);
|
|
89
|
+
const earliestMessageTimestamp = messages_analyze.length > 0
|
|
90
|
+
? messages_analyze.reduce((a, b) => a.timestamp < b.timestamp ? a : b).timestamp
|
|
91
|
+
: now;
|
|
88
92
|
const human = state.getHuman();
|
|
89
93
|
|
|
90
94
|
const resolveItemId = (): string => {
|
|
@@ -135,6 +139,8 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
135
139
|
? incomingSources
|
|
136
140
|
: [...new Set([...(existingTopic?.sources ?? []), ...incomingSources])];
|
|
137
141
|
|
|
142
|
+
const newDescLen = resolvedDescription?.length ?? 0;
|
|
143
|
+
const existingFloor = existingTopic?.rewrite_length_floor;
|
|
138
144
|
const topic: Topic = {
|
|
139
145
|
id: itemId,
|
|
140
146
|
name: resolvedName,
|
|
@@ -144,7 +150,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
144
150
|
exposure_current: calculateExposureCurrent(exposureImpact, existingTopic?.exposure_current ?? 0),
|
|
145
151
|
exposure_desired: result.exposure_desired ?? 0.5,
|
|
146
152
|
last_updated: now,
|
|
147
|
-
learned_on: isNewItem ?
|
|
153
|
+
learned_on: isNewItem ? earliestMessageTimestamp : existingTopic?.learned_on,
|
|
148
154
|
last_mentioned: now,
|
|
149
155
|
learned_by: isNewItem ? primaryId : existingTopic?.learned_by,
|
|
150
156
|
last_changed_by: primaryId,
|
|
@@ -152,6 +158,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
152
158
|
sources: sources.length > 0 ? sources : undefined,
|
|
153
159
|
persona_groups: personaGroupsMerged,
|
|
154
160
|
embedding,
|
|
161
|
+
rewrite_length_floor: existingFloor !== undefined && newDescLen < existingFloor ? existingFloor : undefined,
|
|
155
162
|
};
|
|
156
163
|
state.human_topic_upsert(topic);
|
|
157
164
|
|
|
@@ -168,6 +175,26 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
168
175
|
console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${resolvedName}"`);
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
function ensureEiPersonaHasNickname(identifiers: PersonIdentifier[], state: StateManager): PersonIdentifier[] {
|
|
179
|
+
const eiPersonaId = identifiers.find(i => i.type === 'Ei Persona')?.value;
|
|
180
|
+
if (!eiPersonaId) return identifiers;
|
|
181
|
+
|
|
182
|
+
const persona = state.persona_getById(eiPersonaId);
|
|
183
|
+
if (!persona) return identifiers;
|
|
184
|
+
|
|
185
|
+
const hasNickname = identifiers.some(i => i.type === 'Nickname' && i.value === persona.display_name);
|
|
186
|
+
if (hasNickname) return identifiers;
|
|
187
|
+
|
|
188
|
+
const withoutPrimary = identifiers.map(i =>
|
|
189
|
+
i.type === 'Ei Persona' ? { ...i, is_primary: undefined } : i
|
|
190
|
+
).map(({ is_primary, ...rest }) => is_primary ? { ...rest, is_primary } : rest);
|
|
191
|
+
|
|
192
|
+
return [
|
|
193
|
+
{ type: 'Nickname', value: persona.display_name, is_primary: true as const },
|
|
194
|
+
...withoutPrimary.map(i => i.type === 'Ei Persona' ? { type: i.type, value: i.value } : i),
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
171
198
|
export async function handlePersonUpdate(response: LLMResponse, state: StateManager): Promise<void> {
|
|
172
199
|
const result = response.parsed as (PersonUpdateResult & {
|
|
173
200
|
identifiers?: PersonIdentifier[];
|
|
@@ -194,6 +221,10 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
194
221
|
const primaryId = personaIds[0] ?? personaId;
|
|
195
222
|
|
|
196
223
|
const now = new Date().toISOString();
|
|
224
|
+
const { messages_analyze } = resolveMessageWindow(response, state);
|
|
225
|
+
const earliestMessageTimestamp = messages_analyze.length > 0
|
|
226
|
+
? messages_analyze.reduce((a, b) => a.timestamp < b.timestamp ? a : b).timestamp
|
|
227
|
+
: now;
|
|
197
228
|
const human = state.getHuman();
|
|
198
229
|
|
|
199
230
|
const resolveItemId = (): string => {
|
|
@@ -264,7 +295,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
264
295
|
deduped.push(id);
|
|
265
296
|
}
|
|
266
297
|
}
|
|
267
|
-
resolvedIdentifiers = deduped;
|
|
298
|
+
resolvedIdentifiers = ensureEiPersonaHasNickname(deduped, state);
|
|
268
299
|
} else {
|
|
269
300
|
const base = [...(existingPerson?.identifiers ?? [])];
|
|
270
301
|
const sanitizedToAdd = sanitizeEiPersonaIdentifiers(
|
|
@@ -279,12 +310,16 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
279
310
|
base.push({ type: id.type, value: id.value, ...(id.is_primary ? { is_primary: id.is_primary } : {}) });
|
|
280
311
|
}
|
|
281
312
|
}
|
|
282
|
-
resolvedIdentifiers = base;
|
|
313
|
+
resolvedIdentifiers = ensureEiPersonaHasNickname(base, state);
|
|
283
314
|
}
|
|
284
315
|
|
|
316
|
+
const personName = resolvedIdentifiers.find(i => i.is_primary && i.type !== 'Ei Persona')?.value
|
|
317
|
+
?? resolvedIdentifiers.find(i => i.type !== 'Ei Persona')?.value
|
|
318
|
+
?? candidateName;
|
|
319
|
+
|
|
285
320
|
const person: Person = {
|
|
286
321
|
id: itemId,
|
|
287
|
-
name:
|
|
322
|
+
name: personName,
|
|
288
323
|
description: resolvedDescription,
|
|
289
324
|
sentiment: resolvedSentiment,
|
|
290
325
|
relationship: result.relationship ?? candidateRelationship ?? existingPerson?.relationship ?? "Unknown",
|
|
@@ -293,7 +328,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
293
328
|
identifiers: resolvedIdentifiers,
|
|
294
329
|
validated_date: isNewItem ? '' : (existingPerson?.validated_date ?? ''),
|
|
295
330
|
last_updated: now,
|
|
296
|
-
learned_on: isNewItem ?
|
|
331
|
+
learned_on: isNewItem ? earliestMessageTimestamp : existingPerson?.learned_on,
|
|
297
332
|
last_mentioned: now,
|
|
298
333
|
learned_by: isNewItem ? primaryId : existingPerson?.learned_by,
|
|
299
334
|
last_changed_by: primaryId,
|
|
@@ -301,6 +336,11 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
301
336
|
sources: personSources.length > 0 ? personSources : undefined,
|
|
302
337
|
persona_groups: personaGroupsMerged,
|
|
303
338
|
embedding,
|
|
339
|
+
rewrite_length_floor: (() => {
|
|
340
|
+
const floor = existingPerson?.rewrite_length_floor;
|
|
341
|
+
const newLen = resolvedDescription?.length ?? 0;
|
|
342
|
+
return floor !== undefined && newLen < floor ? floor : undefined;
|
|
343
|
+
})(),
|
|
304
344
|
};
|
|
305
345
|
state.human_person_upsert(person);
|
|
306
346
|
|
|
@@ -323,14 +363,13 @@ function normalizeText(text: string): string {
|
|
|
323
363
|
.replace(/[\u2018\u2019\u0060\u00B4]/g, "'") // curly single, backtick, acute accent
|
|
324
364
|
.replace(/[\u2014\u2013\u2012]/g, '-') // em-dash, en-dash, figure dash
|
|
325
365
|
.replace(/\u00A0/g, ' ') // non-breaking space
|
|
326
|
-
.replace(/[\u2000-\u200F]/g, ' ')
|
|
366
|
+
.replace(/[\u2000-\u200F]/g, ' ') // unicode space variants
|
|
367
|
+
.replace(/[*_`~]/g, ''); // Markdown emphasis/code chars
|
|
327
368
|
}
|
|
328
369
|
|
|
329
370
|
function stripPunctuation(text: string): string {
|
|
330
|
-
// Remove characters LLMs commonly mangle, keep spaces and alphanumeric
|
|
331
|
-
// Strip: punctuation, unicode punctuation variants, curly quotes, dashes, etc.
|
|
332
|
-
// Keep: letters, digits, spaces
|
|
333
371
|
return text
|
|
372
|
+
.replace(/[*_`~]/g, ' ') // Markdown chars (kept by \w, must strip explicitly)
|
|
334
373
|
.replace(/[^\w\s]/gu, ' ') // replace non-word, non-space with space
|
|
335
374
|
.replace(/\s+/g, ' ') // collapse multiple spaces
|
|
336
375
|
.trim()
|
|
@@ -406,6 +445,10 @@ async function validateAndStoreQuotes(
|
|
|
406
445
|
if (!candidates || candidates.length === 0) return;
|
|
407
446
|
|
|
408
447
|
for (const candidate of candidates) {
|
|
448
|
+
if (!candidate.text) {
|
|
449
|
+
console.warn('[extraction] Skipping quote candidate with missing text field');
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
409
452
|
let found = false;
|
|
410
453
|
for (const message of messages) {
|
|
411
454
|
const msgText = getMessageText(message);
|
|
@@ -511,7 +554,7 @@ async function validateAndStoreQuotes(
|
|
|
511
554
|
break;
|
|
512
555
|
}
|
|
513
556
|
if (!found) {
|
|
514
|
-
console.warn(`[extraction] Quote not found in messages (both levels), skipping: "${candidate.text
|
|
557
|
+
console.warn(`[extraction] Quote not found in messages (both levels), skipping: "${candidate.text}"`);
|
|
515
558
|
}
|
|
516
559
|
}
|
|
517
560
|
}
|
|
@@ -16,6 +16,7 @@ import { handleDedupCurate } from "./dedup.js";
|
|
|
16
16
|
import { handleRoomResponse, handleRoomJudge } from "./rooms.js";
|
|
17
17
|
import { handlePersonaPreview } from "./persona-preview.js";
|
|
18
18
|
import { handleDocumentSegmentation } from "./document-segmentation.js";
|
|
19
|
+
import { handleKnowledgeSynthesis } from "./knowledge-synthesis.js";
|
|
19
20
|
|
|
20
21
|
export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
21
22
|
handlePersonaResponse,
|
|
@@ -43,4 +44,5 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
|
43
44
|
[LLMNextStep.HandleTopicValidate]: handleDedupCurate,
|
|
44
45
|
[LLMNextStep.HandleReflectionCritic]: handleReflectionCritic,
|
|
45
46
|
[LLMNextStep.HandleDocumentSegmentation]: handleDocumentSegmentation,
|
|
47
|
+
[LLMNextStep.HandleKnowledgeSynthesis]: handleKnowledgeSynthesis,
|
|
46
48
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ContextStatus } from "../types.js";
|
|
2
|
+
import type { LLMResponse, Message } from "../types.js";
|
|
3
|
+
import type { StateManager } from "../state-manager.js";
|
|
4
|
+
|
|
5
|
+
export function handleKnowledgeSynthesis(response: LLMResponse, state: StateManager): void {
|
|
6
|
+
const { slug, subject } = response.request.data as {
|
|
7
|
+
slug: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (!slug || !subject) {
|
|
12
|
+
throw new Error("[handleKnowledgeSynthesis] Missing slug or subject in request data");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const content = response.content?.trim() ?? "";
|
|
16
|
+
if (!content) {
|
|
17
|
+
throw new Error(`[handleKnowledgeSynthesis] Empty or null response content for slug "${slug}"`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
const sourceTag = `generate:document:${slug}`;
|
|
22
|
+
|
|
23
|
+
const message: Message = {
|
|
24
|
+
id: crypto.randomUUID(),
|
|
25
|
+
role: "system",
|
|
26
|
+
content,
|
|
27
|
+
timestamp: now,
|
|
28
|
+
read: true,
|
|
29
|
+
context_status: ContextStatus.Always,
|
|
30
|
+
external: true,
|
|
31
|
+
source_tag: sourceTag,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
state.messages_append("emmet", message);
|
|
35
|
+
|
|
36
|
+
const updatedHuman = state.getHuman();
|
|
37
|
+
state.setHuman({
|
|
38
|
+
...updatedHuman,
|
|
39
|
+
settings: {
|
|
40
|
+
...updatedHuman.settings,
|
|
41
|
+
document: {
|
|
42
|
+
...updatedHuman.settings?.document,
|
|
43
|
+
processed_documents: {
|
|
44
|
+
...(updatedHuman.settings?.document?.processed_documents ?? {}),
|
|
45
|
+
[slug]: { created_at: now, type: "generated", subject },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -12,8 +12,7 @@ export function handlePersonaGeneration(response: LLMResponse, state: StateManag
|
|
|
12
12
|
const personaId = response.request.data.personaId as string;
|
|
13
13
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
14
14
|
if (!personaId) {
|
|
15
|
-
|
|
16
|
-
return;
|
|
15
|
+
throw new Error("[handlePersonaGeneration] No personaId in request data");
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const result = response.parsed as PersonaGenerationResult | undefined;
|
|
@@ -115,14 +114,12 @@ export function handlePersonaTraitExtraction(response: LLMResponse, state: State
|
|
|
115
114
|
const personaId = response.request.data.personaId as string;
|
|
116
115
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
117
116
|
if (!personaId) {
|
|
118
|
-
|
|
119
|
-
return;
|
|
117
|
+
throw new Error("[handlePersonaTraitExtraction] No personaId in request data");
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
const result = response.parsed as TraitResult[] | undefined;
|
|
123
121
|
if (!result || !Array.isArray(result)) {
|
|
124
|
-
|
|
125
|
-
return;
|
|
122
|
+
throw new Error("[handlePersonaTraitExtraction] Invalid parsed result");
|
|
126
123
|
}
|
|
127
124
|
|
|
128
125
|
if (result.length === 0) {
|
|
@@ -131,8 +128,7 @@ export function handlePersonaTraitExtraction(response: LLMResponse, state: State
|
|
|
131
128
|
|
|
132
129
|
const persona = state.persona_getById(personaId);
|
|
133
130
|
if (!persona) {
|
|
134
|
-
|
|
135
|
-
return;
|
|
131
|
+
throw new Error(`[handlePersonaTraitExtraction] Persona ${personaId} not found`);
|
|
136
132
|
}
|
|
137
133
|
|
|
138
134
|
const now = new Date().toISOString();
|
|
@@ -15,8 +15,7 @@ export function handlePersonaResponse(response: LLMResponse, state: StateManager
|
|
|
15
15
|
const personaId = response.request.data.personaId as string;
|
|
16
16
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
17
17
|
if (!personaId) {
|
|
18
|
-
|
|
19
|
-
return;
|
|
18
|
+
throw new Error("[handlePersonaResponse] No personaId in request data");
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
state.messages_markPendingAsRead(personaId);
|
|
@@ -108,7 +107,7 @@ export function handleToolContinuation(response: LLMResponse, state: StateManage
|
|
|
108
107
|
const originalStep = response.request.data.originalNextStep as LLMNextStep | undefined;
|
|
109
108
|
|
|
110
109
|
if (!originalStep) {
|
|
111
|
-
console.
|
|
110
|
+
console.warn(`[handleToolContinuation] No originalNextStep in data, falling back to handlePersonaResponse`);
|
|
112
111
|
handlePersonaResponse(response, state);
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
@@ -118,7 +117,7 @@ export function handleToolContinuation(response: LLMResponse, state: StateManage
|
|
|
118
117
|
const handler = handlers[originalStep];
|
|
119
118
|
|
|
120
119
|
if (!handler) {
|
|
121
|
-
console.
|
|
120
|
+
console.warn(`[handleToolContinuation] No handler found for ${originalStep}, falling back to handlePersonaResponse`);
|
|
122
121
|
handlePersonaResponse(response, state);
|
|
123
122
|
return;
|
|
124
123
|
}
|
|
@@ -12,8 +12,7 @@ export function handlePersonaTopicRating(response: LLMResponse, state: StateMana
|
|
|
12
12
|
const personaId = response.request.data.personaId as string;
|
|
13
13
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
14
14
|
if (!personaId || !personaDisplayName) {
|
|
15
|
-
|
|
16
|
-
return;
|
|
15
|
+
throw new Error("[handlePersonaTopicRating] Missing personaId or personaDisplayName in request data");
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const result = response.parsed as PersonaTopicRatingResult | undefined;
|
|
@@ -35,8 +34,7 @@ export function handlePersonaTopicRating(response: LLMResponse, state: StateMana
|
|
|
35
34
|
|
|
36
35
|
const persona = state.persona_getById(personaId);
|
|
37
36
|
if (!persona) {
|
|
38
|
-
|
|
39
|
-
return;
|
|
37
|
+
throw new Error(`[handlePersonaTopicRating] Persona not found: ${personaDisplayName}`);
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
const now = new Date().toISOString();
|
|
@@ -20,6 +20,8 @@ import { getEmbeddingService, getItemEmbeddingText } from "../embedding-service.
|
|
|
20
20
|
|
|
21
21
|
import { searchHumanData } from "../human-data-manager.js";
|
|
22
22
|
|
|
23
|
+
const MIN_REWRITE_FLOOR = 750;
|
|
24
|
+
|
|
23
25
|
/**
|
|
24
26
|
* handleRewriteScan — Phase 1 of Rewrite.
|
|
25
27
|
* LLM returns an array of subject strings found in the bloated item.
|
|
@@ -31,20 +33,25 @@ export async function handleRewriteScan(response: LLMResponse, state: StateManag
|
|
|
31
33
|
const rewriteModel = response.request.data.rewriteModel as string;
|
|
32
34
|
|
|
33
35
|
if (!itemId || !itemType) {
|
|
34
|
-
|
|
35
|
-
return;
|
|
36
|
+
throw new Error("[handleRewriteScan] Missing itemId or itemType in request data");
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const subjects = response.parsed as RewriteScanResult | undefined;
|
|
39
40
|
if (!subjects || !Array.isArray(subjects) || subjects.length === 0) {
|
|
40
|
-
console.log(`[handleRewriteScan] No extra subjects found for ${itemType} "${itemId}" —
|
|
41
|
+
console.log(`[handleRewriteScan] No extra subjects found for ${itemType} "${itemId}" — setting rewrite_length_floor`);
|
|
41
42
|
const human = state.getHuman();
|
|
42
43
|
if (itemType === "topic") {
|
|
43
44
|
const topic = human.topics.find(t => t.id === itemId);
|
|
44
|
-
if (topic) state.human_topic_upsert({
|
|
45
|
+
if (topic) state.human_topic_upsert({
|
|
46
|
+
...topic,
|
|
47
|
+
rewrite_length_floor: Math.max(MIN_REWRITE_FLOOR, Math.ceil((topic.description?.length ?? 0) * 1.1)),
|
|
48
|
+
});
|
|
45
49
|
} else if (itemType === "person") {
|
|
46
50
|
const person = human.people.find(p => p.id === itemId);
|
|
47
|
-
if (person) state.human_person_upsert({
|
|
51
|
+
if (person) state.human_person_upsert({
|
|
52
|
+
...person,
|
|
53
|
+
rewrite_length_floor: Math.max(MIN_REWRITE_FLOOR, Math.ceil((person.description?.length ?? 0) * 1.1)),
|
|
54
|
+
});
|
|
48
55
|
}
|
|
49
56
|
return;
|
|
50
57
|
}
|
|
@@ -111,8 +118,7 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
111
118
|
const itemType = response.request.data.itemType as RewriteItemType;
|
|
112
119
|
|
|
113
120
|
if (!itemId || !itemType) {
|
|
114
|
-
|
|
115
|
-
return;
|
|
121
|
+
throw new Error("[handleRewriteRewrite] Missing itemId or itemType in request data");
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
const result = response.parsed as RewriteResult | undefined;
|
|
@@ -171,6 +177,7 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
171
177
|
console.warn(`[handleRewriteRewrite] Failed to compute embedding for existing ${resolvedType} "${item.name}":`, err);
|
|
172
178
|
}
|
|
173
179
|
|
|
180
|
+
const existingFloor = Math.max(MIN_REWRITE_FLOOR, Math.ceil(item.description.length * 1.1));
|
|
174
181
|
switch (resolvedType) {
|
|
175
182
|
case "topic": {
|
|
176
183
|
const existing = human.topics.find(t => t.id === item.id)!;
|
|
@@ -181,6 +188,7 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
181
188
|
sentiment: item.sentiment ?? existing.sentiment,
|
|
182
189
|
last_updated: now,
|
|
183
190
|
embedding,
|
|
191
|
+
rewrite_length_floor: existingFloor,
|
|
184
192
|
});
|
|
185
193
|
break;
|
|
186
194
|
}
|
|
@@ -193,6 +201,7 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
193
201
|
sentiment: item.sentiment ?? existing.sentiment,
|
|
194
202
|
last_updated: now,
|
|
195
203
|
embedding,
|
|
204
|
+
rewrite_length_floor: existingFloor,
|
|
196
205
|
});
|
|
197
206
|
break;
|
|
198
207
|
}
|
|
@@ -216,6 +225,7 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
216
225
|
console.warn(`[handleRewriteRewrite] Failed to compute embedding for new ${item.type} "${item.name}":`, err);
|
|
217
226
|
}
|
|
218
227
|
|
|
228
|
+
const newFloor = Math.max(MIN_REWRITE_FLOOR, Math.ceil(item.description.length * 1.1));
|
|
219
229
|
const baseFields = {
|
|
220
230
|
id: crypto.randomUUID(),
|
|
221
231
|
name: item.name,
|
|
@@ -227,6 +237,7 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
227
237
|
persona_groups: unionGroups,
|
|
228
238
|
interested_personas: unionPersonas,
|
|
229
239
|
embedding,
|
|
240
|
+
rewrite_length_floor: newFloor,
|
|
230
241
|
};
|
|
231
242
|
|
|
232
243
|
switch (item.type) {
|
|
@@ -267,10 +278,16 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
267
278
|
const updatedHuman = state.getHuman();
|
|
268
279
|
if (itemType === "topic") {
|
|
269
280
|
const original = updatedHuman.topics.find(t => t.id === itemId);
|
|
270
|
-
if (original) state.human_topic_upsert({
|
|
281
|
+
if (original) state.human_topic_upsert({
|
|
282
|
+
...original,
|
|
283
|
+
rewrite_length_floor: Math.max(MIN_REWRITE_FLOOR, Math.ceil((original.description?.length ?? 0) * 1.1)),
|
|
284
|
+
});
|
|
271
285
|
} else if (itemType === "person") {
|
|
272
286
|
const original = updatedHuman.people.find(p => p.id === itemId);
|
|
273
|
-
if (original) state.human_person_upsert({
|
|
287
|
+
if (original) state.human_person_upsert({
|
|
288
|
+
...original,
|
|
289
|
+
rewrite_length_floor: Math.max(MIN_REWRITE_FLOOR, Math.ceil((original.description?.length ?? 0) * 1.1)),
|
|
290
|
+
});
|
|
274
291
|
}
|
|
275
292
|
|
|
276
293
|
console.log(`[handleRewriteRewrite] Complete for ${itemType} "${itemId}": ${existingCount} existing updated, ${newCount} new created`);
|
|
@@ -17,8 +17,7 @@ export function handleRoomResponse(response: LLMResponse, state: StateManager):
|
|
|
17
17
|
const parentMessageId = response.request.data.parentMessageId as string | null ?? null;
|
|
18
18
|
|
|
19
19
|
if (!roomId || !personaId) {
|
|
20
|
-
|
|
21
|
-
return;
|
|
20
|
+
throw new Error("[handleRoomResponse] Missing roomId or personaId in request data");
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
const now = new Date().toISOString();
|
|
@@ -111,19 +110,16 @@ export async function handleRoomJudge(response: LLMResponse, state: StateManager
|
|
|
111
110
|
const judgeDisplayName = response.request.data.judgePersonaDisplayName as string;
|
|
112
111
|
|
|
113
112
|
if (!roomId) {
|
|
114
|
-
|
|
115
|
-
return;
|
|
113
|
+
throw new Error("[handleRoomJudge] Missing roomId in request data");
|
|
116
114
|
}
|
|
117
115
|
|
|
118
116
|
if (!response.parsed) {
|
|
119
|
-
|
|
120
|
-
return;
|
|
117
|
+
throw new Error(`[handleRoomJudge] No parsed result from judge ${judgeDisplayName}`);
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
const result = response.parsed as RoomJudgeResult;
|
|
124
121
|
if (!result.winner_message_id) {
|
|
125
|
-
|
|
126
|
-
return;
|
|
122
|
+
throw new Error(`[handleRoomJudge] Judge ${judgeDisplayName} returned no winner_message_id`);
|
|
127
123
|
}
|
|
128
124
|
|
|
129
125
|
const judgePersonaId = response.request.data.judgePersonaId as string;
|
|
@@ -131,16 +127,14 @@ export async function handleRoomJudge(response: LLMResponse, state: StateManager
|
|
|
131
127
|
const allMessages = state.getRoomMessages(roomId);
|
|
132
128
|
const winner = allMessages.find(m => m.id === result.winner_message_id);
|
|
133
129
|
if (!winner) {
|
|
134
|
-
|
|
135
|
-
return;
|
|
130
|
+
throw new Error(`[handleRoomJudge] Winner message ${result.winner_message_id} not found in room ${roomId}`);
|
|
136
131
|
}
|
|
137
132
|
|
|
138
133
|
const verdictParentId = winner.parent_id;
|
|
139
134
|
|
|
140
135
|
const ok = state.setRoomActiveNode(roomId, result.winner_message_id);
|
|
141
136
|
if (!ok) {
|
|
142
|
-
|
|
143
|
-
return;
|
|
137
|
+
throw new Error(`[handleRoomJudge] Could not set active node ${result.winner_message_id} in room ${roomId}`);
|
|
144
138
|
}
|
|
145
139
|
|
|
146
140
|
const losers = allMessages
|
package/src/core/llm-client.ts
CHANGED
|
@@ -76,7 +76,17 @@ export interface LLMRawResponse {
|
|
|
76
76
|
|
|
77
77
|
let llmCallCount = 0;
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
function resolveApiKey(raw: string | undefined): string {
|
|
80
|
+
if (!raw || !raw.startsWith("$")) return raw ?? "";
|
|
81
|
+
const varName = raw.slice(1);
|
|
82
|
+
const resolved =
|
|
83
|
+
(typeof Bun !== "undefined" && (Bun as { env: Record<string, string> }).env?.[varName]) ||
|
|
84
|
+
(typeof process !== "undefined" && process.env?.[varName]);
|
|
85
|
+
if (!resolved) {
|
|
86
|
+
throw new Error(`Provider API key references env var $${varName}, but it is not set.`);
|
|
87
|
+
}
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
80
90
|
|
|
81
91
|
function isGuid(str: string): boolean {
|
|
82
92
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
|
|
@@ -90,7 +100,7 @@ function buildResolvedModel(account: ProviderAccount, model: ModelConfig): Resol
|
|
|
90
100
|
config: {
|
|
91
101
|
name: account.name,
|
|
92
102
|
baseURL: account.url,
|
|
93
|
-
apiKey: account.api_key
|
|
103
|
+
apiKey: resolveApiKey(account.api_key),
|
|
94
104
|
},
|
|
95
105
|
extraHeaders: account.extra_headers,
|
|
96
106
|
};
|
|
@@ -171,7 +181,7 @@ export function resolveModel(modelSpec?: string, accounts?: ProviderAccount[]):
|
|
|
171
181
|
config: {
|
|
172
182
|
name: matchingAccount.name,
|
|
173
183
|
baseURL: matchingAccount.url,
|
|
174
|
-
apiKey: matchingAccount.api_key
|
|
184
|
+
apiKey: resolveApiKey(matchingAccount.api_key),
|
|
175
185
|
},
|
|
176
186
|
extraHeaders: matchingAccount.extra_headers,
|
|
177
187
|
};
|
|
@@ -450,6 +460,7 @@ const JSON_REPAIR_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [
|
|
|
450
460
|
{ pattern: /:\s*(\d{4}-\d{2}-\d{2}T[^"}\],\n]+)/g, replacement: ': "$1"' },
|
|
451
461
|
{ pattern: /:\s*0([1-9][0-9]*)([,\s\n\r\]}])/g, replacement: ": 0.$1$2" },
|
|
452
462
|
{ pattern: /,(\s*[\]}])/g, replacement: "$1" },
|
|
463
|
+
{ pattern: /"(\s*\n[ \t]+"[a-zA-Z_][a-zA-Z0-9_]*"\s*:)/g, replacement: '",$1' },
|
|
453
464
|
];
|
|
454
465
|
|
|
455
466
|
export function repairJSON(jsonStr: string): string {
|
|
@@ -529,6 +540,41 @@ export function rescueGemmaToolCalls(content: string): unknown[] {
|
|
|
529
540
|
return rescued;
|
|
530
541
|
}
|
|
531
542
|
|
|
543
|
+
function findOutermostObject(str: string): string | null {
|
|
544
|
+
const start = str.indexOf('{');
|
|
545
|
+
if (start === -1) return null;
|
|
546
|
+
|
|
547
|
+
let depth = 0;
|
|
548
|
+
let inString = false;
|
|
549
|
+
let escaped = false;
|
|
550
|
+
|
|
551
|
+
for (let i = start; i < str.length; i++) {
|
|
552
|
+
const ch = str[i];
|
|
553
|
+
|
|
554
|
+
if (escaped) {
|
|
555
|
+
escaped = false;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (ch === '\\' && inString) {
|
|
559
|
+
escaped = true;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (ch === '"') {
|
|
563
|
+
inString = !inString;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (inString) continue;
|
|
567
|
+
|
|
568
|
+
if (ch === '{') depth++;
|
|
569
|
+
else if (ch === '}') {
|
|
570
|
+
depth--;
|
|
571
|
+
if (depth === 0) return str.slice(start, i + 1);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
532
578
|
export function parseJSONResponse(content: string): unknown {
|
|
533
579
|
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
534
580
|
const jsonStr = jsonMatch ? jsonMatch[1].trim() : content.trim();
|
|
@@ -541,10 +587,10 @@ export function parseJSONResponse(content: string): unknown {
|
|
|
541
587
|
return JSON.parse(repaired);
|
|
542
588
|
} catch {
|
|
543
589
|
// Last resort: extract the outermost {...} block from mixed prose/JSON content.
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
590
|
+
// Bracket-depth scan (not greedy regex) stops at the first valid close so extra
|
|
591
|
+
// trailing braces from models like Gemma are excluded from the extracted slice.
|
|
592
|
+
const extracted = findOutermostObject(jsonStr);
|
|
593
|
+
if (extracted) {
|
|
548
594
|
try {
|
|
549
595
|
return JSON.parse(extracted);
|
|
550
596
|
} catch {
|
|
@@ -255,8 +255,6 @@ export function checkAndQueueHumanExtraction(
|
|
|
255
255
|
const unextractedPeople = sm.messages_getUnextracted(personaId, "p", undefined, "exclude");
|
|
256
256
|
const peopleThreshold = Math.min(EXTRACTION_TAPER_CAP, human.people.length);
|
|
257
257
|
if (unextractedPeople.length > 0 && unextractedPeople.length >= peopleThreshold) {
|
|
258
|
-
const personaForScan = sm.persona_getById(personaId);
|
|
259
|
-
const personScanOptions = personaForScan?.pending_update ? { reflection_progress: 1 } : undefined;
|
|
260
258
|
const context: ExtractionContext = {
|
|
261
259
|
personaId,
|
|
262
260
|
channelDisplayName: personaDisplayName,
|
|
@@ -264,9 +262,9 @@ export function checkAndQueueHumanExtraction(
|
|
|
264
262
|
messages_analyze: unextractedPeople,
|
|
265
263
|
extraction_flag: "p",
|
|
266
264
|
};
|
|
267
|
-
queuePersonScan(context, sm
|
|
265
|
+
queuePersonScan(context, sm);
|
|
268
266
|
console.log(
|
|
269
|
-
`[Processor] Human Seed extraction: people (threshold: ${peopleThreshold}, unextracted: ${unextractedPeople.length}
|
|
267
|
+
`[Processor] Human Seed extraction: people (threshold: ${peopleThreshold}, unextracted: ${unextractedPeople.length})`
|
|
270
268
|
);
|
|
271
269
|
}
|
|
272
270
|
}
|