@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.848",
3
+ "version": "0.1.850",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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) opts.onProgress({ phase: 'apply', target: merged.target, status: 'done', batches: batchesUsed });
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';
@@ -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 = 30;
98
- export const DEFAULT_MIN_TOKEN_FLOOR = 12_000;
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.8;
101
- export const DEFAULT_HARD_TOKEN_CEILING = 200_000;
102
- export const DEFAULT_MIN_TURNS_FOR_COMPACT = 5;
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 = 2;
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
- // (1) Soft floor: never compact small conversations.
237
- // (2) Short-history guard: fewer than five turns should not compact unless
238
- // the estimated prompt is already at the context-pressure threshold.
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
- // (2) Trigger evaluation. Turn check is opt-in (Infinity by default).
253
- if (Number.isFinite(turnLimit) && turnCount > turnLimit) reason = 'turn_count';
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 safeTail = pairSanitize(tail);
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);