ei-tui 0.9.4 → 1.0.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 +22 -3
- package/package.json +5 -1
- package/src/README.md +9 -25
- package/src/core/handlers/document-segmentation.ts +113 -0
- package/src/core/handlers/human-extraction.ts +16 -16
- package/src/core/handlers/index.ts +2 -0
- package/src/core/handlers/rewrite.ts +13 -9
- package/src/core/heartbeat-manager.ts +2 -2
- package/src/core/llm-client.ts +66 -6
- package/src/core/message-manager.ts +20 -18
- package/src/core/orchestrators/ceremony.ts +83 -40
- package/src/core/orchestrators/human-extraction.ts +5 -1
- package/src/core/persona-manager.ts +4 -0
- package/src/core/processor.ts +90 -1
- package/src/core/queue-manager.ts +35 -0
- package/src/core/queue-processor.ts +13 -13
- package/src/core/state/queue.ts +9 -1
- package/src/core/state-manager.ts +10 -6
- package/src/core/types/entities.ts +15 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +2 -0
- package/src/core/types/llm.ts +9 -0
- package/src/integrations/document/chunker.ts +88 -0
- package/src/integrations/document/importer.ts +82 -0
- package/src/integrations/document/index.ts +2 -0
- package/src/integrations/document/invoice.ts +63 -0
- package/src/integrations/document/types.ts +16 -0
- package/src/integrations/document/unsource.ts +164 -0
- package/src/integrations/persona-history/importer.ts +197 -0
- package/src/integrations/persona-history/index.ts +3 -0
- package/src/integrations/persona-history/types.ts +7 -0
- package/src/prompts/ceremony/dedup.ts +7 -3
- package/src/prompts/ceremony/index.ts +2 -1
- package/src/prompts/ceremony/people-rewrite.ts +190 -0
- package/src/prompts/ceremony/{rewrite.ts → topic-rewrite.ts} +103 -78
- package/src/prompts/human/person-scan.ts +13 -4
- package/src/prompts/human/topic-scan.ts +16 -2
- package/src/prompts/human/topic-update.ts +36 -4
- package/src/prompts/human/types.ts +1 -0
- package/src/storage/indexed.ts +4 -0
- package/src/storage/interface.ts +1 -0
- package/src/storage/local.ts +4 -0
- package/src/templates/emmett.ts +49 -0
- package/tui/README.md +25 -2
- package/tui/src/app.tsx +9 -6
- package/tui/src/commands/delete.tsx +7 -1
- package/tui/src/commands/import.tsx +30 -0
- package/tui/src/commands/unsource.tsx +115 -0
- package/tui/src/components/PromptInput.tsx +4 -0
- package/tui/src/components/WelcomeOverlay.tsx +58 -32
- package/tui/src/context/ei.tsx +80 -60
- package/tui/src/index.tsx +14 -0
- package/tui/src/storage/file.ts +11 -5
- package/tui/src/util/e2e-flags.ts +4 -3
- package/tui/src/util/help-content.ts +20 -0
- package/tui/src/util/logger.ts +1 -1
- package/tui/src/util/provider-detection.ts +251 -0
- package/tui/src/util/yaml-human.ts +7 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic
|
|
1
|
+
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
import { normalizeRoomMessages } from "../handlers/utils.js";
|
|
4
4
|
import { applyDecayToValue } from "../utils/index.js";
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
} from "./human-extraction.js";
|
|
13
13
|
import { queuePersonaTopicRating, type PersonaTopicContext, type PersonaTopicOptions } from "./persona-topics.js";
|
|
14
14
|
import { getRoomVisibleMessages, queueRoomHumanExtraction } from "./room-extraction.js";
|
|
15
|
-
import {
|
|
15
|
+
import { type RewriteItemType } from "../../prompts/ceremony/index.js";
|
|
16
|
+
import { buildPersonRewriteScanPrompt } from "../../prompts/ceremony/people-rewrite.js";
|
|
17
|
+
import { buildTopicRewriteScanPrompt } from "../../prompts/ceremony/topic-rewrite.js";
|
|
16
18
|
import { buildReflectionCriticPrompt } from "../../prompts/reflection/index.js";
|
|
17
19
|
import { getModelForPersona } from "../heartbeat-manager.js";
|
|
18
20
|
|
|
@@ -51,7 +53,7 @@ export function shouldStartCeremony(config: CeremonyConfig, state: StateManager,
|
|
|
51
53
|
* Start the ceremony by queuing Exposure scans for all active personas with recent activity.
|
|
52
54
|
*
|
|
53
55
|
* IMPORTANT: Sets last_ceremony FIRST to prevent re-triggering from the processor loop.
|
|
54
|
-
* The actual Decay →
|
|
56
|
+
* The actual Decay → Person Rewrite → Topic Rewrite phases happen later via handleCeremonyProgress
|
|
55
57
|
* once all exposure scans have completed.
|
|
56
58
|
*/
|
|
57
59
|
export function startCeremony(state: StateManager): void {
|
|
@@ -167,7 +169,7 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
|
|
|
167
169
|
* AND at the end of startCeremony (for the zero-messages edge case).
|
|
168
170
|
*
|
|
169
171
|
* If any ceremony_progress items remain in the queue, does nothing — more work pending.
|
|
170
|
-
* Phase 1: Dedup → Phase 2: Expose → Phase 3: EventSummary → Decay →
|
|
172
|
+
* Phase 1: Dedup → Phase 2: Expose → Phase 3: EventSummary → Decay → Phase 4: Person Rewrite → Topic Rewrite (fire-and-forget)
|
|
171
173
|
*/
|
|
172
174
|
export function handleCeremonyProgress(state: StateManager, lastPhase: number): void {
|
|
173
175
|
if (state.queue_hasPendingCeremonies()) {
|
|
@@ -236,6 +238,12 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
236
238
|
return;
|
|
237
239
|
}
|
|
238
240
|
|
|
241
|
+
if (lastPhase === 4) {
|
|
242
|
+
console.log("[ceremony:progress] Person Rewrite complete, starting Topic Rewrite");
|
|
243
|
+
queueTopicRewritePhase(state);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
239
247
|
if (lastPhase === 2) {
|
|
240
248
|
console.log("[ceremony:progress] Expose complete, starting EventSummary phase");
|
|
241
249
|
const options: ExtractionOptions = { ceremony_progress: 3 };
|
|
@@ -249,7 +257,7 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
249
257
|
return;
|
|
250
258
|
}
|
|
251
259
|
|
|
252
|
-
// Phase 3 (EventSummary) complete → advance to Decay/Prune
|
|
260
|
+
// Phase 3 (EventSummary) complete → advance to Decay/Prune then Person Rewrite (phase 4)
|
|
253
261
|
console.log("[ceremony:progress] EventSummary complete, advancing to Decay");
|
|
254
262
|
|
|
255
263
|
const personas = state.persona_getAll();
|
|
@@ -276,8 +284,16 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
276
284
|
// Human ceremony: decay topics + people
|
|
277
285
|
runHumanCeremony(state);
|
|
278
286
|
|
|
279
|
-
// Rewrite phase
|
|
280
|
-
|
|
287
|
+
// Person Rewrite phase (phase 4): scan bloated Person records, extract Topics from them.
|
|
288
|
+
// Gated via ceremony_progress so Topic Rewrite can run after — Topics created here
|
|
289
|
+
// need to be visible before Topic Rewrite snapshots the threshold.
|
|
290
|
+
queuePersonRewritePhase(state);
|
|
291
|
+
|
|
292
|
+
// Zero-work guard: if no person rewrites queued, advance to topic rewrite immediately
|
|
293
|
+
if (!state.queue_hasPendingCeremonies()) {
|
|
294
|
+
console.log("[ceremony:progress] No person rewrite work, advancing to Topic Rewrite");
|
|
295
|
+
handleCeremonyProgress(state, 4);
|
|
296
|
+
}
|
|
281
297
|
|
|
282
298
|
// Reflection phase: fire-and-forget critic calls for persona person records above threshold
|
|
283
299
|
queueReflectionPhase(state);
|
|
@@ -441,15 +457,6 @@ export function runHumanCeremony(state: StateManager): void {
|
|
|
441
457
|
|
|
442
458
|
const REWRITE_DESCRIPTION_THRESHOLD = 750;
|
|
443
459
|
|
|
444
|
-
/**
|
|
445
|
-
* Queue Phase 1 "scan" for every human data item whose description exceeds the
|
|
446
|
-
* threshold. Gated on rewrite_model being set in HumanSettings.
|
|
447
|
-
*
|
|
448
|
-
* Fire-and-forget: no ceremony_progress, no blocking. Expire/Explore proceed
|
|
449
|
-
* immediately since they only touch persona topics (zero overlap with human data).
|
|
450
|
-
* Phase 2 items enqueue at Normal priority, naturally processing before more
|
|
451
|
-
* Low-priority Phase 1 scans.
|
|
452
|
-
*/
|
|
453
460
|
/**
|
|
454
461
|
* Forces an unconditional, threshold-bypassing Person scan on Apply/Dismiss.
|
|
455
462
|
* Cannot be replaced by checkAndQueueHumanExtraction — that function gates on
|
|
@@ -479,41 +486,77 @@ export function queueReflectionDrain(personaId: string, state: StateManager): vo
|
|
|
479
486
|
console.log(`[reflection:drain] Queued Person scan for ${persona.display_name} (${unextractedPeople.length} messages) — clears on completion`);
|
|
480
487
|
}
|
|
481
488
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
489
|
+
function getRewriteModel(state: StateManager): string | undefined {
|
|
490
|
+
return state.getHuman().settings?.rewrite_model;
|
|
491
|
+
}
|
|
485
492
|
|
|
493
|
+
export function queuePersonRewritePhase(state: StateManager): void {
|
|
494
|
+
const rewriteModel = getRewriteModel(state);
|
|
486
495
|
if (!rewriteModel) {
|
|
487
|
-
console.log("[ceremony:rewrite] rewrite_model not set — skipping rewrite phase");
|
|
496
|
+
console.log("[ceremony:rewrite] rewrite_model not set — skipping person rewrite phase");
|
|
488
497
|
return;
|
|
489
498
|
}
|
|
490
499
|
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
for (const topic of human.topics) {
|
|
494
|
-
if ((topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD && !topic.rewrite_checked) {
|
|
495
|
-
itemsToScan.push({ item: topic, type: "topic" });
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
for (const person of human.people) {
|
|
500
|
+
const human = state.getHuman();
|
|
501
|
+
const personsToScan = human.people.filter(person => {
|
|
499
502
|
const isPersonaLinked = (person.identifiers ?? []).some(
|
|
500
503
|
i => i.type.toLowerCase() === 'ei persona'
|
|
501
504
|
);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
+
return !isPersonaLinked
|
|
506
|
+
&& (person.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD
|
|
507
|
+
&& !person.rewrite_checked;
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (personsToScan.length === 0) {
|
|
511
|
+
console.log("[ceremony:rewrite] No persons above threshold — skipping person rewrite phase");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log(`[ceremony:rewrite] Found ${personsToScan.length} person(s) above ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing person rewrite scans`);
|
|
516
|
+
|
|
517
|
+
for (const person of personsToScan) {
|
|
518
|
+
const prompt = buildPersonRewriteScanPrompt({ item: person, itemType: "person" });
|
|
519
|
+
state.queue_enqueue({
|
|
520
|
+
type: LLMRequestType.JSON,
|
|
521
|
+
priority: LLMPriority.Low,
|
|
522
|
+
system: prompt.system,
|
|
523
|
+
user: prompt.user,
|
|
524
|
+
next_step: LLMNextStep.HandleRewriteScan,
|
|
525
|
+
model: rewriteModel,
|
|
526
|
+
data: {
|
|
527
|
+
itemId: person.id,
|
|
528
|
+
itemType: "person" as RewriteItemType,
|
|
529
|
+
rewriteModel,
|
|
530
|
+
ceremony_progress: 4,
|
|
531
|
+
},
|
|
532
|
+
});
|
|
505
533
|
}
|
|
506
534
|
|
|
507
|
-
|
|
508
|
-
|
|
535
|
+
console.log(`[ceremony:rewrite] Queued ${personsToScan.length} person rewrite scan(s)`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function queueTopicRewritePhase(state: StateManager): void {
|
|
539
|
+
const rewriteModel = getRewriteModel(state);
|
|
540
|
+
if (!rewriteModel) {
|
|
541
|
+
console.log("[ceremony:rewrite] rewrite_model not set — skipping topic rewrite phase");
|
|
509
542
|
return;
|
|
510
543
|
}
|
|
511
544
|
|
|
512
|
-
|
|
545
|
+
const human = state.getHuman();
|
|
546
|
+
const topicsToScan = human.topics.filter(topic =>
|
|
547
|
+
(topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD
|
|
548
|
+
&& !topic.rewrite_checked
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
if (topicsToScan.length === 0) {
|
|
552
|
+
console.log("[ceremony:rewrite] No topics above threshold — skipping topic rewrite phase");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
513
555
|
|
|
514
|
-
|
|
515
|
-
const prompt = buildRewriteScanPrompt({ item, itemType: type });
|
|
556
|
+
console.log(`[ceremony:rewrite] Found ${topicsToScan.length} topic(s) above ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing topic rewrite scans`);
|
|
516
557
|
|
|
558
|
+
for (const topic of topicsToScan) {
|
|
559
|
+
const prompt = buildTopicRewriteScanPrompt({ item: topic, itemType: "topic" });
|
|
517
560
|
state.queue_enqueue({
|
|
518
561
|
type: LLMRequestType.JSON,
|
|
519
562
|
priority: LLMPriority.Low,
|
|
@@ -522,14 +565,14 @@ export function queueRewritePhase(state: StateManager): void {
|
|
|
522
565
|
next_step: LLMNextStep.HandleRewriteScan,
|
|
523
566
|
model: rewriteModel,
|
|
524
567
|
data: {
|
|
525
|
-
itemId:
|
|
526
|
-
itemType:
|
|
527
|
-
rewriteModel,
|
|
568
|
+
itemId: topic.id,
|
|
569
|
+
itemType: "topic" as RewriteItemType,
|
|
570
|
+
rewriteModel,
|
|
528
571
|
},
|
|
529
572
|
});
|
|
530
573
|
}
|
|
531
574
|
|
|
532
|
-
console.log(`[ceremony:rewrite] Queued ${
|
|
575
|
+
console.log(`[ceremony:rewrite] Queued ${topicsToScan.length} topic rewrite scan(s)`);
|
|
533
576
|
}
|
|
534
577
|
|
|
535
578
|
function queueEventSummaryForAll(state: StateManager, options?: ExtractionOptions): void {
|
|
@@ -164,6 +164,7 @@ export function queueTopicScan(context: ExtractionContext, state: StateManager,
|
|
|
164
164
|
messages_context: chunk.messages_context,
|
|
165
165
|
messages_analyze: chunk.messages_analyze,
|
|
166
166
|
participant_context: buildParticipantContext(context.personaId, state),
|
|
167
|
+
technical_context: (context.sources?.length ?? 0) > 0,
|
|
167
168
|
});
|
|
168
169
|
|
|
169
170
|
state.queue_enqueue({
|
|
@@ -275,6 +276,7 @@ export function queueDirectTopicUpdate(
|
|
|
275
276
|
messages_analyze: chunk.messages_analyze,
|
|
276
277
|
persona_name: chunk.channelDisplayName,
|
|
277
278
|
participant_context: buildParticipantContext(context.personaId, state),
|
|
279
|
+
technical_context: (context.sources?.length ?? 0) > 0,
|
|
278
280
|
});
|
|
279
281
|
|
|
280
282
|
state.queue_enqueue({
|
|
@@ -291,6 +293,7 @@ export function queueDirectTopicUpdate(
|
|
|
291
293
|
existingItemId: topic.id,
|
|
292
294
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
293
295
|
extraction_model: extractionModel,
|
|
296
|
+
sources: context.sources,
|
|
294
297
|
},
|
|
295
298
|
});
|
|
296
299
|
}
|
|
@@ -306,7 +309,7 @@ const EMBEDDING_MIN_SIMILARITY = 0.3;
|
|
|
306
309
|
* Higher than EMBEDDING_MIN_SIMILARITY (0.3) because we need near-duplicates,
|
|
307
310
|
* not just vague thematic overlap.
|
|
308
311
|
*/
|
|
309
|
-
export const VALIDATE_MIN_SIMILARITY = 0.
|
|
312
|
+
export const VALIDATE_MIN_SIMILARITY = 0.92;
|
|
310
313
|
|
|
311
314
|
/**
|
|
312
315
|
* Queue a topic match request using embedding-based similarity (topics only).
|
|
@@ -425,6 +428,7 @@ export function queueTopicUpdate(
|
|
|
425
428
|
messages_analyze: chunk.messages_analyze,
|
|
426
429
|
persona_name: chunk.channelDisplayName,
|
|
427
430
|
participant_context: buildParticipantContext(primaryPersonaId, state),
|
|
431
|
+
technical_context: (context.sources?.length ?? 0) > 0,
|
|
428
432
|
});
|
|
429
433
|
|
|
430
434
|
state.queue_enqueue({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RESERVED_PERSONA_NAMES,
|
|
3
3
|
isReservedPersonaName,
|
|
4
|
+
isReservedPersonaId,
|
|
4
5
|
type PersonaSummary,
|
|
5
6
|
type PersonaEntity,
|
|
6
7
|
type PersonaCreationInput,
|
|
@@ -110,6 +111,9 @@ export async function deletePersona(
|
|
|
110
111
|
personaId: string,
|
|
111
112
|
_deleteHumanData: boolean
|
|
112
113
|
): Promise<boolean> {
|
|
114
|
+
if (isReservedPersonaId(personaId)) {
|
|
115
|
+
throw new Error(`Cannot delete reserved persona "${personaId}". Use archive instead.`);
|
|
116
|
+
}
|
|
113
117
|
const persona = sm.persona_getById(personaId);
|
|
114
118
|
if (!persona) return false;
|
|
115
119
|
sm.persona_delete(personaId);
|
package/src/core/processor.ts
CHANGED
|
@@ -39,7 +39,9 @@ import { ContextStatus as ContextStatusEnum, RoomMode } from "./types.js";
|
|
|
39
39
|
import { registerReadMemoryExecutor, registerFileReadExecutor } from "./tools/index.js";
|
|
40
40
|
import { createReadMemoryExecutor } from "./tools/builtin/read-memory.js";
|
|
41
41
|
import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
|
|
42
|
+
import { EMMETT_PERSONA_DEFINITION } from "../templates/emmett.js";
|
|
42
43
|
import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueReflectionDrain, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction, queueTargetedPersonUpdate, queueTargetedTopicUpdate } from "./orchestrators/index.js";
|
|
44
|
+
import { finishDocumentBatch } from "./handlers/document-segmentation.js";
|
|
43
45
|
import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
|
|
44
46
|
import { DEFAULT_SEED_TRAITS } from "./constants/seed-traits.js";
|
|
45
47
|
|
|
@@ -132,6 +134,8 @@ import {
|
|
|
132
134
|
markAllRoomMessagesRead,
|
|
133
135
|
} from "./room-manager.js";
|
|
134
136
|
import type { RoomCreationInput, RoomEntity, RoomMessage, RoomSummary } from "./types.js";
|
|
137
|
+
import { previewUnsource as _previewUnsource } from "../integrations/document/unsource.js";
|
|
138
|
+
import type { UnsourcePreview, UnsourceResult } from "../integrations/document/unsource.js";
|
|
135
139
|
|
|
136
140
|
const DEFAULT_LOOP_INTERVAL_MS = 100;
|
|
137
141
|
const DEFAULT_OPENCODE_POLLING_MS = 60000;
|
|
@@ -282,6 +286,46 @@ export class Processor {
|
|
|
282
286
|
this.interface.onMessageAdded?.(eiEntity.id);
|
|
283
287
|
}
|
|
284
288
|
|
|
289
|
+
private bootstrapEmmett(): void {
|
|
290
|
+
const existing = this.stateManager.persona_getById("emmet");
|
|
291
|
+
if (existing) {
|
|
292
|
+
if (existing.is_archived) {
|
|
293
|
+
this.stateManager.persona_unarchive("emmet");
|
|
294
|
+
}
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const readMemoryTool = this.stateManager.tools_getByName("read_memory");
|
|
298
|
+
const emmettEntity: PersonaEntity = {
|
|
299
|
+
...EMMETT_PERSONA_DEFINITION,
|
|
300
|
+
id: "emmet",
|
|
301
|
+
display_name: "Emmett",
|
|
302
|
+
last_updated: new Date().toISOString(),
|
|
303
|
+
tools: readMemoryTool ? [readMemoryTool.id] : [],
|
|
304
|
+
};
|
|
305
|
+
this.stateManager.persona_add(emmettEntity);
|
|
306
|
+
this.interface.onPersonaAdded?.();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async importDocument(content: string, filename: string): Promise<import("../integrations/document/types.js").DocumentImportResult> {
|
|
310
|
+
this.bootstrapEmmett();
|
|
311
|
+
const { importDocument } = await import("../integrations/document/importer.js");
|
|
312
|
+
return importDocument({
|
|
313
|
+
stateManager: this.stateManager,
|
|
314
|
+
interface: this.interface,
|
|
315
|
+
content,
|
|
316
|
+
filename,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
getUnsourcePreview(sourceTag: string): UnsourcePreview {
|
|
321
|
+
return _previewUnsource(sourceTag, this.stateManager);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async executeUnsource(preview: UnsourcePreview): Promise<UnsourceResult> {
|
|
325
|
+
const { executeUnsource } = await import("../integrations/document/unsource.js");
|
|
326
|
+
return executeUnsource(preview, this.stateManager);
|
|
327
|
+
}
|
|
328
|
+
|
|
285
329
|
/**
|
|
286
330
|
* Seed built-in tool providers and tools if they don't exist yet.
|
|
287
331
|
* Called on every startup (after state load/restore) — safe to call repeatedly.
|
|
@@ -1168,6 +1212,15 @@ const toolNextSteps = new Set([
|
|
|
1168
1212
|
await this.checkAndSyncCursor(human, now);
|
|
1169
1213
|
}
|
|
1170
1214
|
|
|
1215
|
+
if (
|
|
1216
|
+
this.isTUI &&
|
|
1217
|
+
human.settings?.personaHistory?.integration &&
|
|
1218
|
+
!human.settings.personaHistory.complete &&
|
|
1219
|
+
this.stateManager.queue_length() === 0
|
|
1220
|
+
) {
|
|
1221
|
+
await this.checkAndSyncPersonaHistory(human);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1171
1224
|
if (human.settings?.ceremony && shouldStartCeremony(human.settings.ceremony, this.stateManager)) {
|
|
1172
1225
|
if (human.settings?.sync && remoteSync.isConfigured()) {
|
|
1173
1226
|
const state = this.stateManager.getStorageState();
|
|
@@ -1180,7 +1233,7 @@ const toolNextSteps = new Set([
|
|
|
1180
1233
|
}
|
|
1181
1234
|
|
|
1182
1235
|
for (const persona of this.stateManager.persona_getAll()) {
|
|
1183
|
-
if (persona.is_paused || persona.is_archived) continue;
|
|
1236
|
+
if (persona.is_paused || persona.is_archived || persona.is_static) continue;
|
|
1184
1237
|
|
|
1185
1238
|
const defaultHeartbeatMs = this.stateManager.getHuman().settings?.default_heartbeat_ms ?? 1800000;
|
|
1186
1239
|
const heartbeatDelay = persona.heartbeat_delay_ms ?? defaultHeartbeatMs;
|
|
@@ -1408,6 +1461,32 @@ const toolNextSteps = new Set([
|
|
|
1408
1461
|
});
|
|
1409
1462
|
}
|
|
1410
1463
|
|
|
1464
|
+
private personaHistoryImportInProgress = false;
|
|
1465
|
+
|
|
1466
|
+
private async checkAndSyncPersonaHistory(_human: HumanEntity): Promise<void> {
|
|
1467
|
+
if (this.personaHistoryImportInProgress) return;
|
|
1468
|
+
|
|
1469
|
+
this.personaHistoryImportInProgress = true;
|
|
1470
|
+
import("../integrations/persona-history/importer.js")
|
|
1471
|
+
.then(({ importPersonaHistory }) =>
|
|
1472
|
+
importPersonaHistory({ stateManager: this.stateManager })
|
|
1473
|
+
)
|
|
1474
|
+
.then((result) => {
|
|
1475
|
+
if (result.scansQueued > 0) {
|
|
1476
|
+
console.log(
|
|
1477
|
+
`[Processor] PersonaHistory: ${result.scansQueued} scans queued` +
|
|
1478
|
+
(result.complete ? " — import complete" : "")
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
})
|
|
1482
|
+
.catch((err) => {
|
|
1483
|
+
console.warn(`[Processor] PersonaHistory sync failed:`, err);
|
|
1484
|
+
})
|
|
1485
|
+
.finally(() => {
|
|
1486
|
+
this.personaHistoryImportInProgress = false;
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1411
1490
|
private augmentRoomRequest(request: LLMRequest): LLMRequest {
|
|
1412
1491
|
if (request.next_step !== LLMNextStep.HandleRoomResponse) return request;
|
|
1413
1492
|
|
|
@@ -1675,6 +1754,16 @@ const toolNextSteps = new Set([
|
|
|
1675
1754
|
if (typeof response.request.data.ceremony_progress === "number") {
|
|
1676
1755
|
handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
|
|
1677
1756
|
}
|
|
1757
|
+
|
|
1758
|
+
if (response.request.next_step === LLMNextStep.HandleDocumentSegmentation) {
|
|
1759
|
+
const batchId = response.request.data.batchId as string;
|
|
1760
|
+
const filename = response.request.data.filename as string;
|
|
1761
|
+
if (batchId && !this.stateManager.queue_hasPendingDocumentSegments(batchId)) {
|
|
1762
|
+
finishDocumentBatch(batchId, filename, this.stateManager);
|
|
1763
|
+
this.interface.onMessageAdded?.("emmet");
|
|
1764
|
+
this.interface.onHumanUpdated?.();
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1678
1767
|
} catch (err) {
|
|
1679
1768
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1680
1769
|
const result = this.stateManager.queue_fail(response.request.id, errorMsg);
|
|
@@ -18,6 +18,39 @@ export async function resumeQueue(sm: StateManager): Promise<void> {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export async function getQueueStatus(sm: StateManager): Promise<QueueStatus> {
|
|
21
|
+
const activeItems = sm.queue_getAllActiveItems();
|
|
22
|
+
const segmentationItems = activeItems.filter(
|
|
23
|
+
r => r.next_step === LLMNextStep.HandleDocumentSegmentation
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const batchMap = new Map<string, { filename: string; count: number }>();
|
|
27
|
+
for (const item of segmentationItems) {
|
|
28
|
+
const { batchId, filename } = item.data as { batchId: string; filename: string };
|
|
29
|
+
if (!batchId || !filename) continue;
|
|
30
|
+
const existing = batchMap.get(batchId);
|
|
31
|
+
if (existing) {
|
|
32
|
+
existing.count++;
|
|
33
|
+
} else {
|
|
34
|
+
batchMap.set(batchId, { filename, count: 1 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const pending_documents = batchMap.size > 0
|
|
39
|
+
? Array.from(batchMap.entries()).map(([batchId, { filename, count }]) => ({ batchId, filename, count }))
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
const extractingSet = new Set<string>();
|
|
43
|
+
for (const item of activeItems) {
|
|
44
|
+
const sources = item.data.sources as string[] | undefined;
|
|
45
|
+
if (!Array.isArray(sources)) continue;
|
|
46
|
+
for (const s of sources) {
|
|
47
|
+
if (typeof s === "string" && s.startsWith("import:document:")) {
|
|
48
|
+
extractingSet.add(s.slice("import:document:".length));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const extracting_documents = extractingSet.size > 0 ? Array.from(extractingSet) : undefined;
|
|
53
|
+
|
|
21
54
|
return {
|
|
22
55
|
state: sm.queue_isPaused()
|
|
23
56
|
? "paused"
|
|
@@ -27,6 +60,8 @@ export async function getQueueStatus(sm: StateManager): Promise<QueueStatus> {
|
|
|
27
60
|
pending_count: sm.queue_length(),
|
|
28
61
|
dlq_count: sm.queue_dlqLength(),
|
|
29
62
|
embedding_warning: sm.embedding_getWarning() || undefined,
|
|
63
|
+
pending_documents,
|
|
64
|
+
extracting_documents,
|
|
30
65
|
};
|
|
31
66
|
}
|
|
32
67
|
|
|
@@ -200,7 +200,7 @@ export class QueueProcessor {
|
|
|
200
200
|
hydratedUser,
|
|
201
201
|
messages,
|
|
202
202
|
request.model,
|
|
203
|
-
{ signal: this.abortController?.signal, tools: openAITools, onUsageUpdate: this.currentOnUsageUpdate },
|
|
203
|
+
{ signal: this.abortController?.signal, tools: openAITools, onUsageUpdate: this.currentOnUsageUpdate, nextStep: `${request.data.originalNextStep ?? request.next_step}+tool_continuation` },
|
|
204
204
|
this.currentAccounts
|
|
205
205
|
);
|
|
206
206
|
|
|
@@ -219,7 +219,7 @@ export class QueueProcessor {
|
|
|
219
219
|
if (!args.should_respond && args.content) {
|
|
220
220
|
args.should_respond = true;
|
|
221
221
|
}
|
|
222
|
-
|
|
222
|
+
console.debug(`[QueueProcessor] submit tool "${submitCall.name}" called — returning arguments as parsed response`);
|
|
223
223
|
return {
|
|
224
224
|
request,
|
|
225
225
|
success: true,
|
|
@@ -297,9 +297,9 @@ export class QueueProcessor {
|
|
|
297
297
|
const isHeartbeat = request.next_step === LLMNextStep.HandleHeartbeatCheck || request.next_step === LLMNextStep.HandleEiHeartbeat;
|
|
298
298
|
if (isHeartbeat) {
|
|
299
299
|
const personaName = request.data.personaDisplayName as string | undefined ?? 'Ei';
|
|
300
|
-
console.
|
|
300
|
+
console.debug(`[${personaName} Heartbeat] LLM call - tools offered: ${openAITools.length} (${activeTools.map(t => t.name).join(', ') || 'none'})`);
|
|
301
301
|
} else {
|
|
302
|
-
console.
|
|
302
|
+
console.debug(`[QueueProcessor] LLM call for ${request.next_step}, tools=${openAITools.length}`);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
const { content, finishReason, rawToolCalls, assistantMessage, thinking } = await callLLMRaw(
|
|
@@ -307,18 +307,18 @@ export class QueueProcessor {
|
|
|
307
307
|
hydratedUser,
|
|
308
308
|
messages,
|
|
309
309
|
request.model,
|
|
310
|
-
{ signal: this.abortController?.signal, tools: openAITools, onUsageUpdate: this.currentOnUsageUpdate },
|
|
310
|
+
{ signal: this.abortController?.signal, tools: openAITools, onUsageUpdate: this.currentOnUsageUpdate, nextStep: request.next_step },
|
|
311
311
|
this.currentAccounts
|
|
312
312
|
);
|
|
313
313
|
if (thinking) {
|
|
314
|
-
console.
|
|
314
|
+
console.debug(`[QueueProcessor] Extended thinking on ${request.next_step} (${thinking.length} chars) — TODO(#13): stream to TUI`);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
// =========================================================================
|
|
318
318
|
// Tool call path: execute tools, enqueue HandleToolContinuation, done.
|
|
319
319
|
// =========================================================================
|
|
320
320
|
if (finishReason === "tool_calls" && rawToolCalls?.length) {
|
|
321
|
-
console.
|
|
321
|
+
console.debug(`[QueueProcessor] finish_reason=tool_calls — executing tools, will enqueue HandleToolContinuation`);
|
|
322
322
|
|
|
323
323
|
const toolCalls = parseToolCalls(rawToolCalls);
|
|
324
324
|
if (toolCalls.length === 0) {
|
|
@@ -364,7 +364,7 @@ export class QueueProcessor {
|
|
|
364
364
|
});
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
console.
|
|
367
|
+
console.debug(`[QueueProcessor] Tool execution complete: ${results.length} result(s). Enqueueing HandleToolContinuation.`);
|
|
368
368
|
|
|
369
369
|
if (this.currentOnEnqueue) {
|
|
370
370
|
this.currentOnEnqueue({
|
|
@@ -412,7 +412,7 @@ export class QueueProcessor {
|
|
|
412
412
|
// =========================================================================
|
|
413
413
|
// Normal stop path
|
|
414
414
|
// =========================================================================
|
|
415
|
-
console.
|
|
415
|
+
console.debug(`[QueueProcessor] finish_reason="${finishReason}" — normal stop`);
|
|
416
416
|
return this.handleResponseType(request, content ?? "", finishReason);
|
|
417
417
|
}
|
|
418
418
|
|
|
@@ -497,9 +497,9 @@ export class QueueProcessor {
|
|
|
497
497
|
const { content: reformatContent, finishReason: reformatReason } = await callLLMRaw(
|
|
498
498
|
request.system,
|
|
499
499
|
reformatUserPrompt,
|
|
500
|
-
messages,
|
|
500
|
+
messages,
|
|
501
501
|
request.model,
|
|
502
|
-
{ signal: this.abortController?.signal, onUsageUpdate: this.currentOnUsageUpdate },
|
|
502
|
+
{ signal: this.abortController?.signal, onUsageUpdate: this.currentOnUsageUpdate, nextStep: `${request.data.originalNextStep ?? request.next_step}+prose_reformat` },
|
|
503
503
|
this.currentAccounts
|
|
504
504
|
);
|
|
505
505
|
|
|
@@ -554,9 +554,9 @@ export class QueueProcessor {
|
|
|
554
554
|
const { content: reformatContent, finishReason: reformatReason } = await callLLMRaw(
|
|
555
555
|
request.system,
|
|
556
556
|
reformatUserPrompt,
|
|
557
|
-
[],
|
|
557
|
+
[],
|
|
558
558
|
request.model,
|
|
559
|
-
{ signal: this.abortController?.signal, onUsageUpdate: this.currentOnUsageUpdate },
|
|
559
|
+
{ signal: this.abortController?.signal, onUsageUpdate: this.currentOnUsageUpdate, nextStep: `${request.next_step}+json_reformat` },
|
|
560
560
|
this.currentAccounts
|
|
561
561
|
);
|
|
562
562
|
|
package/src/core/state/queue.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LLMRequest, QueueFailResult } from "../types.js";
|
|
2
|
-
import { DLQ_MAX_COUNT, DLQ_MAX_AGE_DAYS } from "../types.js";
|
|
2
|
+
import { DLQ_MAX_COUNT, DLQ_MAX_AGE_DAYS, LLMNextStep } from "../types.js";
|
|
3
3
|
|
|
4
4
|
const BASE_BACKOFF_MS = 2_000;
|
|
5
5
|
const MAX_BACKOFF_MS = 30_000;
|
|
@@ -200,6 +200,14 @@ export class QueueState {
|
|
|
200
200
|
return this.queue.some(r => r.state !== "dlq" && typeof r.data.ceremony_progress === "number" && r.data.ceremony_progress > 0);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
hasPendingDocumentSegments(batchId: string): boolean {
|
|
204
|
+
return this.queue.some(r =>
|
|
205
|
+
r.state !== "dlq" &&
|
|
206
|
+
r.next_step === LLMNextStep.HandleDocumentSegmentation &&
|
|
207
|
+
r.data.batchId === batchId
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
203
211
|
clear(): number {
|
|
204
212
|
const count = this.queue.filter(r => r.state !== "dlq").length;
|
|
205
213
|
this.queue = this.queue.filter(r => r.state === "dlq");
|
|
@@ -917,6 +917,10 @@ export class StateManager {
|
|
|
917
917
|
return this.queueState.hasPendingCeremonies();
|
|
918
918
|
}
|
|
919
919
|
|
|
920
|
+
queue_hasPendingDocumentSegments(batchId: string): boolean {
|
|
921
|
+
return this.queueState.hasPendingDocumentSegments(batchId);
|
|
922
|
+
}
|
|
923
|
+
|
|
920
924
|
queue_clear(): number {
|
|
921
925
|
const result = this.queueState.clear();
|
|
922
926
|
this.scheduleSave();
|
|
@@ -1050,7 +1054,7 @@ export class StateManager {
|
|
|
1050
1054
|
tools_getForPersona(personaId: string, isTUI: boolean): ToolDefinition[] {
|
|
1051
1055
|
const persona = this.personaState.getById(personaId);
|
|
1052
1056
|
if (!persona?.tools?.length) {
|
|
1053
|
-
console.
|
|
1057
|
+
console.debug(`[Tools] tools_getForPersona(${personaId}): persona has no assigned tools`);
|
|
1054
1058
|
return [];
|
|
1055
1059
|
}
|
|
1056
1060
|
const assignedIds = new Set(persona.tools);
|
|
@@ -1073,13 +1077,13 @@ export class StateManager {
|
|
|
1073
1077
|
if (result.length < assignedIds.size) {
|
|
1074
1078
|
for (const id of assignedIds) {
|
|
1075
1079
|
const tool = this.tools.find(t => t.id === id);
|
|
1076
|
-
if (!tool) { console.
|
|
1077
|
-
if (!tool.enabled) { console.
|
|
1078
|
-
if (!enabledProviderIds.has(tool.provider_id)) { console.
|
|
1079
|
-
if (!(tool.runtime === "any" || (tool.runtime === "node" && isTUI))) { console.
|
|
1080
|
+
if (!tool) { console.debug(`[Tools] tools_getForPersona: assigned tool id=${id} not found in registry`); continue; }
|
|
1081
|
+
if (!tool.enabled) { console.debug(`[Tools] tools_getForPersona: tool "${tool.name}" is disabled`); continue; }
|
|
1082
|
+
if (!enabledProviderIds.has(tool.provider_id)) { console.debug(`[Tools] tools_getForPersona: tool "${tool.name}" provider is disabled`); continue; }
|
|
1083
|
+
if (!(tool.runtime === "any" || (tool.runtime === "node" && isTUI))) { console.debug(`[Tools] tools_getForPersona: tool "${tool.name}" runtime "${tool.runtime}" not available (isTUI=${isTUI})`); continue; }
|
|
1080
1084
|
}
|
|
1081
1085
|
}
|
|
1082
|
-
console.
|
|
1086
|
+
console.debug(`[Tools] tools_getForPersona(${personaId}): resolved ${result.length}/${assignedIds.size} tools: [${result.map(t => t.name).join(", ")}]`);
|
|
1083
1087
|
return result;
|
|
1084
1088
|
}
|
|
1085
1089
|
|
|
@@ -20,6 +20,11 @@ export interface OpenCodeSettings {
|
|
|
20
20
|
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export interface DocumentSettings {
|
|
24
|
+
extraction_model?: string;
|
|
25
|
+
processed_documents?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export interface CeremonyConfig {
|
|
24
29
|
time: string; // "HH:MM" format (e.g., "09:00")
|
|
25
30
|
last_ceremony?: string; // ISO timestamp
|
|
@@ -117,8 +122,10 @@ export interface HumanSettings {
|
|
|
117
122
|
backup?: BackupConfig;
|
|
118
123
|
claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
119
124
|
cursor?: import("../../integrations/cursor/types.js").CursorSettings;
|
|
125
|
+
document?: DocumentSettings;
|
|
120
126
|
active_theme?: string;
|
|
121
127
|
custom_themes?: ThemeDefinition[];
|
|
128
|
+
personaHistory?: import("../../integrations/persona-history/types.js").PersonaHistorySettings;
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
export interface HumanEntity {
|
|
@@ -202,3 +209,11 @@ export type ReservedPersonaName = typeof RESERVED_PERSONA_NAMES[number];
|
|
|
202
209
|
export function isReservedPersonaName(name: string): boolean {
|
|
203
210
|
return RESERVED_PERSONA_NAMES.includes(name.toLowerCase() as ReservedPersonaName);
|
|
204
211
|
}
|
|
212
|
+
|
|
213
|
+
// Reserved persona IDs (built-in system personas that cannot be deleted)
|
|
214
|
+
export const RESERVED_PERSONA_IDS = ["ei", "emmet"] as const;
|
|
215
|
+
export type ReservedPersonaId = typeof RESERVED_PERSONA_IDS[number];
|
|
216
|
+
|
|
217
|
+
export function isReservedPersonaId(id: string): boolean {
|
|
218
|
+
return (RESERVED_PERSONA_IDS as readonly string[]).includes(id);
|
|
219
|
+
}
|
package/src/core/types/enums.ts
CHANGED
|
@@ -52,6 +52,7 @@ export enum LLMNextStep {
|
|
|
52
52
|
HandlePersonaPreview = "handlePersonaPreview",
|
|
53
53
|
HandleTopicValidate = "handleTopicValidate",
|
|
54
54
|
HandleReflectionCritic = "handleReflectionCritic",
|
|
55
|
+
HandleDocumentSegmentation = "handleDocumentSegmentation",
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export enum ProviderType {
|
|
@@ -75,6 +75,8 @@ export interface QueueStatus {
|
|
|
75
75
|
current_operation?: string;
|
|
76
76
|
/** True when the embedding service failed and topic/person matching fell back to recent items. */
|
|
77
77
|
embedding_warning?: boolean;
|
|
78
|
+
pending_documents?: Array<{ batchId: string; filename: string; count: number }>;
|
|
79
|
+
extracting_documents?: string[];
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
export interface EiError {
|