에이전트 큐 우선순위 소비와 tool_result preview 재사용 안정화

목적:
- claude-code 대비 남아 있던 에이전틱 루프/큐/컨텍스트 격차를 줄이기 위한 1차 배치를 반영한다.
- 낮은 우선순위 알림이 같은 턴에 섞여 들어오던 흐름과 tool_result preview가 MsgId에만 묶여 재사용되던 한계를 개선한다.

핵심 수정:
- AgentCommandQueue를 snapshot/peek/dequeue/dequeueAllMatching/dequeuePriorityBatch를 지원하는 우선순위 큐로 재구성했다.
- AgentLoopService가 전체 큐를 한 번에 비우지 않고 같은 우선순위 배치만 소비하도록 조정했다.
- AgentToolResultBudget가 QueryPreviewContent를 tool_use_id 단위로 재사용하도록 확장해 재구성된 tool_result 메시지에서도 동일 preview를 유지한다.
- AgentCommandQueueTests, AgentToolResultBudgetTests를 확장해 priority batch dequeue, predicate matching, tool_use_id preview reuse를 회귀 검증한다.
- README와 DEVELOPMENT 문서에 2026-04-15 07:00 (KST) 기준 이력과 다음 통합 고도화 계획을 반영했다.

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_queue_preview\ -p:IntermediateOutputPath=obj\verify_queue_preview\
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentCommandQueueTests^|AgentToolResultBudgetTests -p:OutputPath=bin\verify_queue_preview_tests\ -p:IntermediateOutputPath=obj\verify_queue_preview_tests"
This commit is contained in:
2026-04-15 07:01:45 +09:00
parent f33ee7f7db
commit 07fd2267cb
7 changed files with 224 additions and 32 deletions

View File

@@ -1874,3 +1874,12 @@ MIT License
- PPT는 품질 리뷰 뒤에 바로 실행 가능한 보정 가이드를 붙이도록 [DeckRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs)를 추가했고, [PptxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PptxSkill.cs)는 `Deck repair guide:`를 함께 반환합니다. - PPT는 품질 리뷰 뒤에 바로 실행 가능한 보정 가이드를 붙이도록 [DeckRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs)를 추가했고, [PptxSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PptxSkill.cs)는 `Deck repair guide:`를 함께 반환합니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_master_batch\\ -p:IntermediateOutputPath=obj\\verify_master_batch\\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_master_batch\\ -p:IntermediateOutputPath=obj\\verify_master_batch\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|PptxSkillConsultingDeckTests|DeckRepairGuideServiceTests" -p:OutputPath=bin\\verify_master_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_master_batch_tests\\` 통과 35 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|PptxSkillConsultingDeckTests|DeckRepairGuideServiceTests" -p:OutputPath=bin\\verify_master_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_master_batch_tests\\` 통과 35
업데이트: 2026-04-15 07:00 (KST)
- `claw-code` 기준 남은 격차를 줄이기 위한 통합 고도화 계획을 확정했습니다. 우선순위는 `에이전틱 루프/명령 큐`, `tool_result preview 안정화`, `명령/스킬 합성`, `문서 포맷 마감`, `개발언어 지원 정합화`, `회귀 테스트/문서/릴리즈 게이트` 순서입니다.
- 첫 배치로 [AgentCommandQueue.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentCommandQueue.cs)를 `peek`, `dequeue`, `dequeueAllMatching`, `dequeuePriorityBatch`, `snapshot`까지 지원하는 우선순위 큐로 확장했습니다. 이제 높은 우선순위 입력이 들어온 턴에는 낮은 우선순위 notification을 다음 턴으로 미루어 `claw-code`에 더 가까운 부분 소비 흐름으로 동작합니다.
- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)는 위 큐를 이용해 한 번에 전체 큐를 비우지 않고, 같은 우선순위 배치만 소비한 뒤 남은 lower-priority 항목을 뒤로 미루도록 조정했습니다.
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)는 preview 재사용 기준을 `MsgId`뿐 아니라 `tool_use_id`까지 넓혔습니다. 같은 tool result가 재구성된 메시지로 다시 들어와도 이전 preview를 재사용해 query view 안정성을 높입니다.
- 테스트로 [AgentCommandQueueTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs), [AgentToolResultBudgetTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs)를 확장해 `priority batch dequeue`, `predicate matching`, `tool_use_id preview reuse`를 회귀 검증했습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_queue_preview\\ -p:IntermediateOutputPath=obj\\verify_queue_preview\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|AgentToolResultBudgetTests" -p:OutputPath=bin\\verify_queue_preview_tests\\ -p:IntermediateOutputPath=obj\\verify_queue_preview_tests\\` 통과 7

