// Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; using System.Text; namespace Microsoft.Agents.AI.Tools.Shell; /// /// Bounded accumulator that keeps the first half of the input and the most recent /// half (rolling tail), summing to cap UTF-8 bytes total. When the input fits /// in cap bytes, the result is the original concatenation. Otherwise the middle /// is dropped and the result includes a "[... truncated N bytes ...]" marker. /// /// /// /// Used by and when /// streaming stdout / stderr from a long-running subprocess. Memory usage is bounded /// at roughly cap bytes regardless of how much is appended. /// /// /// The buffer counts UTF-8 bytes (matching the public maxOutputBytes contract /// and ). Append happens one rune at a time /// — when the head fills, the next rune's UTF-8 bytes go to the tail as an indivisible /// unit, and the oldest rune is dropped from the tail. This guarantees the final /// string never contains a split rune (no orphan surrogates, no invalid UTF-8). /// /// internal sealed class HeadTailBuffer { private readonly int _cap; private readonly int _headCap; private readonly int _tailCap; private readonly List _head = new(); // Tail is a queue of complete rune-byte-sequences so we can drop oldest rune // atomically when capacity is exceeded. private readonly Queue _tail = new(); private int _tailBytes; private long _totalBytes; public HeadTailBuffer(int cap) { this._cap = cap < 0 ? 0 : cap; // Split the budget so head and tail sum to exactly _cap. With odd caps, // the extra byte goes to the tail. This guarantees that any input whose // UTF-8 size is <= _cap round-trips losslessly (no silent data drop). this._headCap = this._cap / 2; this._tailCap = this._cap - this._headCap; } public void AppendLine(string line) { this.AppendInternal(line); this.AppendInternal("\n"); } private void AppendInternal(string s) { Span scratch = stackalloc byte[4]; foreach (var rune in s.EnumerateRunes()) { // Encode this rune to its UTF-8 bytes (1-4 bytes). var n = rune.EncodeToUtf8(scratch); this._totalBytes += n; if (this._head.Count + n <= this._headCap) { for (var i = 0; i < n; i++) { this._head.Add(scratch[i]); } continue; } // Head is full — append to tail as a single rune-sized chunk. var bytes = scratch[..n].ToArray(); this._tail.Enqueue(bytes); this._tailBytes += n; // Evict whole runes from the front of the tail until we fit. while (this._tailBytes > this._tailCap && this._tail.Count > 0) { var dropped = this._tail.Dequeue(); this._tailBytes -= dropped.Length; } } } public (string text, bool truncated) ToFinalString() { if (this._totalBytes <= this._cap) { var combinedBytes = new byte[this._head.Count + this._tailBytes]; this._head.CopyTo(combinedBytes, 0); var offset = this._head.Count; foreach (var chunk in this._tail) { Array.Copy(chunk, 0, combinedBytes, offset, chunk.Length); offset += chunk.Length; } return (Encoding.UTF8.GetString(combinedBytes), false); } var dropped = this._totalBytes - this._head.Count - this._tailBytes; var headStr = Encoding.UTF8.GetString(this._head.ToArray()); var tailBytes = new byte[this._tailBytes]; var tailOffset = 0; foreach (var chunk in this._tail) { Array.Copy(chunk, 0, tailBytes, tailOffset, chunk.Length); tailOffset += chunk.Length; } var tailStr = Encoding.UTF8.GetString(tailBytes); var sb = new StringBuilder(headStr.Length + tailStr.Length + 64); _ = sb.Append(headStr); _ = sb.Append('\n'); _ = sb.Append("[... truncated ").Append(dropped).Append(" bytes ...]"); _ = sb.Append('\n'); _ = sb.Append(tailStr); return (sb.ToString(), true); } }