@yeaft/webchat-agent 0.1.408 → 0.1.410

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/unify/engine.js CHANGED
@@ -2,22 +2,33 @@
2
2
  * engine.js — Yeaft query loop
3
3
  *
4
4
  * The engine is the core orchestrator:
5
- * 1. Build messages array
6
- * 2. Call adapter.stream()
7
- * 3. Collect text + tool_calls from stream events
8
- * 4. If tool_calls execute tools append results → goto 2
9
- * 5. If end_turndone
10
- * 6. If max_tokensdone (Phase 2: auto-continue)
5
+ * 1. Before first turn: recall memories → inject into system prompt
6
+ * 2. Build messages array (with compact summary if available)
7
+ * 3. Call adapter.stream()
8
+ * 4. Collect text + tool_calls from stream events
9
+ * 5. If tool_callsexecute tools → append results → goto 3
10
+ * 6. If end_turnpersist messages check consolidation → done
11
+ * 7. If max_tokens → auto-continue (up to maxContinueTurns)
12
+ * 8. On LLMContextError → force compact → retry
13
+ * 9. On retryable error with fallbackModel → switch model → retry
11
14
  *
12
15
  * Pattern derived from Claude Code's query loop (src/query.ts).
16
+ *
17
+ * Reference: yeaft-unify-implementation-plan.md §3.1, §4 (Phase 2)
13
18
  */
14
19
 
15
20
  import { randomUUID } from 'crypto';
16
21
  import { buildSystemPrompt } from './prompts.js';
22
+ import { LLMContextError } from './llm/adapter.js';
23
+ import { recall } from './memory/recall.js';
24
+ import { shouldConsolidate, consolidate } from './memory/consolidate.js';
17
25
 
18
26
  /** Maximum number of turns before the engine stops to prevent infinite loops. */
19
27
  const MAX_TURNS = 25;
20
28
 
29
+ /** Maximum auto-continue turns when stopReason is 'max_tokens'. */
30
+ const MAX_CONTINUE_TURNS = 3;
31
+
21
32
  // ─── Engine Events (superset of adapter events) ──────────────────
22
33
 
23
34
  /**
@@ -25,8 +36,11 @@ const MAX_TURNS = 25;
25
36
  * @typedef {{ type: 'turn_end', turnNumber: number, stopReason: string }} TurnEndEvent
26
37
  * @typedef {{ type: 'tool_start', id: string, name: string, input: object }} ToolStartEvent
27
38
  * @typedef {{ type: 'tool_end', id: string, name: string, output: string, isError: boolean }} ToolEndEvent
39
+ * @typedef {{ type: 'consolidate', archivedCount: number, extractedCount: number }} ConsolidateEvent
40
+ * @typedef {{ type: 'recall', entryCount: number, cached: boolean }} RecallEvent
41
+ * @typedef {{ type: 'fallback', from: string, to: string, reason: string }} FallbackEvent
28
42
  *
29
- * @typedef {import('./llm/adapter.js').StreamEvent | TurnStartEvent | TurnEndEvent | ToolStartEvent | ToolEndEvent} EngineEvent
43
+ * @typedef {import('./llm/adapter.js').StreamEvent | TurnStartEvent | TurnEndEvent | ToolStartEvent | ToolEndEvent | ConsolidateEvent | RecallEvent | FallbackEvent} EngineEvent
30
44
  */
31
45
 
32
46
  // ─── Engine ──────────────────────────────────────────────────────
