@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/package.json +1 -1
- package/unify/cli.js +214 -16
- package/unify/config.js +13 -0
- package/unify/conversation/persist.js +436 -0
- package/unify/conversation/search.js +65 -0
- package/unify/engine.js +210 -18
- package/unify/index.js +18 -0
- package/unify/mcp.js +433 -0
- package/unify/memory/consolidate.js +187 -0
- package/unify/memory/dream-prompt.js +272 -0
- package/unify/memory/dream.js +468 -0
- package/unify/memory/extract.js +97 -0
- package/unify/memory/recall.js +243 -0
- package/unify/memory/scan.js +273 -0
- package/unify/memory/store.js +507 -0
- package/unify/memory/types.js +139 -0
- package/unify/prompts.js +51 -3
- package/unify/skills.js +315 -0
- package/unify/stop-hooks.js +146 -0
- package/unify/tools/enter-worktree.js +97 -0
- package/unify/tools/exit-worktree.js +131 -0
- package/unify/tools/mcp-tools.js +133 -0
- package/unify/tools/registry.js +146 -0
- package/unify/tools/skill.js +107 -0
- package/unify/tools/types.js +71 -0
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.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
9
|
-
* 5. If
|
|
10
|
-
* 6. If
|
|
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_calls → execute tools → append results → goto 3
|
|
10
|
+
* 6. If end_turn → persist 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 {{
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
|