@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/unify/engine.js +176 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.599",
3
+ "version": "0.1.601",
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,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: stripMetaForWire([...conversationMessages]),
954
+ messages: wireMessages,
781
955
  tools: toolDefs.length > 0 ? toolDefs : undefined,
782
956
  maxTokens: this.#config.maxOutputTokens || 16384,
783
957
  effort: resolvedEffort,