@@ -47,15 +61,29 @@ export class Engine {
47
61
  /** @type {string} */
48
62
  #traceId;
49
63
 
64
+ /** @type {import('./conversation/persist.js').ConversationStore|null} */
65
+ #conversationStore;
66
+
67
+ /** @type {import('./memory/store.js').MemoryStore|null} */
68
+ #memoryStore;
69
+
50
70
  /**
51
- * @param {{ adapter: import('./llm/adapter.js').LLMAdapter, trace: object, config: object }} params
71
+ * @param {{
72
+ * adapter: import('./llm/adapter.js').LLMAdapter,
73
+ * trace: object,
74
+ * config: object,
75
+ * conversationStore?: import('./conversation/persist.js').ConversationStore,
76
+ * memoryStore?: import('./memory/store.js').MemoryStore
77
+ * }} params
52
78
  */
53
- constructor({ adapter, trace, config }) {
79
+ constructor({ adapter, trace, config, conversationStore, memoryStore }) {
54
80
  this.#adapter = adapter;
55
81
  this.#trace = trace;
56
82
  this.#config = config;
57
83
  this.#tools = new Map();
58
84
  this.#traceId = randomUUID();
85
+ this.#conversationStore = conversationStore || null;
86
+ this.#memoryStore = memoryStore || null;
59
87
  }
60
88
 
61
89
  /**
@@ -94,19 +122,120 @@ export class Engine {
94
122
  }
95
123
 
96
124
  /**
97
- * Build the system prompt.
125
+ * Build the system prompt with memory and compact summary.
98
126
  *
99
- * @param {string} mode — 'chat' | 'work' | 'dream'
127
+ * @param {string} mode
128
+ * @param {{ profile?: string, entries?: object[] }} [memory]
129
+ * @param {string} [compactSummary]
100
130
  * @returns {string}
101
131
  */
102
- #buildSystemPrompt(mode) {
132
+ #buildSystemPrompt(mode, memory, compactSummary) {
103
133
  return buildSystemPrompt({
104
134
  language: this.#config.language || 'en',
105
135
  mode,
106
136
  toolNames: Array.from(this.#tools.keys()),
137
+ memory,
138
+ compactSummary,
107
139
  });
108
140
  }
109
141
 
142
+ /**
143
+ * Perform memory recall for a given prompt.
144
+ *
145
+ * @param {string} prompt
146
+ * @returns {Promise<{ profile: string, entries: object[] }|null>}
147
+ */
148
+ async #recallMemory(prompt) {
149
+ if (!this.#memoryStore) return null;
150
+
151
+ const memory = { profile: '', entries: [] };
152
+
153
+ // Read user profile
154
+ memory.profile = this.#memoryStore.readProfile();
155
+
156
+ // Recall relevant entries
157
+ try {
158
+ const result = await recall({
159
+ prompt,
160
+ adapter: this.#adapter,
161
+ config: this.#config,
162
+ memoryStore: this.#memoryStore,
163
+ });
164
+ memory.entries = result.entries;
165
+ } catch {
166
+ // Recall failure is non-critical
167
+ }
168
+
169
+ return memory;
170
+ }
171
+
172
+ /**
173
+ * Read compact summary from conversation store.
174
+ *
175
+ * @returns {string}
176
+ */
177
+ #getCompactSummary() {
178
+ if (!this.#conversationStore) return '';
179
+ return this.#conversationStore.readCompactSummary();
180
+ }
181
+
182
+ /**
183
+ * Persist user message and assistant response to conversation store.
184
+ *
185
+ * @param {string} userContent
186
+ * @param {string} assistantContent
187
+ * @param {string} mode
188
+ * @param {object[]} [toolCalls]
189
+ */
190
+ #persistMessages(userContent, assistantContent, mode, toolCalls) {
191
+ if (!this.#conversationStore) return;
192
+
193
+ // Persist user message
194
+ this.#conversationStore.append({
195
+ role: 'user',
196
+ content: userContent,
197
+ mode,
198
+ });
199
+
200
+ // Persist assistant message
201
+ const assistantMsg = {
202
+ role: 'assistant',
203
+ content: assistantContent,
204
+ mode,
205
+ model: this.#config.model,
206
+ };
207
+ if (toolCalls && toolCalls.length > 0) {
208
+ assistantMsg.toolCalls = toolCalls;
209
+ }
210
+ this.#conversationStore.append(assistantMsg);
211
+ }
212
+
213
+ /**
214
+ * Check and trigger consolidation if needed.
215
+ *
216
+ * @returns {Promise<{ archivedCount: number, extractedCount: number }|null>}
217
+ */
218
+ async #maybeConsolidate() {
219
+ if (!this.#conversationStore || !this.#memoryStore) return null;
220
+
221
+ const budget = this.#config.messageTokenBudget || 8192;
222
+ if (!shouldConsolidate(this.#conversationStore, budget)) return null;
223
+
224
+ try {
225
+ const result = await consolidate({
226
+ conversationStore: this.#conversationStore,
227
+ memoryStore: this.#memoryStore,
228
+ adapter: this.#adapter,
229
+ config: this.#config,
230
+ budget,
231
+ });
232
+ return { archivedCount: result.archivedCount, extractedCount: result.extractedEntries.length };
233
+ } catch {
234
+ // Consolidation failure is non-critical
235
+ return null;
236
+ }
237
+ }
238
+
110
239
  /**
111
240
  * Run a query — the main loop.
112
241
  *
@@ -126,7 +255,14 @@ export class Engine {
126
255
  return;
127
256
  }
128
257
 
129
- const systemPrompt = this.#buildSystemPrompt(mode);
258
+ // ─── Pre-query: Recall + Compact Summary ────────────────
259
+ const memory = await this.#recallMemory(prompt);
260
+ if (memory && memory.entries.length > 0) {
261
+ yield { type: 'recall', entryCount: memory.entries.length, cached: false };
262
+ }
263
+
264
+ const compactSummary = this.#getCompactSummary();
265
+ const systemPrompt = this.#buildSystemPrompt(mode, memory, compactSummary);
130
266
 
131
267
  // Build conversation: existing messages + new user message
132
268
  const conversationMessages = [
@@ -136,6 +272,9 @@ export class Engine {
136
272
 
137
273
  const toolDefs = this.#getToolDefs();
138
274
  let turnNumber = 0;
275
+ let continueTurns = 0; // auto-continue counter
276
+ let fullResponseText = '';
277
+ let currentModel = this.#config.model;
139
278
 
140
279
  while (true) {
141
280
  turnNumber++;
@@ -166,9 +305,8 @@ export class Engine {
166
305
 
167
306
  try {
168
307
  // Stream from adapter
169
- // Note: pass a snapshot of messages so later mutations don't affect the adapter
170
308
  for await (const event of this.#adapter.stream({
171
- model: this.#config.model,
309
+ model: currentModel,
172
310
  system: systemPrompt,
173
311
  messages: [...conversationMessages],
174
312
  tools: toolDefs.length > 0 ? toolDefs : undefined,
@@ -202,10 +340,9 @@ export class Engine {
202
340
  }
203
341
  }
204
342
  } catch (err) {
205
- // Adapter threw an exception (network, auth, etc.)
206
343
  const latencyMs = Date.now() - startTime;
207
344
  this.#trace.endTurn(turnId, {
208
- model: this.#config.model,
345
+ model: currentModel,
209
346
  inputTokens: totalUsage.inputTokens,
210
347
  outputTokens: totalUsage.outputTokens,
211
348
  stopReason: 'error',
@@ -213,6 +350,26 @@ export class Engine {
213
350
  responseText,
214
351
  });
215
352
 
353
+ // ─── LLMContextError → force compact → retry ──────
354
+ if (err instanceof LLMContextError && this.#conversationStore && this.#memoryStore) {
355
+ const consolidated = await this.#maybeConsolidate();
356
+ if (consolidated && consolidated.archivedCount > 0) {
357
+ yield { type: 'consolidate', archivedCount: consolidated.archivedCount, extractedCount: consolidated.extractedCount };
358
+ yield { type: 'turn_end', turnNumber, stopReason: 'context_overflow_retry' };
359
+ continue; // retry with fewer messages
360
+ }
361
+ }
362
+
363
+ // ─── Fallback model ──────────────────────────────
364
+ const fallbackModel = this.#config.fallbackModel;
365
+ if (fallbackModel && fallbackModel !== currentModel &&
366
+ (err.name === 'LLMRateLimitError' || err.name === 'LLMServerError')) {
367
+ yield { type: 'fallback', from: currentModel, to: fallbackModel, reason: err.message };
368
+ currentModel = fallbackModel;
369
+ yield { type: 'turn_end', turnNumber, stopReason: 'fallback_retry' };
370
+ continue; // retry with fallback model
371
+ }
372
+
216
373
  yield {
217
374
  type: 'error',
218
375
  error: err,
@@ -226,7 +383,7 @@ export class Engine {
226
383
 
227
384
  // Record turn in debug trace
228
385
  this.#trace.endTurn(turnId, {
229
- model: this.#config.model,
386
+ model: currentModel,
230
387
  inputTokens: totalUsage.inputTokens,
231
388
  outputTokens: totalUsage.outputTokens,
232
389
  stopReason,
@@ -244,10 +401,29 @@ export class Engine {
244
401
  }));
245
402
  }
246
403
  conversationMessages.push(assistantMsg);
404
+ fullResponseText += responseText;
405
+
406
+ // ─── Handle max_tokens → auto-continue ────────────
407
+ if (stopReason === 'max_tokens' && continueTurns < MAX_CONTINUE_TURNS) {
408
+ continueTurns++;
409
+ // Append a "Continue" user message
410
+ conversationMessages.push({ role: 'user', content: 'Continue' });
411
+ yield { type: 'turn_end', turnNumber, stopReason: 'max_tokens_continue' };
412
+ continue; // loop back to call adapter again
413
+ }
247
414
 
248
415
  // If no tool calls, we're done
249
416
  if (stopReason !== 'tool_use' || toolCalls.length === 0) {
250
417
  yield { type: 'turn_end', turnNumber, stopReason };
418
+
419
+ // ─── Post-query: Persist + Consolidate ────────────
420
+ this.#persistMessages(prompt, fullResponseText, mode, assistantMsg.toolCalls);
421
+
422
+ const consolidated = await this.#maybeConsolidate();
423
+ if (consolidated && consolidated.archivedCount > 0) {
424
+ yield { type: 'consolidate', archivedCount: consolidated.archivedCount, extractedCount: consolidated.extractedCount };
425
+ }
426
+
251
427
  break;
252
428
  }
253
429
 
@@ -316,4 +492,20 @@ export class Engine {
316
492
  get toolNames() {
317
493
  return Array.from(this.#tools.keys());
318
494
  }
495
+
496
+ /**
497
+ * Get the conversation store (for external access, e.g., CLI commands).
498
+ * @returns {import('./conversation/persist.js').ConversationStore|null}
499
+ */
500
+ get conversationStore() {
501
+ return this.#conversationStore;
502
+ }
503
+
504
+ /**
505
+ * Get the memory store (for external access, e.g., CLI commands).
506
+ * @returns {import('./memory/store.js').MemoryStore|null}
507
+ */
508
+ get memoryStore() {
509
+ return this.#memoryStore;
510
+ }
319
511
  }
package/unify/index.js CHANGED
@@ -19,3 +19,21 @@ export {
19
19
  export { MODEL_REGISTRY, resolveModel, listModels, isKnownModel } from './models.js';
20
20
  export { buildSystemPrompt, SUPPORTED_LANGUAGES } from './prompts.js';
21
21
  export { Engine } from './engine.js';
22
+ export { ConversationStore, parseMessage, estimateTokens } from './conversation/persist.js';
23
+ export { searchMessages } from './conversation/search.js';
24
+ export { MemoryStore, parseEntry, serializeEntry, MEMORY_KINDS } from './memory/store.js';
25
+ export { recall, extractKeywords, computeFingerprint, clearRecallCache } from './memory/recall.js';
26
+ export { extractMemories } from './memory/extract.js';
27
+ export { consolidate, shouldConsolidate } from './memory/consolidate.js';
28
+
29
+ // Phase 5: Advanced features
30
+ export { KINDS, KIND_PRIORITY, KIND_DESCRIPTIONS, IMPORTANCE_LEVELS, validateEntry, parseScopePath, getAncestorScopes, areScopesRelated } from './memory/types.js';
31
+ export { scanEntries, scoreEntry, findStaleEntries, findDuplicateGroups, summarizeScan } from './memory/scan.js';
32
+ export { dream, checkDreamGate, readDreamState, writeDreamState, incrementQueryCount } from './memory/dream.js';
33
+ export { buildOrientPrompt, buildGatherPrompt, buildMergePrompt, buildPrunePrompt, buildPromotePrompt } from './memory/dream-prompt.js';
34
+ export { runStopHooks } from './stop-hooks.js';
35
+ export { MCPManager, createMCPManager } from './mcp.js';
36
+ export { SkillManager, createSkillManager, parseSkill, serializeSkill } from './skills.js';
37
+ export { defineTool } from './tools/types.js';
38
+ export { ToolRegistry, createEmptyRegistry } from './tools/registry.js';
39
+