@yeaft/webchat-agent 0.1.408 → 0.1.409
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 +6 -0
- package/unify/memory/consolidate.js +187 -0
- package/unify/memory/extract.js +97 -0
- package/unify/memory/recall.js +243 -0
- package/unify/memory/store.js +507 -0
- package/unify/prompts.js +51 -3
package/package.json
CHANGED
package/unify/cli.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* --trace stats|recent|search <keyword> — Query debug.db
|
|
8
8
|
* -i / --interactive — REPL mode with / commands
|
|
9
9
|
* <prompt> — One-shot query (Phase 1: engine.query)
|
|
10
|
+
*
|
|
11
|
+
* Phase 2 additions:
|
|
12
|
+
* /memory — Show memory status or manage entries
|
|
13
|
+
* /history [n] — Show last N messages from conversation
|
|
14
|
+
* /search <keyword> — Search conversation history
|
|
15
|
+
* /compact — Trigger manual consolidation
|
|
16
|
+
* Conversation persistence across REPL sessions
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
import { createInterface } from 'readline';
|
|
@@ -18,6 +25,10 @@ import { createLLMAdapter } from './llm/adapter.js';
|
|
|
18
25
|
import { Engine } from './engine.js';
|
|
19
26
|
import { listModels, resolveModel } from './models.js';
|
|
20
27
|
import { buildSystemPrompt } from './prompts.js';
|
|
28
|
+
import { ConversationStore } from './conversation/persist.js';
|
|
29
|
+
import { searchMessages } from './conversation/search.js';
|
|
30
|
+
import { MemoryStore } from './memory/store.js';
|
|
31
|
+
import { shouldConsolidate, consolidate } from './memory/consolidate.js';
|
|
21
32
|
|
|
22
33
|
// ─── Argument parsing ──────────────────────────────────────────
|
|
23
34
|
|
|
@@ -191,21 +202,37 @@ async function runREPL(config, args) {
|
|
|
191
202
|
dbPath: join(config.dir, 'debug.db'),
|
|
192
203
|
});
|
|
193
204
|
|
|
205
|
+
// Initialize conversation and memory stores
|
|
206
|
+
const conversationStore = new ConversationStore(config.dir);
|
|
207
|
+
const memoryStore = new MemoryStore(config.dir);
|
|
208
|
+
|
|
194
209
|
let adapter;
|
|
195
210
|
let engine;
|
|
196
211
|
let currentMode = args.mode;
|
|
197
|
-
|
|
212
|
+
|
|
213
|
+
// Load persisted conversation as initial messages
|
|
214
|
+
let conversationMessages = conversationStore.loadRecent(50).map(m => ({
|
|
215
|
+
role: m.role,
|
|
216
|
+
content: m.content,
|
|
217
|
+
...(m.toolCallId && { toolCallId: m.toolCallId }),
|
|
218
|
+
...(m.toolCalls && { toolCalls: m.toolCalls }),
|
|
219
|
+
}));
|
|
198
220
|
|
|
199
221
|
// Lazy adapter creation (don't fail on start if no API key for --trace-only usage)
|
|
200
222
|
async function ensureEngine() {
|
|
201
223
|
if (!engine) {
|
|
202
224
|
adapter = await createLLMAdapter(config);
|
|
203
|
-
engine = new Engine({ adapter, trace, config });
|
|
225
|
+
engine = new Engine({ adapter, trace, config, conversationStore, memoryStore });
|
|
204
226
|
}
|
|
205
227
|
return engine;
|
|
206
228
|
}
|
|
207
229
|
|
|
230
|
+
const hotCount = conversationStore.countHot();
|
|
231
|
+
const coldCount = conversationStore.countCold();
|
|
232
|
+
const memStats = memoryStore.stats();
|
|
233
|
+
|
|
208
234
|
console.log(`Yeaft Unify REPL (model: ${config.model}, mode: ${currentMode})`);
|
|
235
|
+
console.log(`Conversation: ${hotCount} hot, ${coldCount} cold | Memory: ${memStats.entryCount} entries`);
|
|
209
236
|
console.log('Type /help for commands, /quit to exit.');
|
|
210
237
|
console.log();
|
|
211
238
|
|
|
@@ -233,7 +260,10 @@ async function runREPL(config, args) {
|
|
|
233
260
|
console.log(' /mode <chat|work|dream> — Switch mode');
|
|
234
261
|
console.log(' /debug — Toggle debug mode');
|
|
235
262
|
console.log(' /trace <stats|recent> — Query debug trace');
|
|
236
|
-
console.log(' /memory
|
|
263
|
+
console.log(' /memory [add|clear|stats] — Memory management');
|
|
264
|
+
console.log(' /history [n] — Show last N messages');
|
|
265
|
+
console.log(' /search <keyword> — Search conversation history');
|
|
266
|
+
console.log(' /compact — Trigger consolidation');
|
|
237
267
|
console.log(' /context — Show context info');
|
|
238
268
|
console.log(' /dry-run — Toggle dry-run mode');
|
|
239
269
|
console.log(' /stats — Show session stats');
|
|
@@ -269,9 +299,126 @@ async function runREPL(config, args) {
|
|
|
269
299
|
break;
|
|
270
300
|
}
|
|
271
301
|
|
|
272
|
-
case 'memory':
|
|
273
|
-
|
|
302
|
+
case 'memory': {
|
|
303
|
+
const subcmd = cmdArgs[0];
|
|
304
|
+
if (subcmd === 'add') {
|
|
305
|
+
// /memory add <section> <entry text>
|
|
306
|
+
const section = cmdArgs[1];
|
|
307
|
+
const entryText = cmdArgs.slice(2).join(' ');
|
|
308
|
+
if (!section || !entryText) {
|
|
309
|
+
console.log('Usage: /memory add <section> <entry text>');
|
|
310
|
+
} else {
|
|
311
|
+
memoryStore.addToSection(section, `- ${entryText}`);
|
|
312
|
+
console.log(`Added to MEMORY.md [${section}]: ${entryText}`);
|
|
313
|
+
}
|
|
314
|
+
} else if (subcmd === 'clear') {
|
|
315
|
+
memoryStore.clear();
|
|
316
|
+
console.log('All memory cleared.');
|
|
317
|
+
} else if (subcmd === 'stats') {
|
|
318
|
+
const s = memoryStore.stats();
|
|
319
|
+
console.log('Memory stats:');
|
|
320
|
+
console.log(` Entries: ${s.entryCount}`);
|
|
321
|
+
console.log(` Scopes: ${s.scopes.join(', ') || '(none)'}`);
|
|
322
|
+
console.log(` Kinds: ${Object.entries(s.kinds).map(([k, v]) => `${k}=${v}`).join(', ') || '(none)'}`);
|
|
323
|
+
} else if (subcmd === 'search') {
|
|
324
|
+
const keyword = cmdArgs.slice(1).join(' ');
|
|
325
|
+
if (!keyword) {
|
|
326
|
+
console.log('Usage: /memory search <keyword>');
|
|
327
|
+
} else {
|
|
328
|
+
const results = memoryStore.search(keyword);
|
|
329
|
+
if (results.length === 0) {
|
|
330
|
+
console.log(`No memory entries matching "${keyword}".`);
|
|
331
|
+
} else {
|
|
332
|
+
console.log(`Found ${results.length} entries:`);
|
|
333
|
+
for (const e of results) {
|
|
334
|
+
console.log(` [${e.kind}] ${e.name} (scope: ${e.scope})`);
|
|
335
|
+
console.log(` ${(e.content || '').slice(0, 100)}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
// Default: show MEMORY.md content
|
|
341
|
+
const profile = memoryStore.readProfile();
|
|
342
|
+
const s = memoryStore.stats();
|
|
343
|
+
if (profile) {
|
|
344
|
+
console.log('--- MEMORY.md ---');
|
|
345
|
+
console.log(profile.slice(0, 1000));
|
|
346
|
+
if (profile.length > 1000) console.log('... (truncated)');
|
|
347
|
+
} else {
|
|
348
|
+
console.log('MEMORY.md is empty.');
|
|
349
|
+
}
|
|
350
|
+
console.log(`\nEntries: ${s.entryCount} | Scopes: ${s.scopes.length}`);
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
case 'history': {
|
|
356
|
+
const limit = parseInt(cmdArgs[0], 10) || 10;
|
|
357
|
+
const messages = conversationStore.loadRecent(limit);
|
|
358
|
+
if (messages.length === 0) {
|
|
359
|
+
console.log('No messages in history.');
|
|
360
|
+
} else {
|
|
361
|
+
console.log(`Last ${messages.length} messages:`);
|
|
362
|
+
for (const m of messages) {
|
|
363
|
+
const time = m.time ? new Date(m.time).toLocaleString() : '?';
|
|
364
|
+
const preview = (m.content || '').slice(0, 100).replace(/\n/g, ' ');
|
|
365
|
+
console.log(` [${time}] ${m.role}: ${preview}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case 'search': {
|
|
372
|
+
const keyword = cmdArgs.join(' ');
|
|
373
|
+
if (!keyword) {
|
|
374
|
+
console.log('Usage: /search <keyword>');
|
|
375
|
+
} else {
|
|
376
|
+
const results = searchMessages(config.dir, keyword, 20);
|
|
377
|
+
if (results.length === 0) {
|
|
378
|
+
console.log(`No messages matching "${keyword}".`);
|
|
379
|
+
} else {
|
|
380
|
+
console.log(`Found ${results.length} messages:`);
|
|
381
|
+
for (const m of results) {
|
|
382
|
+
const time = m.time ? new Date(m.time).toLocaleString() : '?';
|
|
383
|
+
const preview = (m.content || '').slice(0, 100).replace(/\n/g, ' ');
|
|
384
|
+
console.log(` [${time}] ${m.role}: ${preview}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
274
388
|
break;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
case 'compact': {
|
|
392
|
+
try {
|
|
393
|
+
const eng = await ensureEngine();
|
|
394
|
+
const budget = config.messageTokenBudget || 8192;
|
|
395
|
+
const hotTokens = conversationStore.hotTokens();
|
|
396
|
+
console.log(`Hot tokens: ${hotTokens} / budget: ${budget}`);
|
|
397
|
+
|
|
398
|
+
if (hotTokens <= 0) {
|
|
399
|
+
console.log('No messages to compact.');
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
console.log('Running consolidation...');
|
|
404
|
+
const result = await consolidate({
|
|
405
|
+
conversationStore,
|
|
406
|
+
memoryStore,
|
|
407
|
+
adapter: adapter || await createLLMAdapter(config),
|
|
408
|
+
config,
|
|
409
|
+
budget: Math.min(budget, hotTokens), // force trigger
|
|
410
|
+
});
|
|
411
|
+
console.log(`Consolidation complete:`);
|
|
412
|
+
console.log(` Archived: ${result.archivedCount} messages`);
|
|
413
|
+
console.log(` Extracted: ${result.extractedEntries.length} memory entries`);
|
|
414
|
+
if (result.compactSummary) {
|
|
415
|
+
console.log(` Summary: ${result.compactSummary.slice(0, 200)}...`);
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
console.error(`Consolidation error: ${e.message}`);
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
275
422
|
|
|
276
423
|
case 'context':
|
|
277
424
|
console.log(`Context info:`);
|
|
@@ -280,6 +427,10 @@ async function runREPL(config, args) {
|
|
|
280
427
|
console.log(` Language: ${config.language}`);
|
|
281
428
|
console.log(` Max context: ${config.maxContextTokens} tokens`);
|
|
282
429
|
console.log(` System prompt: ${buildSystemPrompt({ language: config.language, mode: currentMode }).length} chars`);
|
|
430
|
+
console.log(` Hot messages: ${conversationStore.countHot()}`);
|
|
431
|
+
console.log(` Hot tokens: ${conversationStore.hotTokens()}`);
|
|
432
|
+
console.log(` Cold messages: ${conversationStore.countCold()}`);
|
|
433
|
+
console.log(` Memory entries: ${memoryStore.stats().entryCount}`);
|
|
283
434
|
break;
|
|
284
435
|
|
|
285
436
|
case 'dry-run':
|
|
@@ -293,6 +444,9 @@ async function runREPL(config, args) {
|
|
|
293
444
|
console.log(` Debug: ${config.debug}`);
|
|
294
445
|
console.log(` Turns: ${s.turnCount}`);
|
|
295
446
|
console.log(` Tools: ${s.toolCount}`);
|
|
447
|
+
console.log(` Hot messages: ${conversationStore.countHot()}`);
|
|
448
|
+
console.log(` Cold messages: ${conversationStore.countCold()}`);
|
|
449
|
+
console.log(` Memory entries: ${memoryStore.stats().entryCount}`);
|
|
296
450
|
break;
|
|
297
451
|
}
|
|
298
452
|
|
|
@@ -340,8 +494,9 @@ async function runREPL(config, args) {
|
|
|
340
494
|
break;
|
|
341
495
|
|
|
342
496
|
case 'clear':
|
|
497
|
+
conversationStore.clear();
|
|
343
498
|
conversationMessages = [];
|
|
344
|
-
console.log('Conversation history cleared.');
|
|
499
|
+
console.log('Conversation history cleared (including persisted messages).');
|
|
345
500
|
break;
|
|
346
501
|
|
|
347
502
|
case 'quit':
|
|
@@ -383,6 +538,19 @@ async function runREPL(config, args) {
|
|
|
383
538
|
process.stderr.write(`[tool] ${event.name} → ${status}\n`);
|
|
384
539
|
}
|
|
385
540
|
break;
|
|
541
|
+
case 'recall':
|
|
542
|
+
if (config.debug) {
|
|
543
|
+
process.stderr.write(`[recall] ${event.entryCount} entries${event.cached ? ' (cached)' : ''}\n`);
|
|
544
|
+
}
|
|
545
|
+
break;
|
|
546
|
+
case 'consolidate':
|
|
547
|
+
if (config.debug) {
|
|
548
|
+
process.stderr.write(`[consolidate] archived=${event.archivedCount}, extracted=${event.extractedCount}\n`);
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
case 'fallback':
|
|
552
|
+
process.stderr.write(`\n[fallback] ${event.from} → ${event.to}: ${event.reason}\n`);
|
|
553
|
+
break;
|
|
386
554
|
case 'error':
|
|
387
555
|
process.stderr.write(`\nError: ${event.error.message}\n`);
|
|
388
556
|
break;
|
|
@@ -395,7 +563,8 @@ async function runREPL(config, args) {
|
|
|
395
563
|
}
|
|
396
564
|
console.log(); // newline after response
|
|
397
565
|
|
|
398
|
-
//
|
|
566
|
+
// Update in-memory conversation for multi-turn context
|
|
567
|
+
// (engine.js already persists to disk via conversationStore)
|
|
399
568
|
conversationMessages.push({ role: 'user', content: input });
|
|
400
569
|
if (responseText) {
|
|
401
570
|
conversationMessages.push({ role: 'assistant', content: responseText });
|
|
@@ -421,6 +590,10 @@ async function runOnce(config, args) {
|
|
|
421
590
|
dbPath: join(config.dir, 'debug.db'),
|
|
422
591
|
});
|
|
423
592
|
|
|
593
|
+
// Initialize stores for persistence
|
|
594
|
+
const conversationStore = new ConversationStore(config.dir);
|
|
595
|
+
const memoryStore = new MemoryStore(config.dir);
|
|
596
|
+
|
|
424
597
|
try {
|
|
425
598
|
if (args.dryRun) {
|
|
426
599
|
handleDryRun(args, config);
|
|
@@ -428,9 +601,21 @@ async function runOnce(config, args) {
|
|
|
428
601
|
}
|
|
429
602
|
|
|
430
603
|
const adapter = await createLLMAdapter(config);
|
|
431
|
-
const engine = new Engine({ adapter, trace, config });
|
|
432
|
-
|
|
433
|
-
|
|
604
|
+
const engine = new Engine({ adapter, trace, config, conversationStore, memoryStore });
|
|
605
|
+
|
|
606
|
+
// Load recent conversation as context
|
|
607
|
+
const priorMessages = conversationStore.loadRecent(20).map(m => ({
|
|
608
|
+
role: m.role,
|
|
609
|
+
content: m.content,
|
|
610
|
+
...(m.toolCallId && { toolCallId: m.toolCallId }),
|
|
611
|
+
...(m.toolCalls && { toolCalls: m.toolCalls }),
|
|
612
|
+
}));
|
|
613
|
+
|
|
614
|
+
for await (const event of engine.query({
|
|
615
|
+
prompt: args.prompt,
|
|
616
|
+
mode: args.mode,
|
|
617
|
+
messages: priorMessages,
|
|
618
|
+
})) {
|
|
434
619
|
switch (event.type) {
|
|
435
620
|
case 'text_delta':
|
|
436
621
|
process.stdout.write(event.text);
|
|
@@ -446,6 +631,19 @@ async function runOnce(config, args) {
|
|
|
446
631
|
process.stderr.write(`[tool] ${event.name} → ${status}\n`);
|
|
447
632
|
}
|
|
448
633
|
break;
|
|
634
|
+
case 'recall':
|
|
635
|
+
if (args.verbose || args.debug) {
|
|
636
|
+
process.stderr.write(`[recall] ${event.entryCount} entries${event.cached ? ' (cached)' : ''}\n`);
|
|
637
|
+
}
|
|
638
|
+
break;
|
|
639
|
+
case 'consolidate':
|
|
640
|
+
if (args.verbose || args.debug) {
|
|
641
|
+
process.stderr.write(`[consolidate] archived=${event.archivedCount}, extracted=${event.extractedCount}\n`);
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
case 'fallback':
|
|
645
|
+
process.stderr.write(`\n[fallback] ${event.from} → ${event.to}: ${event.reason}\n`);
|
|
646
|
+
break;
|
|
449
647
|
case 'error':
|
|
450
648
|
process.stderr.write(`\nError: ${event.error.message}\n`);
|
|
451
649
|
break;
|
|
@@ -513,12 +711,12 @@ async function main() {
|
|
|
513
711
|
console.log('Yeaft Unify CLI');
|
|
514
712
|
console.log();
|
|
515
713
|
console.log('Usage:');
|
|
516
|
-
console.log(' node cli.js "your prompt"
|
|
517
|
-
console.log(' node cli.js -i
|
|
518
|
-
console.log(' node cli.js --dry-run "prompt"
|
|
519
|
-
console.log(' node cli.js --trace stats
|
|
520
|
-
console.log(' node cli.js --trace recent
|
|
521
|
-
console.log(' node cli.js --trace search "keyword"
|
|
714
|
+
console.log(' node cli.js "your prompt" — One-shot query');
|
|
715
|
+
console.log(' node cli.js -i — Interactive REPL');
|
|
716
|
+
console.log(' node cli.js --dry-run "prompt" — Show what would be sent');
|
|
717
|
+
console.log(' node cli.js --trace stats — Debug trace statistics');
|
|
718
|
+
console.log(' node cli.js --trace recent — Recent turns');
|
|
719
|
+
console.log(' node cli.js --trace search "keyword" — Search traces');
|
|
522
720
|
console.log();
|
|
523
721
|
console.log('Options:');
|
|
524
722
|
console.log(' -m, --mode <mode> Mode: chat, work, dream (default: chat)');
|
package/unify/config.js
CHANGED
|
@@ -26,6 +26,8 @@ const DEFAULTS = {
|
|
|
26
26
|
dir: DEFAULT_YEAFT_DIR,
|
|
27
27
|
maxContextTokens: 200000,
|
|
28
28
|
maxOutputTokens: 16384,
|
|
29
|
+
messageTokenBudget: 8192, // Phase 2: context * 4%, triggers consolidation
|
|
30
|
+
maxContinueTurns: 3, // Phase 2: auto-continue on max_tokens
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -217,6 +219,17 @@ export function loadConfig(overrides = {}) {
|
|
|
217
219
|
overrides.maxOutputTokens ??
|
|
218
220
|
fileConfig.maxOutputTokens ??
|
|
219
221
|
DEFAULTS.maxOutputTokens,
|
|
222
|
+
|
|
223
|
+
messageTokenBudget:
|
|
224
|
+
overrides.messageTokenBudget ??
|
|
225
|
+
(env.YEAFT_MESSAGE_TOKEN_BUDGET ? parseInt(env.YEAFT_MESSAGE_TOKEN_BUDGET, 10) : null) ??
|
|
226
|
+
fileConfig.messageTokenBudget ??
|
|
227
|
+
DEFAULTS.messageTokenBudget,
|
|
228
|
+
|
|
229
|
+
maxContinueTurns:
|
|
230
|
+
overrides.maxContinueTurns ??
|
|
231
|
+
fileConfig.maxContinueTurns ??
|
|
232
|
+
DEFAULTS.maxContinueTurns,
|
|
220
233
|
};
|
|
221
234
|
|
|
222
235
|
// Auto-detect adapter using model registry + credential fallback
|