오래된 tool_result를 시간 기반으로 정리하고 compact 직후 안내를 단순화
- 마지막 assistant 이후 긴 휴지기가 있으면 오래된 tool_result를 cleared marker로 교체하는 time-based microcompact 성격의 분기 추가 - compact 직후 첫 턴에서 운영성 thinking 문구를 다시 띄우지 않도록 AgentLoopCompactionPolicy 정리 - README와 DEVELOPMENT 문서에 2026-04-12 22:11 (KST) 기준 작업 이력 반영 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\ (경고 0, 오류 0)
This commit is contained in:
@@ -1638,3 +1638,8 @@ MIT License
|
|||||||
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)를 추가해 오래된 `tool_result`를 최근 보호 구간과 aggregate budget 기준으로 공용 축약하도록 만들었습니다.
|
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)를 추가해 오래된 `tool_result`를 최근 보호 구간과 aggregate budget 기준으로 공용 축약하도록 만들었습니다.
|
||||||
- [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs)는 query view에서 이 공용 helper를 사용하게 바뀌어, 전송 직전 budget과 축약 결과가 압축 본체와 같은 규칙을 따릅니다.
|
- [AgentQueryContextBuilder.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentQueryContextBuilder.cs)는 query view에서 이 공용 helper를 사용하게 바뀌어, 전송 직전 budget과 축약 결과가 압축 본체와 같은 규칙을 따릅니다.
|
||||||
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 1단계에서 `tool_result`를 별도 공용 budget으로 먼저 줄이고, 그 외 긴 assistant/user 메시지만 추가 절단하도록 역할을 분리했습니다.
|
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 1단계에서 `tool_result`를 별도 공용 budget으로 먼저 줄이고, 그 외 긴 assistant/user 메시지만 추가 절단하도록 역할을 분리했습니다.
|
||||||
|
- 업데이트: 2026-04-12 22:11 (KST)
|
||||||
|
- `claw-code`의 time-based microcompact 방향을 따라, 오래 쉬었다가 다시 호출할 때 오래된 `tool_result`를 먼저 정리하는 분기를 AX에 추가했습니다.
|
||||||
|
- [ContextCondenser.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ContextCondenser.cs)는 마지막 assistant 이후 20분 이상 경과한 경우, 가장 최근 `tool_result` 1개만 남기고 나머지는 작은 cleared marker로 바꾸도록 처리합니다.
|
||||||
|
- [AgentToolResultBudget.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentToolResultBudget.cs)는 cleared `tool_result` JSON을 만드는 helper를 추가해, 기존 `tool_use_id`/`tool_name`은 유지하면서 content만 가볍게 비우도록 보강했습니다.
|
||||||
|
- [AgentLoopCompactionPolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs)는 compact 직후 첫 턴에서 추가 운영성 thinking 문구를 다시 띄우지 않고 내부 상태만 갱신하도록 바꿔, compact 후 흐름이 더 조용하게 이어지도록 정리했습니다.
|
||||||
|
|||||||
@@ -615,3 +615,19 @@ owKindCounts를 함께 남겨 %APPDATA%\\AxCopilot\\perf 기준으로 transcript
|
|||||||
- query view와 저장 상태 사이에서 `tool_result` 축약 기준이 달라 생기던 흔들림이 줄어듭니다.
|
- query view와 저장 상태 사이에서 `tool_result` 축약 기준이 달라 생기던 흔들림이 줄어듭니다.
|
||||||
- 같은 세션을 반복 호출할 때 오래된 `tool_result`의 토큰 사용량이 더 예측 가능해집니다.
|
- 같은 세션을 반복 호출할 때 오래된 `tool_result`의 토큰 사용량이 더 예측 가능해집니다.
|
||||||
|
|
||||||
|
## time-based tool_result 정리 / post-compact 안내 축소 (2026-04-12 22:11 KST)
|
||||||
|
|
||||||
|
- `claw-code`의 `maybeTimeBasedMicrocompact()`처럼, 오래 쉬었다가 다시 호출할 때 오래된 `tool_result`를 먼저 비워 prompt 재전송량을 줄이는 분기를 AX에 추가했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/ContextCondenser.cs`
|
||||||
|
- 마지막 assistant 응답 이후 20분 이상 경과했고 오래된 `tool_result`가 여러 개 남아 있으면, 가장 최근 1개를 제외한 나머지를 cleared marker로 교체합니다.
|
||||||
|
- 이 단계는 일반 토큰 한계 기반 compact보다 먼저 실행되어, 긴 휴지기 뒤 첫 호출의 prompt 부피를 줄이는 역할을 합니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/AgentToolResultBudget.cs`
|
||||||
|
- 기존 budget 축약 외에 cleared `tool_result` JSON 생성 helper를 추가했습니다.
|
||||||
|
- `tool_use_id`/`tool_name`은 유지하고 content만 작은 marker로 바꿔 pairing 정보는 잃지 않도록 했습니다.
|
||||||
|
- `src/AxCopilot/Services/Agent/AgentLoopCompactionPolicy.cs`
|
||||||
|
- compact 직후 첫 턴에서 `compact 이후 n번째 턴` 같은 운영성 thinking 문구를 다시 띄우지 않고 내부 상태만 갱신하도록 바꿨습니다.
|
||||||
|
- 이미 `컨텍스트 압축 완료` 이벤트를 한 번 내보낸 뒤라, 추가 안내를 생략해 `claw-code`처럼 compact 후 흐름이 더 자연스럽게 이어지도록 맞췄습니다.
|
||||||
|
- 기대 효과
|
||||||
|
- 오래된 세션을 다시 이어갈 때 과거 `tool_result` 때문에 첫 요청이 불필요하게 비대해지는 현상이 줄어듭니다.
|
||||||
|
- compact 직후 transcript에 운영 문구가 한 번 더 끼어드는 노이즈가 줄어듭니다.
|
||||||
|
|
||||||
|
|||||||
@@ -19,17 +19,6 @@ public partial class AgentLoopService
|
|||||||
runState.PendingPostCompactionTurn = false;
|
runState.PendingPostCompactionTurn = false;
|
||||||
runState.PostCompactionTurnCounter++;
|
runState.PostCompactionTurnCounter++;
|
||||||
SyncRunPostCompactionState(runState);
|
SyncRunPostCompactionState(runState);
|
||||||
|
|
||||||
var stage = string.IsNullOrWhiteSpace(runState.LastCompactionStageSummary)
|
|
||||||
? "기본"
|
|
||||||
: runState.LastCompactionStageSummary;
|
|
||||||
var saved = runState.LastCompactionSavedTokens > 0
|
|
||||||
? $" · {Services.TokenEstimator.Format(runState.LastCompactionSavedTokens)} tokens 절감"
|
|
||||||
: "";
|
|
||||||
EmitEvent(
|
|
||||||
AgentEventType.Thinking,
|
|
||||||
"",
|
|
||||||
$"compact 이후 {runState.PostCompactionTurnCounter}번째 턴 · {stage}{saved}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncRunPostCompactionState(RunState runState)
|
private void SyncRunPostCompactionState(RunState runState)
|
||||||
@@ -59,7 +48,7 @@ public partial class AgentLoopService
|
|||||||
|| summary.StartsWith("컨텍스트 압축 완료", StringComparison.Ordinal);
|
|| summary.StartsWith("컨텍스트 압축 완료", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildLoopToolResultMessage(LlmService.ContentBlock call, ToolResult result, RunState runState)
|
private string BuildLoopToolResultMessage(ContentBlock call, ToolResult result, RunState runState)
|
||||||
{
|
{
|
||||||
var output = result.Output ?? "";
|
var output = result.Output ?? "";
|
||||||
if (!ShouldCompactToolResultForPostCompactionTurn(runState, call.ToolName, output))
|
if (!ShouldCompactToolResultForPostCompactionTurn(runState, call.ToolName, output))
|
||||||
|
|||||||
@@ -106,6 +106,32 @@ public static class AgentToolResultBudget
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string CreateClearedToolResultJson(string json, string clearedContent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
var toolType = root.TryGetProperty("type", out var typeEl) ? typeEl.GetString() ?? "" : "";
|
||||||
|
if (!string.Equals(toolType, "tool_result", StringComparison.Ordinal))
|
||||||
|
return json;
|
||||||
|
|
||||||
|
var toolUseId = root.TryGetProperty("tool_use_id", out var idEl) ? idEl.GetString() ?? "" : "";
|
||||||
|
var toolName = root.TryGetProperty("tool_name", out var nameEl) ? nameEl.GetString() ?? "" : "";
|
||||||
|
return JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
type = "tool_result",
|
||||||
|
tool_use_id = toolUseId,
|
||||||
|
tool_name = toolName,
|
||||||
|
content = clearedContent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return clearedContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ChatMessage CloneMessage(ChatMessage source, string content)
|
private static ChatMessage CloneMessage(ChatMessage source, string content)
|
||||||
{
|
{
|
||||||
return new ChatMessage
|
return new ChatMessage
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ public static class ContextCondenser
|
|||||||
private const int RecentKeepCount = 6;
|
private const int RecentKeepCount = 6;
|
||||||
private const int AutoCompactBufferTokens = 13_000;
|
private const int AutoCompactBufferTokens = 13_000;
|
||||||
private const int SummaryReserveTokens = 20_000;
|
private const int SummaryReserveTokens = 20_000;
|
||||||
|
private const int TimeBasedToolResultGapMinutes = 20;
|
||||||
|
private const int TimeBasedKeepRecentToolResults = 1;
|
||||||
|
private const string TimeBasedClearedToolResultMessage = "[time-based microcompact] 이전 tool_result 내용이 정리되었습니다.";
|
||||||
|
|
||||||
private sealed class CompactionWindow
|
private sealed class CompactionWindow
|
||||||
{
|
{
|
||||||
@@ -119,8 +122,16 @@ public static class ContextCondenser
|
|||||||
|
|
||||||
var currentTokens = TokenEstimator.EstimateMessages(messages);
|
var currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
result.BeforeTokens = currentTokens;
|
result.BeforeTokens = currentTokens;
|
||||||
|
|
||||||
|
if (ApplyTimeBasedToolResultCompaction(messages, out var clearedToolResults))
|
||||||
|
{
|
||||||
|
result.AppliedStages.Add($"time-gap-tool-result({clearedToolResults})");
|
||||||
|
currentTokens = TokenEstimator.EstimateMessages(messages);
|
||||||
|
}
|
||||||
|
|
||||||
if (!force && currentTokens < threshold)
|
if (!force && currentTokens < threshold)
|
||||||
{
|
{
|
||||||
|
result.Changed = result.AppliedStages.Count > 0;
|
||||||
result.AfterTokens = currentTokens;
|
result.AfterTokens = currentTokens;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -240,6 +251,55 @@ public static class ContextCondenser
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool ApplyTimeBasedToolResultCompaction(List<ChatMessage> messages, out int clearedCount)
|
||||||
|
{
|
||||||
|
clearedCount = 0;
|
||||||
|
var lastAssistant = messages
|
||||||
|
.Where(m => string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderByDescending(m => m.Timestamp)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (lastAssistant?.Timestamp is not DateTime lastAssistantAt)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var gapMinutes = (DateTime.Now - lastAssistantAt).TotalMinutes;
|
||||||
|
if (!double.IsFinite(gapMinutes) || gapMinutes < TimeBasedToolResultGapMinutes)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var candidateIndexes = messages
|
||||||
|
.Select((message, index) => new { message, index })
|
||||||
|
.Where(x => AgentMessageInvariantHelper.TryGetToolResultId(x.message, out _))
|
||||||
|
.Select(x => x.index)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (candidateIndexes.Count <= TimeBasedKeepRecentToolResults)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var keepSet = candidateIndexes
|
||||||
|
.TakeLast(Math.Max(1, TimeBasedKeepRecentToolResults))
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
foreach (var index in candidateIndexes)
|
||||||
|
{
|
||||||
|
if (keepSet.Contains(index))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var message = messages[index];
|
||||||
|
var content = message.Content ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var cleared = AgentToolResultBudget.CreateClearedToolResultJson(content, TimeBasedClearedToolResultMessage);
|
||||||
|
if (string.Equals(cleared, content, StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
messages[index] = CloneWithContent(message, cleared);
|
||||||
|
clearedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clearedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 1단계: 오래된 tool_result는 aggregate budget 기준으로 먼저 줄이고,
|
/// 1단계: 오래된 tool_result는 aggregate budget 기준으로 먼저 줄이고,
|
||||||
/// 그 외 긴 assistant/user 메시지는 경량 절단합니다.
|
/// 그 외 긴 assistant/user 메시지는 경량 절단합니다.
|
||||||
|
|||||||
Reference in New Issue
Block a user