@@ -36,17 +36,20 @@ public sealed class ToolCallingChatOrchestrator
3636 private readonly ToolExecutorRegistry _executorRegistry ;
3737 private readonly ILogger < ToolCallingChatOrchestrator > _logger ;
3838 private readonly IToolStatusNotifier ? _statusNotifier ;
39+ private readonly LlmToolCallingSettings _settings ;
3940
4041 public ToolCallingChatOrchestrator (
4142 ILlmProvider provider ,
4243 ToolExecutorRegistry executorRegistry ,
4344 ILogger < ToolCallingChatOrchestrator > logger ,
44- IToolStatusNotifier ? statusNotifier = null )
45+ IToolStatusNotifier ? statusNotifier = null ,
46+ LlmToolCallingSettings ? settings = null )
4547 {
4648 _provider = provider ;
4749 _executorRegistry = executorRegistry ;
4850 _logger = logger ;
4951 _statusNotifier = statusNotifier ;
52+ _settings = settings ?? new LlmToolCallingSettings ( ) ;
5053 }
5154
5255 /// <summary>
@@ -232,6 +235,11 @@ await _statusNotifier.NotifyToolStatusAsync(
232235 }
233236 }
234237
238+ // Enforce token budget: truncate oversized tool results before they
239+ // are fed back to the LLM. This keeps the conversation within the
240+ // provider's context window even when a tool returns a large payload.
241+ resultContent = TruncateToolResult ( resultContent , _settings . MaxToolResultBytes ) ;
242+
235243 results . Add ( new ToolCallResult (
236244 toolCall . CallId , toolCall . ToolName , resultContent , isError , toolCall . Arguments ) ) ;
237245
@@ -409,6 +417,79 @@ private static ToolCallingResult BuildDegradedResult(
409417 DegradedReason : reason ?? "Tool calling is not available; falling back to single-turn." ) ;
410418 }
411419
420+ /// <summary>
421+ /// Truncates a tool result string to the configured byte budget so oversized
422+ /// payloads do not blow out the provider's context window.
423+ /// When <paramref name="maxBytes"/> is 0 or negative, no truncation is applied.
424+ /// A "...(truncated)" marker is appended so the LLM knows the result was cut short.
425+ /// The returned string is always within <paramref name="maxBytes"/> UTF-8 bytes.
426+ /// </summary>
427+ internal static string TruncateToolResult ( string content , int maxBytes )
428+ {
429+ if ( maxBytes <= 0 || string . IsNullOrEmpty ( content ) )
430+ return content ;
431+
432+ var utf8 = System . Text . Encoding . UTF8 ;
433+ var encoded = utf8 . GetByteCount ( content ) ;
434+ if ( encoded <= maxBytes )
435+ return content ;
436+
437+ const string marker = "...(truncated)" ;
438+ var markerBytes = utf8 . GetByteCount ( marker ) ;
439+
440+ // If the budget cannot even hold the marker, return as many bytes as fit
441+ // from the marker itself so the result is always <= maxBytes.
442+ if ( maxBytes <= markerBytes )
443+ return marker [ ..FindCharCountFittingBytes ( marker , maxBytes , utf8 ) ] ;
444+
445+ var maxContentBytes = maxBytes - markerBytes ;
446+
447+ // Binary search for the longest prefix whose UTF-8 encoding fits the budget.
448+ // Avoids the O(n) worst case of a decrementing walk and makes no heap
449+ // allocations during the search (GetByteCount accepts ReadOnlySpan<char>).
450+ var span = content . AsSpan ( ) ;
451+ var low = 0 ;
452+ var high = content . Length ;
453+ var best = 0 ;
454+ while ( low <= high )
455+ {
456+ var mid = low + ( ( high - low ) / 2 ) ;
457+ if ( utf8 . GetByteCount ( span [ ..mid ] ) <= maxContentBytes )
458+ {
459+ best = mid ;
460+ low = mid + 1 ;
461+ }
462+ else
463+ {
464+ high = mid - 1 ;
465+ }
466+ }
467+
468+ return best > 0 ? content [ ..best ] + marker : marker ;
469+ }
470+
471+ private static int FindCharCountFittingBytes ( string s , int maxBytes , System . Text . Encoding utf8 )
472+ {
473+ var low = 0 ;
474+ var high = s . Length ;
475+ var best = 0 ;
476+ var span = s . AsSpan ( ) ;
477+ while ( low <= high )
478+ {
479+ var mid = low + ( ( high - low ) / 2 ) ;
480+ if ( utf8 . GetByteCount ( span [ ..mid ] ) <= maxBytes )
481+ {
482+ best = mid ;
483+ low = mid + 1 ;
484+ }
485+ else
486+ {
487+ high = mid - 1 ;
488+ }
489+ }
490+ return best ;
491+ }
492+
412493 private static string TruncateForLog ( string content )
413494 {
414495 const int maxLength = 200 ;
0 commit comments