@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/unify/engine.js +157 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.598",
3
+ "version": "0.1.600",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
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
- const resolvedEffort = pickEffort({ scenario, toolLoopTurns, userEffort });
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({