@yeaft/webchat-agent 0.1.598 → 0.1.600
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 +157 -2
package/package.json
CHANGED
package/unify/engine.js
CHANGED
|
@@ -21,7 +21,10 @@ 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';
|
|
25
28
|
import { buildMemoryInjection } from './memory/layout.js';
|
|
26
29
|
import { buildUserProfile } from './memory/user-memory-store.js';
|
|
27
30
|
import { readSummary as readScopeSummary } from './memory/scope-tree.js';
|
|
@@ -30,6 +33,7 @@ import { getThreadStore, MAIN_THREAD_ID } from './threads/store.js';
|
|
|
30
33
|
import { pickEffort, parseEffortPrefix } from './effort.js';
|
|
31
34
|
import { normalizeEffort } from './models.js';
|
|
32
35
|
import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
|
|
36
|
+
import { resolveThinking } from './router/thinking.js';
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
39
|
* task-324 — Turn cap removed.
|
|
@@ -529,6 +533,35 @@ export class Engine {
|
|
|
529
533
|
if (this.#config._readOnly) return null;
|
|
530
534
|
|
|
531
535
|
const budget = this.#config.messageTokenBudget || 8192;
|
|
536
|
+
const compactCfg = (this.#config && this.#config.compact) || {};
|
|
537
|
+
|
|
538
|
+
// Phase 8 PR-D: orchestrator opt-in. evaluateCompactTriggers (DESIGN
|
|
539
|
+
// §4.1) and runCompact (DESIGN §4.2) are the new path. Existing
|
|
540
|
+
// shouldConsolidate / consolidate stays as the default to preserve
|
|
541
|
+
// production behaviour; flip via config.compact.useOrchestrator=true.
|
|
542
|
+
if (compactCfg.useOrchestrator) {
|
|
543
|
+
return this.#runOrchestratorCompact(budget, compactCfg);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Default path — surface trigger reasons via trace for observability
|
|
547
|
+
// even when we still fall through to the legacy consolidate.
|
|
548
|
+
try {
|
|
549
|
+
const messages = this.#conversationStore.loadAll();
|
|
550
|
+
const tokenCount = this.#conversationStore.hotTokens();
|
|
551
|
+
const trig = evaluateCompactTriggers({
|
|
552
|
+
messages,
|
|
553
|
+
tokenCount,
|
|
554
|
+
contextLimit: this.#config.maxContextTokens || 200000,
|
|
555
|
+
tokenRatio: compactCfg.tokenRatio,
|
|
556
|
+
maxMessages: compactCfg.maxMessages,
|
|
557
|
+
});
|
|
558
|
+
this.#trace.logEvent && this.#trace.logEvent({
|
|
559
|
+
traceId: 'compact_triggers_eval',
|
|
560
|
+
eventType: 'compact_triggers_eval',
|
|
561
|
+
eventData: { trigger: trig.trigger, reasons: trig.reasons },
|
|
562
|
+
});
|
|
563
|
+
} catch { /* observability only */ }
|
|
564
|
+
|
|
532
565
|
if (!shouldConsolidate(this.#conversationStore, budget)) return null;
|
|
533
566
|
|
|
534
567
|
try {
|
|
@@ -546,6 +579,101 @@ export class Engine {
|
|
|
546
579
|
}
|
|
547
580
|
}
|
|
548
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Phase 8 PR-D: run compact via the new orchestrator (DESIGN §4.2).
|
|
584
|
+
* Hooks adapt the orchestrator's injectable contract to the existing
|
|
585
|
+
* conversationStore / memoryStore primitives, so behaviour matches
|
|
586
|
+
* the legacy `consolidate` path 1:1 while exercising the new
|
|
587
|
+
* triggers / turn-group / orchestrator code on the live path.
|
|
588
|
+
*
|
|
589
|
+
* @param {number} budget
|
|
590
|
+
* @param {object} compactCfg
|
|
591
|
+
* @returns {Promise<{archivedCount:number, extractedCount:number}|null>}
|
|
592
|
+
*/
|
|
593
|
+
async #runOrchestratorCompact(budget, _compactCfg) {
|
|
594
|
+
const conversationStore = this.#conversationStore;
|
|
595
|
+
const memoryStore = this.#memoryStore;
|
|
596
|
+
const adapter = this.#adapter;
|
|
597
|
+
const fastConfig = this.#fastConfig;
|
|
598
|
+
|
|
599
|
+
let messages;
|
|
600
|
+
try {
|
|
601
|
+
messages = conversationStore.loadAll();
|
|
602
|
+
} catch { return null; }
|
|
603
|
+
if (!Array.isArray(messages) || messages.length === 0) return null;
|
|
604
|
+
|
|
605
|
+
const tokenCount = conversationStore.hotTokens();
|
|
606
|
+
const trig = evaluateCompactTriggers({
|
|
607
|
+
messages,
|
|
608
|
+
tokenCount,
|
|
609
|
+
contextLimit: this.#config.maxContextTokens || 200000,
|
|
610
|
+
});
|
|
611
|
+
if (!trig.trigger) return null;
|
|
612
|
+
|
|
613
|
+
// Use partitionMessages (the legacy primitive) to decide what is
|
|
614
|
+
// "cooling": orchestrator's own keepHot is a count, but we want to
|
|
615
|
+
// honour the token-budget partitioning the rest of the system uses.
|
|
616
|
+
const { toArchive } = partitionMessages(messages, budget);
|
|
617
|
+
if (toArchive.length === 0) return null;
|
|
618
|
+
|
|
619
|
+
const archiveIds = [];
|
|
620
|
+
|
|
621
|
+
const hooks = {
|
|
622
|
+
summarise: async () => {
|
|
623
|
+
// Reuse the legacy consolidate path's summary technique by
|
|
624
|
+
// invoking adapter directly with a fresh prompt. We keep the
|
|
625
|
+
// orchestrator's contract honoured: it takes the cooling slice
|
|
626
|
+
// and returns a string.
|
|
627
|
+
try {
|
|
628
|
+
const result = await adapter.call({
|
|
629
|
+
model: fastConfig.model,
|
|
630
|
+
system: 'You are a conversation summarizer. Summarize concisely in 2–3 paragraphs, preserving decisions, facts, and context.',
|
|
631
|
+
messages: [{ role: 'user', content: `Summarize:\n\n${toArchive.map(m => `[${m.role}] ${(m.content || '').slice(0, 500)}`).join('\n\n')}` }],
|
|
632
|
+
maxTokens: 1024,
|
|
633
|
+
});
|
|
634
|
+
return (result.text || '').trim();
|
|
635
|
+
} catch {
|
|
636
|
+
return '';
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
archive: async (_groupIdx, groupMsgs) => {
|
|
640
|
+
for (const m of groupMsgs) if (m.id) archiveIds.push(m.id);
|
|
641
|
+
return { turnId: groupMsgs[0]?.id || `g_${Date.now()}` };
|
|
642
|
+
},
|
|
643
|
+
extract: async (coolingMessages) => {
|
|
644
|
+
try {
|
|
645
|
+
const extracted = await extractMemories({
|
|
646
|
+
messages: coolingMessages, adapter, config: fastConfig,
|
|
647
|
+
});
|
|
648
|
+
for (const e of extracted) memoryStore.writeEntry(e);
|
|
649
|
+
if (extracted.length > 0) memoryStore.rebuildScopes();
|
|
650
|
+
return { written: extracted.length };
|
|
651
|
+
} catch {
|
|
652
|
+
return { written: 0 };
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const out = await runCompactOrchestrator({
|
|
659
|
+
messages, keepHot: 10, hooks,
|
|
660
|
+
});
|
|
661
|
+
// Apply side effects to the conversation store: move archived
|
|
662
|
+
// ids to cold, persist compact summary, update index.
|
|
663
|
+
if (archiveIds.length > 0) conversationStore.moveToColdBatch(archiveIds);
|
|
664
|
+
if (out.compactSummary) conversationStore.updateCompactSummary(out.compactSummary);
|
|
665
|
+
const lastKept = messages[messages.length - 1];
|
|
666
|
+
conversationStore.updateIndex({ lastMessageId: lastKept?.id || null });
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
archivedCount: out.archivedMessages,
|
|
670
|
+
extractedCount: out.extractedCount,
|
|
671
|
+
};
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
549
677
|
/**
|
|
550
678
|
* Run a query — the main loop.
|
|
551
679
|
*
|
|
@@ -743,7 +871,34 @@ export class Engine {
|
|
|
743
871
|
try {
|
|
744
872
|
// task-327b: resolve effort per-turn so the long-loop auto-bump
|
|
745
873
|
// kicks in once toolLoopTurns crosses the threshold.
|
|
746
|
-
|
|
874
|
+
let resolvedEffort = pickEffort({ scenario, toolLoopTurns, userEffort });
|
|
875
|
+
|
|
876
|
+
// DESIGN.md §9.16: thinking-mode precedence chain. When a VP
|
|
877
|
+
// persona is active, the router/continuity bookkeeping has more
|
|
878
|
+
// signal than the raw scenario tag — the prior assistant turn's
|
|
879
|
+
// routerPlan, the VP's role default, and the global config all
|
|
880
|
+
// outrank the scenario picker for `'high'|'max'`. UI/userEffort
|
|
881
|
+
// is already honoured by pickEffort (highest precedence).
|
|
882
|
+
if (vpPersona && vpPersona.vpId) {
|
|
883
|
+
const priorPlan = extractPriorPlan(conversationMessages, vpPersona.vpId);
|
|
884
|
+
const thinkingCfg = (this.#config && this.#config.thinking) || {};
|
|
885
|
+
const resolved = resolveThinking({
|
|
886
|
+
uiOverride: (userEffort === 'max' || userEffort === 'high') ? userEffort : null,
|
|
887
|
+
routerPlan: null, // PR-C scope: priorPlan continuity only;
|
|
888
|
+
// live router-plan thinking is a follow-up.
|
|
889
|
+
priorPlan: priorPlan && priorPlan.thinking ? priorPlan.thinking : null,
|
|
890
|
+
vpDefault: typeof vpPersona.thinking === 'string' ? vpPersona.thinking : null,
|
|
891
|
+
globalDefault: typeof thinkingCfg.default === 'string' ? thinkingCfg.default : null,
|
|
892
|
+
allowRouterEscalate: thinkingCfg.allowRouterEscalate !== false,
|
|
893
|
+
});
|
|
894
|
+
// Only adopt the chain's choice when it strengthens the
|
|
895
|
+
// baseline. We never weaken below pickEffort (e.g. consolidate
|
|
896
|
+
// = 'max' must not be downgraded to 'high' just because the VP
|
|
897
|
+
// default is 'high').
|
|
898
|
+
if (resolved.value === 'max' || (resolved.value === 'high' && resolvedEffort === 'low')) {
|
|
899
|
+
resolvedEffort = resolved.value;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
747
902
|
|
|
748
903
|
// Stream from adapter
|
|
749
904
|
for await (const event of this.#adapter.stream({
|