@yeaft/webchat-agent 1.0.463 → 1.0.465

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": "1.0.463",
3
+ "version": "1.0.465",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -195,14 +195,21 @@ function hasContentAfterToolStrip(content) {
195
195
  return content != null;
196
196
  }
197
197
 
198
- function stripToolContentParts(content) {
198
+ function toolContentPartCallId(part) {
199
+ if (!part || typeof part !== 'object') return null;
200
+ return part.id || part.tool_use_id || part.call_id || null;
201
+ }
202
+
203
+ function stripToolContentParts(content, selectedCallIds = null) {
199
204
  if (!Array.isArray(content)) return content;
200
205
  return content.filter(part => {
201
206
  if (!part || typeof part !== 'object') return true;
202
- return part.type !== 'tool_use'
203
- && part.type !== 'tool_result'
204
- && part.type !== 'function_call'
205
- && part.type !== 'function_call_output';
207
+ const isToolPart = part.type === 'tool_use'
208
+ || part.type === 'tool_result'
209
+ || part.type === 'function_call'
210
+ || part.type === 'function_call_output';
211
+ if (!isToolPart) return true;
212
+ return selectedCallIds instanceof Set && selectedCallIds.has(toolContentPartCallId(part));
206
213
  });
207
214
  }
208
215
 
@@ -232,6 +239,7 @@ export function stripToolNoiseFromOlderTurns(messages, options = {}) {
232
239
 
233
240
  const next = { ...message };
234
241
  if (Array.isArray(next.toolCalls)) delete next.toolCalls;
242
+ if (Array.isArray(next.thinkingBlocks)) delete next.thinkingBlocks;
235
243
  if (Array.isArray(next.content)) next.content = stripToolContentParts(next.content);
236
244
  if (next.role === 'assistant' && !hasContentAfterToolStrip(next.content)) continue;
237
245
  if (next.role === 'user' && Array.isArray(next.content) && next.content.length === 0) continue;
@@ -240,6 +248,21 @@ export function stripToolNoiseFromOlderTurns(messages, options = {}) {
240
248
  return [...cleanedOlder, ...recent.map(message => ({ ...message }))];
241
249
  }
242
250
 
251
+ function stripAllToolNoise(messages) {
252
+ const cleaned = [];
253
+ for (const message of messages) {
254
+ if (!message || typeof message !== 'object' || message.role === 'tool') continue;
255
+ const next = { ...message };
256
+ if (Array.isArray(next.toolCalls)) delete next.toolCalls;
257
+ if (Array.isArray(next.thinkingBlocks)) delete next.thinkingBlocks;
258
+ if (Array.isArray(next.content)) next.content = stripToolContentParts(next.content);
259
+ if (next.role === 'assistant' && !hasContentAfterToolStrip(next.content)) continue;
260
+ if (next.role === 'user' && Array.isArray(next.content) && next.content.length === 0) continue;
261
+ cleaned.push(next);
262
+ }
263
+ return cleaned;
264
+ }
265
+
243
266
  function truncateTextToTokens(text, tokenBudget) {
244
267
  if (typeof text !== 'string' || tokenBudget <= 0) return '';
245
268
  if (estimateTokens(text) <= tokenBudget) return text;
@@ -411,6 +434,21 @@ function dropOldestHistoryUntilBudget(messages, tokenBudget) {
411
434
  return out;
412
435
  }
413
436
 
437
+ function dropOldestHistoryUntilMessageCap(messages, maxMessageCount) {
438
+ let out = pairSanitize(messages);
439
+ let turns = countTurns(out);
440
+ while (out.length > maxMessageCount && turns > 1) {
441
+ const next = pairSanitize(sliceLastNTurns(out, turns - 1));
442
+ if (next.length === out.length) break;
443
+ out = next;
444
+ turns = countTurns(out);
445
+ }
446
+ if (out.length > maxMessageCount) {
447
+ out = pairSanitize(out.slice(-maxMessageCount));
448
+ }
449
+ return out;
450
+ }
451
+
414
452
  function providerUnits(messages) {
415
453
  const units = [];
416
454
  for (let index = 0; index < messages.length;) {
@@ -489,9 +527,7 @@ function fitMessagesToBudget(messages, tokenBudget) {
489
527
  function truncateToolResultsForModel(messages, options = {}) {
490
528
  if (!Array.isArray(messages) || messages.length === 0) return [];
491
529
  return messages.map(message => {
492
- if (!message || message.role !== 'tool' || typeof message.content !== 'string') {
493
- return { ...message };
494
- }
530
+ if (!message || message.role !== 'tool') return { ...message };
495
531
  return {
496
532
  ...message,
497
533
  content: truncateToolResultIfNeeded(message.content, {
@@ -502,16 +538,124 @@ function truncateToolResultsForModel(messages, options = {}) {
502
538
  });
503
539
  }
504
540
 
541
+ const HISTORY_SOURCE_INDEX = Symbol('historySourceIndex');
542
+
543
+ function withHistorySourceIndexes(messages) {
544
+ return messages.map((message, index) => (
545
+ message && typeof message === 'object'
546
+ ? { ...message, [HISTORY_SOURCE_INDEX]: index }
547
+ : message
548
+ ));
549
+ }
550
+
551
+ function withoutHistorySourceIndexes(messages) {
552
+ return messages.map(message => {
553
+ if (!message || typeof message !== 'object') return message;
554
+ const next = { ...message };
555
+ delete next[HISTORY_SOURCE_INDEX];
556
+ return next;
557
+ });
558
+ }
559
+
560
+ function completeToolCallIds(messages) {
561
+ const resultIds = new Set(messages
562
+ .filter(message => message?.role === 'tool' && typeof message.toolCallId === 'string')
563
+ .map(message => message.toolCallId));
564
+ const ids = [];
565
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
566
+ const message = messages[index];
567
+ if (message?.role !== 'assistant' || !Array.isArray(message.toolCalls)) continue;
568
+ for (let callIndex = message.toolCalls.length - 1; callIndex >= 0; callIndex -= 1) {
569
+ const id = message.toolCalls[callIndex]?.id;
570
+ if (typeof id === 'string' && resultIds.has(id)) ids.push(id);
571
+ }
572
+ }
573
+ return ids;
574
+ }
575
+
576
+ function enrichTextBaselineWithTools(textBaseline, toolSource, selectedCallIds) {
577
+ const messagesBySourceIndex = new Map();
578
+ for (const message of textBaseline) {
579
+ if (Number.isInteger(message?.[HISTORY_SOURCE_INDEX])) {
580
+ messagesBySourceIndex.set(message[HISTORY_SOURCE_INDEX], { ...message });
581
+ }
582
+ }
583
+
584
+ for (const source of toolSource) {
585
+ const sourceIndex = source?.[HISTORY_SOURCE_INDEX];
586
+ if (!Number.isInteger(sourceIndex)) continue;
587
+ if (source.role === 'tool') {
588
+ if (selectedCallIds.has(source.toolCallId)) {
589
+ messagesBySourceIndex.set(sourceIndex, { ...source });
590
+ }
591
+ continue;
592
+ }
593
+ if (source.role !== 'assistant' || !Array.isArray(source.toolCalls)) continue;
594
+
595
+ const selectedCalls = source.toolCalls.filter(call => selectedCallIds.has(call?.id));
596
+ if (selectedCalls.length === 0) continue;
597
+ const selectedIds = new Set(selectedCalls.map(call => call.id));
598
+ const baseline = messagesBySourceIndex.get(sourceIndex);
599
+ const owner = baseline ? { ...baseline } : { ...source };
600
+ owner.toolCalls = selectedCalls;
601
+ if (Array.isArray(source.thinkingBlocks)) owner.thinkingBlocks = source.thinkingBlocks;
602
+ if (Array.isArray(source.content)) {
603
+ const baselineContent = Array.isArray(baseline?.content)
604
+ ? baseline.content
605
+ : stripToolContentParts(source.content);
606
+ const selectedParts = source.content.filter(part => selectedIds.has(toolContentPartCallId(part)));
607
+ owner.content = [...baselineContent, ...selectedParts];
608
+ }
609
+ messagesBySourceIndex.set(sourceIndex, owner);
610
+ }
611
+
612
+ return pairSanitize([...messagesBySourceIndex.entries()]
613
+ .sort(([left], [right]) => left - right)
614
+ .map(([, message]) => message));
615
+ }
616
+
617
+ function preservesTextBaseline(textBaseline, enriched) {
618
+ const projected = stripAllToolNoise(enriched);
619
+ if (projected.length !== textBaseline.length) return false;
620
+ return textBaseline.every((baseline, index) => {
621
+ const candidate = projected[index];
622
+ return candidate?.[HISTORY_SOURCE_INDEX] === baseline?.[HISTORY_SOURCE_INDEX]
623
+ && candidate?.role === baseline?.role
624
+ && serializeJsonValue(candidate?.content) === serializeJsonValue(baseline?.content);
625
+ });
626
+ }
627
+
628
+ function addOptionalRecentToolPairs(textBaseline, toolSource, options) {
629
+ const selectedCallIds = new Set();
630
+ let out = pairSanitize(textBaseline);
631
+ for (const callId of completeToolCallIds(toolSource)) {
632
+ const trialIds = new Set(selectedCallIds);
633
+ trialIds.add(callId);
634
+ const trial = enrichTextBaselineWithTools(textBaseline, toolSource, trialIds);
635
+ if (trial.length > options.maxMessageCount) continue;
636
+ const fitted = fitMessagesToBudget(trial, options.messageTokenBudget);
637
+ const fittedCallIds = new Set(completeToolCallIds(fitted));
638
+ if (![...trialIds].every(id => fittedCallIds.has(id))) continue;
639
+ if (!preservesTextBaseline(textBaseline, fitted)) continue;
640
+ selectedCallIds.add(callId);
641
+ out = fitted;
642
+ }
643
+ return out;
644
+ }
645
+
505
646
  /**
506
647
  * Build a bounded, pair-safe copy for one provider request.
507
648
  *
508
649
  * The transform is deterministic and non-persistent:
509
- * 1. keep at most `recentTurnCap` turns and `maxMessageCount` rows;
510
- * 2. drop oldest turns until the configured approximate message budget fits;
511
- * 3. remove old tool noise;
512
- * 4. bound large tool-result bodies and multimodal content;
650
+ * 1. keep at most `recentTurnCap` turns;
651
+ * 2. build the text-only history baseline and fit it to row/token caps;
652
+ * 3. consider complete tool/function pairs from the newest `keepToolTurns`;
653
+ * 4. add each recent pair only while the fitted text baseline still fits;
513
654
  * 5. remove orphan tool pairs.
514
655
  *
656
+ * Tool replay is optional enrichment. Text history gets the hard row/token
657
+ * budget first so even recent function payloads cannot evict textual turns.
658
+ *
515
659
  * @param {Array<object>} snapshot
516
660
  * @param {{ messageTokenBudget?: number, recentTurnCap?: number, maxMessageCount?: number, keepToolTurns?: number, language?: string }} [options]
517
661
  * @returns {Array<object>}
@@ -529,25 +673,24 @@ export function trimSnapshotForBudget(snapshot, options = {}) {
529
673
  ? Math.floor(options.maxMessageCount)
530
674
  : DEFAULT_RUNTIME_CACHE_MESSAGE_CAP;
531
675
 
532
- let trimmed = sliceLastNTurns(snapshot, recentTurnCap);
533
- if (trimmed.length > maxMessageCount) trimmed = trimmed.slice(-maxMessageCount);
534
- let remainingTurnCap = recentTurnCap;
535
- let tokens = estimateMessagesTokens(trimmed);
536
- while (tokens > messageTokenBudget && remainingTurnCap > 1) {
537
- const nextTurnCap = remainingTurnCap - 1;
538
- const next = sliceLastNTurns(trimmed, nextTurnCap);
539
- if (next.length === trimmed.length) break;
540
- remainingTurnCap = nextTurnCap;
541
- trimmed = next;
542
- tokens = estimateMessagesTokens(trimmed);
543
- }
544
-
545
- trimmed = stripToolNoiseFromOlderTurns(trimmed, {
546
- keepToolTurns: options.keepToolTurns,
676
+ const keepToolTurns = Number.isFinite(options.keepToolTurns) && options.keepToolTurns >= 0
677
+ ? Math.floor(options.keepToolTurns)
678
+ : DEFAULT_KEEP_TOOL_TURNS;
679
+ const bounded = withHistorySourceIndexes(sliceLastNTurns(snapshot, recentTurnCap));
680
+ let textBaseline = stripAllToolNoise(bounded);
681
+ textBaseline = dropOldestHistoryUntilMessageCap(textBaseline, maxMessageCount);
682
+ textBaseline = fitMessagesToBudget(textBaseline, messageTokenBudget);
683
+
684
+ const toolCutIndex = indexOfNthTurnFromEnd(bounded, keepToolTurns);
685
+ const recentToolSource = keepToolTurns > 0
686
+ ? bounded.slice(toolCutIndex < 0 ? 0 : toolCutIndex)
687
+ : [];
688
+ const truncatedToolSource = truncateToolResultsForModel(recentToolSource, { language: options.language });
689
+ const enriched = addOptionalRecentToolPairs(textBaseline, truncatedToolSource, {
690
+ maxMessageCount,
691
+ messageTokenBudget,
547
692
  });
548
- trimmed = truncateToolResultsForModel(trimmed, { language: options.language });
549
- trimmed = pairSanitize(trimmed);
550
- return fitMessagesToBudget(trimmed, messageTokenBudget);
693
+ return withoutHistorySourceIndexes(enriched);
551
694
  }
552
695
 
553
696
  /**
@@ -560,11 +703,13 @@ export function trimSnapshotForBudget(snapshot, options = {}) {
560
703
  * @returns {Array<object>}
561
704
  */
562
705
  export function trimHistoryCacheForRuntime(snapshot, options = {}) {
563
- return trimSnapshotForBudget(snapshot, {
564
- recentTurnCap: DEFAULT_RUNTIME_CACHE_TURN_CAP,
565
- messageTokenBudget: DEFAULT_RUNTIME_CACHE_TOKEN_BUDGET,
566
- maxMessageCount: DEFAULT_RUNTIME_CACHE_MESSAGE_CAP,
706
+ if (!Array.isArray(snapshot) || snapshot.length === 0) return [];
707
+ let trimmed = sliceLastNTurns(snapshot, DEFAULT_RUNTIME_CACHE_TURN_CAP);
708
+ trimmed = stripToolNoiseFromOlderTurns(trimmed, {
567
709
  keepToolTurns: DEFAULT_KEEP_TOOL_TURNS,
568
- language: options.language,
569
710
  });
711
+ trimmed = dropOldestHistoryUntilMessageCap(trimmed, DEFAULT_RUNTIME_CACHE_MESSAGE_CAP);
712
+ trimmed = truncateToolResultsForModel(trimmed, { language: options.language });
713
+ trimmed = pairSanitize(trimmed);
714
+ return fitMessagesToBudget(trimmed, DEFAULT_RUNTIME_CACHE_TOKEN_BUDGET);
570
715
  }