View File

@@ -949,3 +949,13 @@ UI ?붿옄???€洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
- 테스트: [AgentCommandQueueTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs), [CodeLanguageCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs), [WorkspaceContextGeneratorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs), [PptxSkillConsultingDeckTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillConsultingDeckTests.cs) - 테스트: [AgentCommandQueueTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs), [CodeLanguageCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs), [WorkspaceContextGeneratorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs), [DeckRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckRepairGuideServiceTests.cs), [PptxSkillConsultingDeckTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/PptxSkillConsultingDeckTests.cs)
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_master_batch\\ -p:IntermediateOutputPath=obj\\verify_master_batch\\` 경고 0 / 오류 0 - 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_master_batch\\ -p:IntermediateOutputPath=obj\\verify_master_batch\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|PptxSkillConsultingDeckTests|DeckRepairGuideServiceTests" -p:OutputPath=bin\\verify_master_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_master_batch_tests\\` 통과 35 - 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|PptxSkillConsultingDeckTests|DeckRepairGuideServiceTests" -p:OutputPath=bin\\verify_master_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_master_batch_tests\\` 통과 35
업데이트: 2026-04-15 07:00 (KST)
- `claw-code` 기준 남은 격차를 줄이기 위한 통합 고도화 계획을 확정했습니다. 남은 주요 축은 `에이전틱 루프/명령 큐`, `tool_result preview 안정화`, `명령/스킬 합성`, `문서 포맷 마감`, `개발언어 지원 정합화`, `회귀 테스트/릴리즈 게이트`입니다.
- 첫 배치로 [AgentCommandQueue.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentCommandQueue.cs)를 우선순위 배치 소비가 가능한 구조로 재작성했습니다. `peek`, `dequeue`, `dequeueAllMatching`, `dequeuePriorityBatch`, `snapshot` API를 추가해 `claw-code`의 unified queue처럼 고우선 입력을 먼저 소비하고 lower-priority 항목을 뒤로 미루는 기반을 만들었습니다.
- [AgentLoopService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopService.cs)의 큐 배수 로직도 함께 조정했습니다. 기존 `DrainAll()` 방식 대신 같은 우선순위 배치만 소비하고, 남은 큐 항목이 있으면 `Deferred ... lower-priority queued item(s)` thinking 이벤트를 남겨 다음 턴으로 넘깁니다.
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)는 preview 재사용 범위를 넓혔습니다. 기존에는 동일 `MsgId`에서만 `QueryPreviewContent`를 재사용했지만, 이제 `tool_use_id` 기준 preview 인덱스를 만들어 재구성된 tool result 메시지에서도 안정적으로 같은 preview를 재사용합니다.
- 테스트는 [AgentCommandQueueTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentCommandQueueTests.cs)에 `priority batch dequeue`, `predicate matching` 시나리오를 추가했고, [AgentToolResultBudgetTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/AgentToolResultBudgetTests.cs)에는 `tool_use_id`가 같은 cloned tool result가 이전 preview를 재사용하는 회귀 케이스를 추가했습니다.
- 다음 배치에서는 `tool_result replacement state`를 대화 단위로 더 고정하고, 이후 `명령/스킬 합성 계층`과 `문서 포맷 마감`으로 순차 확장할 예정입니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_queue_preview\\ -p:IntermediateOutputPath=obj\\verify_queue_preview\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentCommandQueueTests|AgentToolResultBudgetTests" -p:OutputPath=bin\\verify_queue_preview_tests\\ -p:IntermediateOutputPath=obj\\verify_queue_preview_tests\\` 통과 7

