@yeaft/webchat-agent 0.1.599 → 0.1.601
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/unify/engine.js +176 -2
package/package.json
CHANGED
package/unify/engine.js
CHANGED
|
@@ -21,7 +21,12 @@ import { randomUUID } from 'crypto';
|
|
|
21
21
|
import { buildSystemPrompt, buildWorkerPrompt } from './prompts.js';
|
|
22
22
|
import { LLMContextError, LLMAbortError } from './llm/adapter.js';
|
|
23
23
|
import { recallR6, formatForInjection } from './memory/recall-r6.js';
|
|
24
|
-
import { shouldConsolidate, consolidate } from './memory/consolidate.js';
|
|
24
|
+
import { shouldConsolidate, consolidate, partitionMessages } from './memory/consolidate.js';
|
|
25
|
+
import { extractMemories } from './memory/extract.js';
|
|
26
|
+
import { runCompact as runCompactOrchestrator } from './compact/orchestrator.js';
|
|
27
|
+
import { evaluateCompactTriggers } from './compact/triggers.js';
|
|
28
|
+
import { archiveTurn } from './archive/turn-archive.js';
|
|
29
|
+
import { archiveToolResults } from './archive/tool-results.js';
|
|
25
30
|
import { buildMemoryInjection } from './memory/layout.js';
|
|
26
31
|
import { buildUserProfile } from './memory/user-memory-store.js';
|
|
27
32
|
import { readSummary as readScopeSummary } from './memory/scope-tree.js';
|
|
@@ -530,6 +535,35 @@ export class Engine {
|
|
|
530
535
|
if (this.#config._readOnly) return null;
|
|
531
536
|
|
|
532
537
|
const budget = this.#config.messageTokenBudget || 8192;
|
|
538
|
+
const compactCfg = (this.#config && this.#config.compact) || {};
|
|
539
|
+
|
|
540
|
+
// Phase 8 PR-D: orchestrator opt-in. evaluateCompactTriggers (DESIGN
|
|
541
|
+
// §4.1) and runCompact (DESIGN §4.2) are the new path. Existing
|
|
542
|
+
// shouldConsolidate / consolidate stays as the default to preserve
|
|
543
|
+
// production behaviour; flip via config.compact.useOrchestrator=true.
|
|
544
|
+
if (compactCfg.useOrchestrator) {
|
|
545
|
+
return this.#runOrchestratorCompact(budget, compactCfg);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Default path — surface trigger reasons via trace for observability
|
|
549
|
+
// even when we still fall through to the legacy consolidate.
|
|
550
|
+
try {
|
|
551
|
+
const messages = this.#conversationStore.loadAll();
|
|
552
|
+
const tokenCount = this.#conversationStore.hotTokens();
|
|
553
|
+
const trig = evaluateCompactTriggers({
|
|
554
|
+
messages,
|
|
555
|
+
tokenCount,
|
|
556
|
+
contextLimit: this.#config.maxContextTokens || 200000,
|
|
557
|
+
tokenRatio: compactCfg.tokenRatio,
|
|
558
|
+
maxMessages: compactCfg.maxMessages,
|
|
559
|
+
});
|
|
560
|
+
this.#trace.logEvent && this.#trace.logEvent({
|
|
561
|
+
traceId: 'compact_triggers_eval',
|
|
562
|
+
eventType: 'compact_triggers_eval',
|
|
563
|
+
eventData: { trigger: trig.trigger, reasons: trig.reasons },
|
|
564
|
+
});
|
|
565
|
+
} catch { /* observability only */ }
|
|
566
|
+
|
|
533
567
|
if (!shouldConsolidate(this.#conversationStore, budget)) return null;
|
|
534
568
|
|
|
535
569
|
try {
|
|
@@ -547,6 +581,117 @@ export class Engine {
|
|
|
547
581
|
}
|
|
548
582
|
}
|
|
549
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Phase 8 PR-D: run compact via the new orchestrator (DESIGN §4.2).
|
|
586
|
+
* Hooks adapt the orchestrator's injectable contract to the existing
|
|
587
|
+
* conversationStore / memoryStore primitives, so behaviour matches
|
|
588
|
+
* the legacy `consolidate` path 1:1 while exercising the new
|
|
589
|
+
* triggers / turn-group / orchestrator code on the live path.
|
|
590
|
+
*
|
|
591
|
+
* @param {number} budget
|
|
592
|
+
* @param {object} compactCfg
|
|
593
|
+
* @returns {Promise<{archivedCount:number, extractedCount:number}|null>}
|
|
594
|
+
*/
|
|
595
|
+
async #runOrchestratorCompact(budget, _compactCfg) {
|
|
596
|
+
const conversationStore = this.#conversationStore;
|
|
597
|
+
const memoryStore = this.#memoryStore;
|
|
598
|
+
const adapter = this.#adapter;
|
|
599
|
+
const fastConfig = this.#fastConfig;
|
|
600
|
+
|
|
601
|
+
let messages;
|
|
602
|
+
try {
|
|
603
|
+
messages = conversationStore.loadAll();
|
|
604
|
+
} catch { return null; }
|
|
605
|
+
if (!Array.isArray(messages) || messages.length === 0) return null;
|
|
606
|
+
|
|
607
|
+
const tokenCount = conversationStore.hotTokens();
|
|
608
|
+
const trig = evaluateCompactTriggers({
|
|
609
|
+
messages,
|
|
610
|
+
tokenCount,
|
|
611
|
+
contextLimit: this.#config.maxContextTokens || 200000,
|
|
612
|
+
});
|
|
613
|
+
if (!trig.trigger) return null;
|
|
614
|
+
|
|
615
|
+
// Use partitionMessages (the legacy primitive) to decide what is
|
|
616
|
+
// "cooling": orchestrator's own keepHot is a count, but we want to
|
|
617
|
+
// honour the token-budget partitioning the rest of the system uses.
|
|
618
|
+
const { toArchive } = partitionMessages(messages, budget);
|
|
619
|
+
if (toArchive.length === 0) return null;
|
|
620
|
+
|
|
621
|
+
const archiveIds = [];
|
|
622
|
+
|
|
623
|
+
const hooks = {
|
|
624
|
+
summarise: async () => {
|
|
625
|
+
// Reuse the legacy consolidate path's summary technique by
|
|
626
|
+
// invoking adapter directly with a fresh prompt. We keep the
|
|
627
|
+
// orchestrator's contract honoured: it takes the cooling slice
|
|
628
|
+
// and returns a string.
|
|
629
|
+
try {
|
|
630
|
+
const result = await adapter.call({
|
|
631
|
+
model: fastConfig.model,
|
|
632
|
+
system: 'You are a conversation summarizer. Summarize concisely in 2–3 paragraphs, preserving decisions, facts, and context.',
|
|
633
|
+
messages: [{ role: 'user', content: `Summarize:\n\n${toArchive.map(m => `[${m.role}] ${(m.content || '').slice(0, 500)}`).join('\n\n')}` }],
|
|
634
|
+
maxTokens: 1024,
|
|
635
|
+
});
|
|
636
|
+
return (result.text || '').trim();
|
|
637
|
+
} catch {
|
|
638
|
+
return '';
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
archive: async (_groupIdx, groupMsgs) => {
|
|
642
|
+
for (const m of groupMsgs) if (m.id) archiveIds.push(m.id);
|
|
643
|
+
const turnId = groupMsgs[0]?.id || `g_${Date.now()}`;
|
|
644
|
+
// Phase 8 PR-E: persist the cooling turn to
|
|
645
|
+
// <yeaftDir>/memory/archive/<turnId>.md so message_trace can
|
|
646
|
+
// replay it later. Scope is "user/" by default — group/task
|
|
647
|
+
// scoping is a follow-up that will arrive with multi-VP archive
|
|
648
|
+
// routing. Best-effort: archive failure must not abort compact.
|
|
649
|
+
if (this.#yeaftDir) {
|
|
650
|
+
try {
|
|
651
|
+
await archiveTurn({
|
|
652
|
+
root: `${this.#yeaftDir}/memory`,
|
|
653
|
+
scopeDir: 'user',
|
|
654
|
+
turnId,
|
|
655
|
+
messages: groupMsgs,
|
|
656
|
+
});
|
|
657
|
+
} catch { /* best-effort */ }
|
|
658
|
+
}
|
|
659
|
+
return { turnId };
|
|
660
|
+
},
|
|
661
|
+
extract: async (coolingMessages) => {
|
|
662
|
+
try {
|
|
663
|
+
const extracted = await extractMemories({
|
|
664
|
+
messages: coolingMessages, adapter, config: fastConfig,
|
|
665
|
+
});
|
|
666
|
+
for (const e of extracted) memoryStore.writeEntry(e);
|
|
667
|
+
if (extracted.length > 0) memoryStore.rebuildScopes();
|
|
668
|
+
return { written: extracted.length };
|
|
669
|
+
} catch {
|
|
670
|
+
return { written: 0 };
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const out = await runCompactOrchestrator({
|
|
677
|
+
messages, keepHot: 10, hooks,
|
|
678
|
+
});
|
|
679
|
+
// Apply side effects to the conversation store: move archived
|
|
680
|
+
// ids to cold, persist compact summary, update index.
|
|
681
|
+
if (archiveIds.length > 0) conversationStore.moveToColdBatch(archiveIds);
|
|
682
|
+
if (out.compactSummary) conversationStore.updateCompactSummary(out.compactSummary);
|
|
683
|
+
const lastKept = messages[messages.length - 1];
|
|
684
|
+
conversationStore.updateIndex({ lastMessageId: lastKept?.id || null });
|
|
685
|
+
|
|
686
|
+
return {
|
|
687
|
+
archivedCount: out.archivedMessages,
|
|
688
|
+
extractedCount: out.extractedCount,
|
|
689
|
+
};
|
|
690
|
+
} catch {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
550
695
|
/**
|
|
551
696
|
* Run a query — the main loop.
|
|
552
697
|
*
|
|
@@ -773,11 +918,40 @@ export class Engine {
|
|
|
773
918
|
}
|
|
774
919
|
}
|
|
775
920
|
|
|
921
|
+
// Phase 8 PR-E: archive bulky tool results before they go on the
|
|
922
|
+
// wire. archiveToolResults walks the messages array and replaces
|
|
923
|
+
// any `role:'tool'` body older than turnAgeMin AND larger than
|
|
924
|
+
// lengthMin with a small stub, persisting the original to
|
|
925
|
+
// <yeaftDir>/memory/<scopeDir>/archive/tool-results/<id>.md so
|
|
926
|
+
// message_trace can fetch it on demand. The stub keeps the
|
|
927
|
+
// OpenAI/Anthropic toolCallId pairing intact.
|
|
928
|
+
let wireMessages = stripMetaForWire([...conversationMessages]);
|
|
929
|
+
if (this.#yeaftDir && (this.#config?.archive?.toolResults !== false)) {
|
|
930
|
+
try {
|
|
931
|
+
const swept = await archiveToolResults({
|
|
932
|
+
root: `${this.#yeaftDir}/memory`,
|
|
933
|
+
scopeDir: 'user',
|
|
934
|
+
messages: wireMessages,
|
|
935
|
+
turnAgeMin: this.#config?.archive?.turnAgeMin,
|
|
936
|
+
lengthMin: this.#config?.archive?.lengthMin,
|
|
937
|
+
});
|
|
938
|
+
wireMessages = swept.nextMessages;
|
|
939
|
+
// Mutate the in-memory conversation array so subsequent turns
|
|
940
|
+
// see the stub too — without this, the next turn re-archives
|
|
941
|
+
// the same body.
|
|
942
|
+
if (swept.archivedCount > 0) {
|
|
943
|
+
for (let i = 0; i < conversationMessages.length; i += 1) {
|
|
944
|
+
conversationMessages[i] = wireMessages[i];
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} catch { /* best-effort */ }
|
|
948
|
+
}
|
|
949
|
+
|
|
776
950
|
// Stream from adapter
|
|
777
951
|
for await (const event of this.#adapter.stream({
|
|
778
952
|
model: currentModel,
|
|
779
953
|
system: systemPrompt,
|
|
780
|
-
messages:
|
|
954
|
+
messages: wireMessages,
|
|
781
955
|
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
782
956
|
maxTokens: this.#config.maxOutputTokens || 16384,
|
|
783
957
|
effort: resolvedEffort,
|