@yeaft/webchat-agent 0.1.848 → 0.1.850
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/dream-v2/apply.js +37 -1
- package/unify/dream-v2/session-wiring.js +29 -0
- package/unify/history-compact.js +66 -14
package/package.json
CHANGED
package/unify/dream-v2/apply.js
CHANGED
|
@@ -228,7 +228,23 @@ export async function applyMergedTarget(merged, opts) {
|
|
|
228
228
|
await writeMemory(scope, stamped, { root: opts.root });
|
|
229
229
|
await writeSummary(scope, summaryMd || '', { root: opts.root, language: opts.language });
|
|
230
230
|
|
|
231
|
-
if (opts.onProgress)
|
|
231
|
+
if (opts.onProgress) {
|
|
232
|
+
// feat-dream-debug-detail: surface a truncated copy of what was
|
|
233
|
+
// actually written so the debug panel can show "what segments were
|
|
234
|
+
// generated" instead of just "done". The full bytes are on disk
|
|
235
|
+
// anyway — this preview is for at-a-glance debugging.
|
|
236
|
+
opts.onProgress({
|
|
237
|
+
phase: 'apply',
|
|
238
|
+
target: merged.target,
|
|
239
|
+
status: 'done',
|
|
240
|
+
batches: batchesUsed,
|
|
241
|
+
kind: merged.kind,
|
|
242
|
+
memoryMdPreview: truncateForDebug(stamped),
|
|
243
|
+
summaryMdPreview: truncateForDebug(summaryMd || ''),
|
|
244
|
+
memoryMdLength: (stamped || '').length,
|
|
245
|
+
summaryMdLength: (summaryMd || '').length,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
232
248
|
return { target: merged.target, kind: merged.kind, batches: batchesUsed };
|
|
233
249
|
}
|
|
234
250
|
|
|
@@ -246,3 +262,23 @@ function scopeRelDir(scope) {
|
|
|
246
262
|
}
|
|
247
263
|
|
|
248
264
|
function oneLine(s) { return String(s || '').replace(/\s+/g, ' ').trim().slice(0, 200); }
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Per-field truncation cap for debug previews emitted on `apply/done`.
|
|
268
|
+
* Keep this small — the dream panel only needs a recognisable snippet.
|
|
269
|
+
* Total worst-case payload is `PREVIEW_MAX * 2 * targets_per_run` per
|
|
270
|
+
* dream pass; with N=50 targets that's ~200 KB. The full bytes are on
|
|
271
|
+
* disk under <root>/<scope>/{memory,summary}.md anyway — these previews
|
|
272
|
+
* are for at-a-glance debugging only.
|
|
273
|
+
*/
|
|
274
|
+
const PREVIEW_MAX = 2048;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Truncate a markdown blob for inclusion in a debug-panel cell. Adds a
|
|
278
|
+
* "…(+N chars)" marker so the user knows it was cut.
|
|
279
|
+
*/
|
|
280
|
+
function truncateForDebug(s, max = PREVIEW_MAX) {
|
|
281
|
+
const str = String(s || '');
|
|
282
|
+
if (str.length <= max) return str;
|
|
283
|
+
return str.slice(0, max) + `…(+${str.length - max} chars)`;
|
|
284
|
+
}
|
|
@@ -7,6 +7,35 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Memory v2 is the only path. The legacy `config.memoryV2` opt-out flag was
|
|
9
9
|
* retired (task-710) — the wiring is unconditional.
|
|
10
|
+
*
|
|
11
|
+
* ─────────────────────────────────────────────────────────────────────
|
|
12
|
+
* Wire contract — DreamEvent (consumed by web/components/UnifyDebugPanel.js)
|
|
13
|
+
* ─────────────────────────────────────────────────────────────────────
|
|
14
|
+
* The events persisted to trace_events with these event_type values are
|
|
15
|
+
* load-bearing for the debug panel. Renaming a field on this side
|
|
16
|
+
* silently degrades that UI to a generic JSON dump.
|
|
17
|
+
*
|
|
18
|
+
* dream_turn_open: { type:'turn_open', turnId, userPrompt, vpId, groupId, at }
|
|
19
|
+
* dream_loop: { type:'loop', turnId, loopNumber, pass, model,
|
|
20
|
+
* systemPrompt: string,
|
|
21
|
+
* messages: [{ role:'user', content:string }],
|
|
22
|
+
* response: string,
|
|
23
|
+
* toolCalls: [], usage: { inputTokens, outputTokens, totalTokens },
|
|
24
|
+
* latencyMs, ttfbMs, stopReason, rawRequest, rawResponse }
|
|
25
|
+
* dream_turn_close: { type:'turn_close', turnId, totalMs, totalTokens, loopCount,
|
|
26
|
+
* metrics: { llmCallCount, inputTokens, outputTokens,
|
|
27
|
+
* totalTokens, durationMs, passBreakdown:{[pass]:{
|
|
28
|
+
* llmCallCount, inputTokens, outputTokens,
|
|
29
|
+
* totalTokens, durationMs }} } }
|
|
30
|
+
* dream_run: { type:'dream_run', turnId, phase:'result',
|
|
31
|
+
* status:'done'|'error', metrics, resultSummary:{ groups,
|
|
32
|
+
* targets, error, skipped, skippedReason } }
|
|
33
|
+
* dream_progress: runner-emitted phase events (`start`/`load-diff`/`triage`/
|
|
34
|
+
* `merge`/`apply`/`done`). The `apply/done` variant carries
|
|
35
|
+
* `kind, memoryMdPreview, summaryMdPreview, memoryMdLength,
|
|
36
|
+
* summaryMdLength` (see apply.js).
|
|
37
|
+
*
|
|
38
|
+
* `groupId` may be inherited via `stampDreamScope()` when a scope is active.
|
|
10
39
|
*/
|
|
11
40
|
|
|
12
41
|
import { join } from 'path';
|
package/unify/history-compact.js
CHANGED
|
@@ -94,13 +94,14 @@ export const countTurns = countTurnsImpl;
|
|
|
94
94
|
* Token thresholds are derived from `maxContextTokens` at evaluation
|
|
95
95
|
* time so the policy auto-adjusts to the user's configured context.
|
|
96
96
|
*/
|
|
97
|
-
export const DEFAULT_TURN_LIMIT =
|
|
98
|
-
export const DEFAULT_MIN_TOKEN_FLOOR =
|
|
97
|
+
export const DEFAULT_TURN_LIMIT = Infinity;
|
|
98
|
+
export const DEFAULT_MIN_TOKEN_FLOOR = 0;
|
|
99
99
|
export const DEFAULT_MAX_CONTEXT_TOKENS = 200_000;
|
|
100
|
-
export const DEFAULT_TOKEN_FRACTION = 0.
|
|
101
|
-
export const DEFAULT_HARD_TOKEN_CEILING =
|
|
102
|
-
export const DEFAULT_MIN_TURNS_FOR_COMPACT =
|
|
100
|
+
export const DEFAULT_TOKEN_FRACTION = 0.5;
|
|
101
|
+
export const DEFAULT_HARD_TOKEN_CEILING = Infinity;
|
|
102
|
+
export const DEFAULT_MIN_TURNS_FOR_COMPACT = 0;
|
|
103
103
|
export const DEFAULT_KEEP_TOOL_TURNS = 3;
|
|
104
|
+
export const DEFAULT_TOOL_CALL_COMPACT_THRESHOLD = 30;
|
|
104
105
|
/**
|
|
105
106
|
* Effective default token trigger when no `maxContextTokens` is provided:
|
|
106
107
|
* min(80% of 200K, 200K) = 160K. Preserved as `DEFAULT_TOKEN_LIMIT` for
|
|
@@ -116,7 +117,7 @@ export const DEFAULT_TOKEN_LIMIT = Math.min(
|
|
|
116
117
|
* replaces everything before this window. 2 keeps "what we were just
|
|
117
118
|
* talking about" lossless.
|
|
118
119
|
*/
|
|
119
|
-
export const DEFAULT_KEEP_RECENT_TURNS =
|
|
120
|
+
export const DEFAULT_KEEP_RECENT_TURNS = 3;
|
|
120
121
|
|
|
121
122
|
/**
|
|
122
123
|
* Default cap on the number of turns kept in the per-call snapshot fed
|
|
@@ -233,9 +234,10 @@ export function shouldCompactHistory(messages, opts = {}) {
|
|
|
233
234
|
const tokenCount = estimateMessagesTokens(messages);
|
|
234
235
|
|
|
235
236
|
let reason = null;
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
237
|
+
// Product rule: async group compact is allowed only when the current
|
|
238
|
+
// conversation exceeds the model context window threshold. Turn count is
|
|
239
|
+
// preserved as an explicit test/future-config override, but defaults to
|
|
240
|
+
// Infinity so it cannot compact a small context by itself.
|
|
239
241
|
if (tokenCount < minTokenFloor || (turnCount < minTurnsForCompact && tokenCount < tokenLimit)) {
|
|
240
242
|
return {
|
|
241
243
|
trigger: false,
|
|
@@ -249,10 +251,8 @@ export function shouldCompactHistory(messages, opts = {}) {
|
|
|
249
251
|
hardTokenCeiling,
|
|
250
252
|
};
|
|
251
253
|
}
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
else if (tokenCount > hardTokenCeiling) reason = 'token_ceiling';
|
|
255
|
-
else if (tokenCount >= tokenLimit) reason = 'token_threshold';
|
|
254
|
+
if (tokenCount > hardTokenCeiling) reason = 'token_ceiling';
|
|
255
|
+
else if (tokenCount > tokenLimit) reason = 'token_threshold';
|
|
256
256
|
|
|
257
257
|
return {
|
|
258
258
|
trigger: reason !== null,
|
|
@@ -273,6 +273,27 @@ function hasContentAfterToolStrip(content) {
|
|
|
273
273
|
return content != null;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function countToolCallsInContent(content) {
|
|
277
|
+
if (!Array.isArray(content)) return 0;
|
|
278
|
+
let n = 0;
|
|
279
|
+
for (const part of content) {
|
|
280
|
+
if (!part || typeof part !== 'object') continue;
|
|
281
|
+
if (part.type === 'tool_use' || part.type === 'function_call') n++;
|
|
282
|
+
}
|
|
283
|
+
return n;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function countToolCallsInMessages(messages) {
|
|
287
|
+
if (!Array.isArray(messages)) return 0;
|
|
288
|
+
let n = 0;
|
|
289
|
+
for (const m of messages) {
|
|
290
|
+
if (!m || typeof m !== 'object') continue;
|
|
291
|
+
if (Array.isArray(m.toolCalls)) n += m.toolCalls.length;
|
|
292
|
+
n += countToolCallsInContent(m.content);
|
|
293
|
+
}
|
|
294
|
+
return n;
|
|
295
|
+
}
|
|
296
|
+
|
|
276
297
|
function stripToolContentParts(content) {
|
|
277
298
|
if (!Array.isArray(content)) return content;
|
|
278
299
|
return content.filter(part => {
|
|
@@ -326,6 +347,31 @@ export function stripToolNoiseFromOlderTurns(messages, opts = {}) {
|
|
|
326
347
|
return [...cleanedOlder, ...recent.map(m => ({ ...m }))];
|
|
327
348
|
}
|
|
328
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Apply the async compact retained-tail tool policy. Small retained tails keep
|
|
352
|
+
* every tool pair intact. Once the retained tail exceeds the threshold, keep
|
|
353
|
+
* full tool history only for the latest turn and strip tool noise from the
|
|
354
|
+
* earlier retained turns while preserving their normal text.
|
|
355
|
+
*
|
|
356
|
+
* @param {Array<object>} tail
|
|
357
|
+
* @param {{ keepToolTurns?: number, toolCallCompactThreshold?: number }} [opts]
|
|
358
|
+
* @returns {Array<object>}
|
|
359
|
+
*/
|
|
360
|
+
export function compactRetainedTailToolCalls(tail, opts = {}) {
|
|
361
|
+
if (!Array.isArray(tail) || tail.length === 0) return [];
|
|
362
|
+
|
|
363
|
+
const threshold = Number.isFinite(opts.toolCallCompactThreshold) && opts.toolCallCompactThreshold >= 0
|
|
364
|
+
? opts.toolCallCompactThreshold
|
|
365
|
+
: DEFAULT_TOOL_CALL_COMPACT_THRESHOLD;
|
|
366
|
+
const toolCallCount = countToolCallsInMessages(tail);
|
|
367
|
+
if (toolCallCount <= threshold) return tail.map(m => ({ ...m }));
|
|
368
|
+
|
|
369
|
+
const keepToolTurns = Number.isFinite(opts.keepToolTurns) && opts.keepToolTurns >= 0
|
|
370
|
+
? opts.keepToolTurns
|
|
371
|
+
: 1;
|
|
372
|
+
return stripToolNoiseFromOlderTurns(tail, { keepToolTurns });
|
|
373
|
+
}
|
|
374
|
+
|
|
329
375
|
/**
|
|
330
376
|
* Strip noise from a message list before sending it to the summarizer:
|
|
331
377
|
* - drop `role: 'tool'` (raw tool results — too verbose, mostly redundant)
|
|
@@ -499,6 +545,8 @@ export async function compactHistory(messages, options) {
|
|
|
499
545
|
tokenFraction,
|
|
500
546
|
hardTokenCeiling,
|
|
501
547
|
language,
|
|
548
|
+
keepToolTurns,
|
|
549
|
+
toolCallCompactThreshold,
|
|
502
550
|
} = options || {};
|
|
503
551
|
|
|
504
552
|
if (typeof summarize !== 'function') {
|
|
@@ -600,7 +648,11 @@ export async function compactHistory(messages, options) {
|
|
|
600
648
|
// whose tool_use IDs aren't fully matched in the tail. This is what
|
|
601
649
|
// keeps the next adapter call from 400-ing on tool_use/tool_result
|
|
602
650
|
// mismatch when the storage / fan-out layer reorders messages.
|
|
603
|
-
const
|
|
651
|
+
const compactedTail = compactRetainedTailToolCalls(tail, {
|
|
652
|
+
keepToolTurns,
|
|
653
|
+
toolCallCompactThreshold,
|
|
654
|
+
});
|
|
655
|
+
const safeTail = pairSanitize(compactedTail);
|
|
604
656
|
|
|
605
657
|
const newMessages = [summaryMsg, ...safeTail];
|
|
606
658
|
const after = shouldCompactHistory(newMessages, triggerOpts);
|