View File

@@ -52,4 +52,39 @@ public class AgentCommandQueueTests
drained[1].RequestInterrupt.Should().BeTrue(); drained[1].RequestInterrupt.Should().BeTrue();
drained[2].RequestInterrupt.Should().BeTrue(); drained[2].RequestInterrupt.Should().BeTrue();
} }
[Fact]
public void DequeuePriorityBatch_ShouldLeaveLowerPriorityItemsQueued()
{
var queue = new AgentCommandQueue();
queue.EnqueueNotification("later-note");
queue.EnqueuePrompt("next-prompt");
queue.EnqueueSteering("now-steer");
var firstBatch = queue.DequeuePriorityBatch();
var secondBatch = queue.DequeuePriorityBatch();
var thirdBatch = queue.DequeuePriorityBatch();
firstBatch.Select(x => x.Content).Should().Equal("now-steer");
secondBatch.Select(x => x.Content).Should().Equal("next-prompt");
thirdBatch.Select(x => x.Content).Should().Equal("later-note");
}
[Fact]
public void PeekAndDequeueAllMatching_ShouldHonorPredicate()
{
var queue = new AgentCommandQueue();
queue.EnqueueNotification("later-note");
queue.EnqueuePrompt("next-prompt");
queue.EnqueueSteering("now-steer");
var peeked = queue.Peek(x => x.Kind == AgentCommandKind.Notification);
var removed = queue.DequeueAllMatching(x => x.Kind == AgentCommandKind.Notification);
var remaining = queue.DrainAll();
peeked.Should().NotBeNull();
peeked!.Content.Should().Be("later-note");
removed.Select(x => x.Content).Should().Equal("later-note");
remaining.Select(x => x.Content).Should().Equal("now-steer", "next-prompt");
}
} }

View File

