vLLM 도구 호출 스트리밍 실행기와 코워크 루프 실시간 소비 구조 추가
Some checks failed
Release Gate / gate (push) Has been cancelled

- LlmService에 tool-use 전용 streaming event API를 추가하고 OpenAI vLLM IBM 경로의 partial tool_call 조립을 event 기반으로 재구성함
- Cowork/Code 루프가 streaming event를 직접 소비하도록 바꿔 도구 호출 감지와 진행 표시를 더 빠르게 갱신함
- read-only 도구 조기 실행이 기존 loop와 실제로 이어지도록 정리하고 최종 실행에서는 prefetch 결과를 재사용함
- README와 DEVELOPMENT 문서를 2026-04-08 11:31(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:
2026-04-08 16:58:11 +09:00
parent 90ef3400f6
commit 6e99837a4c
5 changed files with 309 additions and 50 deletions

View File

@@ -2,6 +2,7 @@
using System.Diagnostics;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Text.Json;
using AxCopilot.Models;
using AxCopilot.Services;
@@ -548,6 +549,8 @@ public partial class AgentLoopService
var (_, currentModel) = _llm.GetCurrentModelInfo();
WorkflowLogService.LogLlmRequest(_conversationId, _currentRunId, iteration,
currentModel, sendMessages.Count, activeTools.Count, forceFirst);
var streamedTextPreview = new StringBuilder();
var lastStreamUiUpdateAt = DateTime.MinValue;
blocks = await SendWithToolsWithRecoveryAsync(
sendMessages,
@@ -560,7 +563,39 @@ public partial class AgentLoopService
block,
activeTools,
context,
ct));
ct),
onStreamEventAsync: async evt =>
{
switch (evt.Kind)
{
case LlmService.ToolStreamEventKind.TextDelta:
if (!string.IsNullOrWhiteSpace(evt.Text))
{
streamedTextPreview.Append(evt.Text);
var now = DateTime.UtcNow;
if ((now - lastStreamUiUpdateAt).TotalMilliseconds >= 450 && streamedTextPreview.Length > 0)
{
var preview = streamedTextPreview.ToString();
preview = preview.Length > 140 ? preview[..140] + "…" : preview;
EmitEvent(AgentEventType.Thinking, "", preview);
lastStreamUiUpdateAt = now;
}
}
break;
case LlmService.ToolStreamEventKind.ToolCallReady:
if (evt.ToolCall != null)
{
EmitEvent(
AgentEventType.Thinking,
evt.ToolCall.ToolName,
$"스트리밍 도구 감지: {FormatToolCallSummary(evt.ToolCall)}");
}
break;
case LlmService.ToolStreamEventKind.Completed:
await Task.CompletedTask;
break;
}
});
runState.ContextRecoveryAttempts = 0;
llmCallSw.Stop();
runState.TransientLlmErrorRetries = 0;