에이전트 큐 우선순위 소비와 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:
@@ -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:`를 함께 반환합니다.
|
||||
- 검증: `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
|
||||
|
||||
업데이트: 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
|
||||
|
||||
@@ -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)
|
||||
- 검증: `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
|
||||
|
||||
업데이트: 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
|
||||
|
||||
@@ -52,4 +52,39 @@ public class AgentCommandQueueTests
|
||||
drained[1].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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,4 +55,47 @@ public class AgentToolResultBudgetTests
|
||||
second.ReusedPreviewCount.Should().Be(1);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -38,9 +37,8 @@ public sealed record AgentQueuedCommand(
|
||||
/// </summary>
|
||||
public sealed class AgentCommandQueue
|
||||
{
|
||||
private readonly ConcurrentQueue<AgentQueuedCommand> _now = new();
|
||||
private readonly ConcurrentQueue<AgentQueuedCommand> _next = new();
|
||||
private readonly ConcurrentQueue<AgentQueuedCommand> _later = new();
|
||||
private readonly object _gate = new();
|
||||
private readonly List<AgentQueuedCommand> _items = [];
|
||||
private long _sequence;
|
||||
|
||||
public void EnqueuePrompt(string content, string priority = "next", bool requestInterrupt = false)
|
||||
@@ -63,21 +61,81 @@ public sealed class AgentCommandQueue
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
while (_now.TryDequeue(out _)) { }
|
||||
while (_next.TryDequeue(out _)) { }
|
||||
while (_later.TryDequeue(out _)) { }
|
||||
lock (_gate)
|
||||
{
|
||||
_items.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public List<AgentQueuedCommand> DrainAll()
|
||||
=> DequeueAllMatching(static _ => true);
|
||||
|
||||
public IReadOnlyList<AgentQueuedCommand> Snapshot()
|
||||
{
|
||||
var items = new List<AgentQueuedCommand>();
|
||||
DrainInto(_now, items);
|
||||
DrainInto(_next, items);
|
||||
DrainInto(_later, items);
|
||||
return items
|
||||
.OrderBy(x => x.Priority)
|
||||
.ThenBy(x => x.Sequence)
|
||||
.ToList();
|
||||
lock (_gate)
|
||||
{
|
||||
return Order(_items).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public AgentQueuedCommand? Peek(Func<AgentQueuedCommand, bool>? predicate = null)
|
||||
{
|
||||
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)
|
||||
@@ -93,25 +151,16 @@ public sealed class AgentCommandQueue
|
||||
DateTime.Now,
|
||||
requestInterrupt);
|
||||
|
||||
switch (item.Priority)
|
||||
lock (_gate)
|
||||
{
|
||||
case AgentQueuePriority.Now:
|
||||
_now.Enqueue(item);
|
||||
break;
|
||||
case AgentQueuePriority.Later:
|
||||
_later.Enqueue(item);
|
||||
break;
|
||||
default:
|
||||
_next.Enqueue(item);
|
||||
break;
|
||||
_items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrainInto(ConcurrentQueue<AgentQueuedCommand> queue, List<AgentQueuedCommand> target)
|
||||
{
|
||||
while (queue.TryDequeue(out var item))
|
||||
target.Add(item);
|
||||
}
|
||||
private static IEnumerable<AgentQueuedCommand> Order(IEnumerable<AgentQueuedCommand> items)
|
||||
=> items
|
||||
.OrderBy(x => x.Priority)
|
||||
.ThenBy(x => x.Sequence);
|
||||
|
||||
private static AgentQueuePriority NormalizePriority(string? priority)
|
||||
=> priority?.Trim().ToLowerInvariant() switch
|
||||
|
||||
@@ -89,7 +89,11 @@ public partial class AgentLoopService
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
@@ -164,10 +168,19 @@ public partial class AgentLoopService
|
||||
Content = item.Content,
|
||||
Timestamp = item.CreatedAt,
|
||||
});
|
||||
EmitEvent(AgentEventType.UserMessage, "", item.Content);
|
||||
EmitEvent(AgentEventType.UserMessage, "", item.Content);
|
||||
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>
|
||||
|
||||
@@ -30,6 +30,7 @@ public static class AgentToolResultBudget
|
||||
var sourceById = sourceMessages?
|
||||
.Where(message => !string.IsNullOrWhiteSpace(message.MsgId))
|
||||
.ToDictionary(message => message.MsgId, StringComparer.OrdinalIgnoreCase);
|
||||
var sourcePreviewByToolResultId = BuildPreviewByToolResultId(sourceMessages);
|
||||
var nonSystemIndexes = messages
|
||||
.Select((message, index) => new { message, index })
|
||||
.Where(x => !string.Equals(x.message.Role, "system", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -55,6 +56,19 @@ public static class AgentToolResultBudget
|
||||
result.ProcessedCount++;
|
||||
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))
|
||||
{
|
||||
messages[i] = CloneMessage(message, message.QueryPreviewContent);
|
||||
@@ -86,6 +100,25 @@ public static class AgentToolResultBudget
|
||||
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)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user