@yeaft/webchat-agent 0.1.849 → 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.849",
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",
@@ -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);