@@ -55,4 +55,47 @@ public class AgentToolResultBudgetTests
second.ReusedPreviewCount.Should().Be(1); second.ReusedPreviewCount.Should().Be(1);
secondWindow[0].Content.Should().Be(sourceMessages[0].QueryPreviewContent); secondWindow[0].Content.Should().Be(sourceMessages[0].QueryPreviewContent);
} }
[Fact]
public void Apply_ShouldReusePreviewByToolUseIdAcrossClonedMessages()
{
var longContent = new string('B', 1500);
var sourceMessages = new List<ChatMessage>
{
new()
{
MsgId = "source-tool",
Role = "user",
Content = $$"""{"type":"tool_result","tool_use_id":"call-shared","tool_name":"file_read","content":"{{longContent}}"}""",
QueryPreviewContent = """{"type":"tool_result","tool_use_id":"call-shared","tool_name":"file_read","content":"preview"}"""
},
new()
{
MsgId = "tail-1",
Role = "assistant",
Content = "recent tail"
}
};
var queryView = new List<ChatMessage>
{
new()
{
MsgId = "rebuilt-tool",
Role = "user",
Content = $$"""{"type":"tool_result","tool_use_id":"call-shared","tool_name":"file_read","content":"{{longContent}}"}"""
},
new()
{
MsgId = "tail-2",
Role = "assistant",
Content = "recent tail"
}
};
var result = AgentToolResultBudget.Apply(queryView, protectedRecentNonSystemMessages: 1, sourceMessages: sourceMessages);
result.ReusedPreviewCount.Should().Be(1);
queryView[0].Content.Should().Be(sourceMessages[0].QueryPreviewContent);
}
} }

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -38,9 +37,8 @@ public sealed record AgentQueuedCommand(
/// </summary> /// </summary>
public sealed class AgentCommandQueue public sealed class AgentCommandQueue
{ {
private readonly ConcurrentQueue<AgentQueuedCommand> _now = new(); private readonly object _gate = new();
private readonly ConcurrentQueue<AgentQueuedCommand> _next = new(); private readonly List<AgentQueuedCommand> _items = [];
private readonly ConcurrentQueue<AgentQueuedCommand> _later = new();
private long _sequence; private long _sequence;
public void EnqueuePrompt(string content, string priority = "next", bool requestInterrupt = false) public void EnqueuePrompt(string content, string priority = "next", bool requestInterrupt = false)
@@ -63,21 +61,81 @@ public sealed class AgentCommandQueue
public void Clear() public void Clear()
{ {
while (_now.TryDequeue(out _)) { } lock (_gate)
while (_next.TryDequeue(out _)) { } {
while (_later.TryDequeue(out _)) { } _items.Clear();
}
} }
public List<AgentQueuedCommand> DrainAll() public List<AgentQueuedCommand> DrainAll()
=> DequeueAllMatching(static _ => true);
public IReadOnlyList<AgentQueuedCommand> Snapshot()
{ {
var items = new List<AgentQueuedCommand>(); lock (_gate)
DrainInto(_now, items); {
DrainInto(_next, items); return Order(_items).ToList();
DrainInto(_later, items); }
return items }
.OrderBy(x => x.Priority)
.ThenBy(x => x.Sequence) public AgentQueuedCommand? Peek(Func<AgentQueuedCommand, bool>? predicate = null)
.ToList(); {
lock (_gate)
{
return Order(_items)
.FirstOrDefault(item => predicate == null || predicate(item));
}
}
public AgentQueuedCommand? Dequeue(Func<AgentQueuedCommand, bool>? predicate = null)
{
lock (_gate)
{
var item = Order(_items)
.FirstOrDefault(candidate => predicate == null || predicate(candidate));
if (item == null)
return null;
_items.Remove(item);
return item;
}
}
public List<AgentQueuedCommand> DequeueAllMatching(Func<AgentQueuedCommand, bool> predicate)
{
lock (_gate)
{
var matched = _items.Where(predicate).ToList();
if (matched.Count == 0)
return [];
foreach (var item in matched)
_items.Remove(item);
return Order(matched).ToList();
}
}
public List<AgentQueuedCommand> DequeuePriorityBatch(Func<AgentQueuedCommand, bool>? predicate = null)
{
lock (_gate)
{
var ordered = Order(_items)
.Where(item => predicate == null || predicate(item))
.ToList();
if (ordered.Count == 0)
return [];
var targetPriority = ordered[0].Priority;
var batch = ordered
.Where(item => item.Priority == targetPriority)
.ToList();
foreach (var item in batch)
_items.Remove(item);
return batch;
}
} }
private void Enqueue(AgentCommandKind kind, string content, string? priority, bool requestInterrupt) private void Enqueue(AgentCommandKind kind, string content, string? priority, bool requestInterrupt)
@@ -93,25 +151,16 @@ public sealed class AgentCommandQueue
DateTime.Now, DateTime.Now,
requestInterrupt); requestInterrupt);
switch (item.Priority) lock (_gate)
{ {
case AgentQueuePriority.Now: _items.Add(item);
_now.Enqueue(item);
break;
case AgentQueuePriority.Later:
_later.Enqueue(item);
break;
default:
_next.Enqueue(item);
break;
} }
} }
private static void DrainInto(ConcurrentQueue<AgentQueuedCommand> queue, List<AgentQueuedCommand> target) private static IEnumerable<AgentQueuedCommand> Order(IEnumerable<AgentQueuedCommand> items)
{ => items
while (queue.TryDequeue(out var item)) .OrderBy(x => x.Priority)
target.Add(item); .ThenBy(x => x.Sequence);
}
private static AgentQueuePriority NormalizePriority(string? priority) private static AgentQueuePriority NormalizePriority(string? priority)
=> priority?.Trim().ToLowerInvariant() switch => priority?.Trim().ToLowerInvariant() switch

View File

@@ -89,7 +89,11 @@ public partial class AgentLoopService
private void DrainPendingCommands(List<ChatMessage> messages) private void DrainPendingCommands(List<ChatMessage> messages)
{ {
var drained = _pendingCommands.DrainAll(); var queuedSnapshot = _pendingCommands.Snapshot();
if (queuedSnapshot.Count == 0)
return;
var drained = _pendingCommands.DequeuePriorityBatch();
if (drained.Count == 0) if (drained.Count == 0)
return; return;
@@ -164,10 +168,19 @@ public partial class AgentLoopService
Content = item.Content, Content = item.Content,
Timestamp = item.CreatedAt, Timestamp = item.CreatedAt,
}); });
EmitEvent(AgentEventType.UserMessage, "", item.Content); EmitEvent(AgentEventType.UserMessage, "", item.Content);
break; break;
} }
} }
var deferredCount = queuedSnapshot.Count - drained.Count;
if (deferredCount > 0)
{
EmitEvent(
AgentEventType.Thinking,
"",
$"Deferred {deferredCount} lower-priority queued item(s) for a later turn.");
}
} }
/// <summary>에이전트 이벤트 스트림 (UI 바인딩용).</summary> /// <summary>에이전트 이벤트 스트림 (UI 바인딩용).</summary>

View File

@@ -30,6 +30,7 @@ public static class AgentToolResultBudget
var sourceById = sourceMessages? var sourceById = sourceMessages?
.Where(message => !string.IsNullOrWhiteSpace(message.MsgId)) .Where(message => !string.IsNullOrWhiteSpace(message.MsgId))
.ToDictionary(message => message.MsgId, StringComparer.OrdinalIgnoreCase); .ToDictionary(message => message.MsgId, StringComparer.OrdinalIgnoreCase);
var sourcePreviewByToolResultId = BuildPreviewByToolResultId(sourceMessages);
var nonSystemIndexes = messages var nonSystemIndexes = messages
.Select((message, index) => new { message, index }) .Select((message, index) => new { message, index })
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase)) .Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))
@@ -55,6 +56,19 @@ public static class AgentToolResultBudget
result.ProcessedCount++; result.ProcessedCount++;
result.TotalCharsBefore += content.Length; result.TotalCharsBefore += content.Length;
if (string.IsNullOrWhiteSpace(message.QueryPreviewContent)
&& AgentMessageInvariantHelper.TryGetToolResultId(message, out var toolResultId)
&& sourcePreviewByToolResultId.TryGetValue(toolResultId, out var persistedPreview))
{
message.QueryPreviewContent = persistedPreview;
if (sourceById != null
&& sourceById.TryGetValue(message.MsgId, out var sourceByMessageId)
&& string.IsNullOrWhiteSpace(sourceByMessageId.QueryPreviewContent))
{
sourceByMessageId.QueryPreviewContent = persistedPreview;
}
}
if (!string.IsNullOrWhiteSpace(message.QueryPreviewContent)) if (!string.IsNullOrWhiteSpace(message.QueryPreviewContent))
{ {
messages[i] = CloneMessage(message, message.QueryPreviewContent); messages[i] = CloneMessage(message, message.QueryPreviewContent);
@@ -86,6 +100,25 @@ public static class AgentToolResultBudget
return result; return result;
} }
private static Dictionary<string, string> BuildPreviewByToolResultId(IReadOnlyList<ChatMessage>? sourceMessages)
{
var previews = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (sourceMessages == null)
return previews;
foreach (var message in sourceMessages)
{
if (string.IsNullOrWhiteSpace(message.QueryPreviewContent))
continue;
if (!AgentMessageInvariantHelper.TryGetToolResultId(message, out var toolResultId))
continue;
previews[toolResultId] = message.QueryPreviewContent!;
}
return previews;
}
public static string TruncateToolResultJson(string json, int softCharLimit = DefaultSoftCharLimit) public static string TruncateToolResultJson(string json, int softCharLimit = DefaultSoftCharLimit)
{ {
try try