feat(ai): upgrade agent loop tool calling to true SSE streaming
Move tool calling from non-streaming (complete) API calls to real
streaming SSE, so text, reasoning, and tool call arguments all arrive
incrementally — users see the AI think and act in real time.
- Extract ~370 lines of non-streaming tool-call functions from
agent_loop.rs (call_with_tools, call_openai_with_tools,
call_claude_with_tools, call_gemini_with_tools) and replace with
a single ai::stream_with_tools() dispatch call.
- Add streaming-with-tools infrastructure to ai.rs (~550 lines):
- StreamToolEvent enum: Chunk, ToolCallStart, ToolCallDelta,
ToolCallComplete — provider-agnostic, feeds into the
StreamingToolCallAccumulator
- stream_claude_with_tools(): parse SSE content_block_start/delta/
stop events, incremental input_json_delta for tool arguments
- stream_openai_with_tools(): parse SSE delta.tool_calls with
incremental function.arguments
- stream_gemini_with_tools(): parse SSE functionCall (Gemini
sends complete objects, not deltas — emit as one chunk)
- Cancellation support via tokio::select! in every streaming loop
- Extended timeout for reasoning models (600s vs 120s)
- StreamingToolCallAccumulator: collect partial tool call fragments
from streaming deltas, then deserialize into complete ToolCall
objects in index order once the stream ends.
This commit is contained in:
1 parent
9939e3191f
commit
2d75667b9a
5 files changed
+565
-339
No files matched your search
@@ -253,8 +253,10 @@ function agentStepClass(tone: AiAgentStepTone): string {
|
||||
}
|
||||
|
||||
function agentStepTitle(step: AiAgentStepItem): string {
|
||||
if (!step.titleKey) return t(step.labelKey);
|
||||
return t(step.titleKey, step.titleParams || {});
|
||||
if (step.titleKey) return t(step.titleKey, step.titleParams || {});
|
||||
const tool = step.titleParams?.tool;
|
||||
if (tool) return `${t(step.labelKey)}: ${tool}`;
|
||||
return t(step.labelKey);
|
||||
}
|
||||
|
||||
function toggleReasoning(index: number) {
|
||||
@@ -513,6 +515,22 @@ async function send() {
|
||||
if (event.type === "reasoning_delta" && event.delta) {
|
||||
appendAssistantReasoning(assistantIdx, event.delta);
|
||||
}
|
||||
// Real-time agent step rendering
|
||||
if (event.type === "tool_call_start" || event.type === "tool_call_end") {
|
||||
const msg = messages.value[assistantIdx];
|
||||
if (msg) {
|
||||
const steps = agentEvents
|
||||
.filter((e) => e.type === "tool_call_start" || e.type === "tool_call_end")
|
||||
.map((e) => ({
|
||||
key: `${e.tool_call_id || ""}-${e.type}`,
|
||||
labelKey: e.type === "tool_call_start" ? "ai.agentSteps.callingTool" : e.is_error ? "ai.agentSteps.toolError" : "ai.agentSteps.toolDone",
|
||||
tone: (e.type === "tool_call_start" ? "active" : e.is_error ? "danger" : "success") as AiAgentStepTone,
|
||||
titleKey: undefined,
|
||||
titleParams: { tool: e.tool_name || "" },
|
||||
}));
|
||||
msg.agentSteps = steps;
|
||||
}
|
||||
}
|
||||
scrollToBottom();
|
||||
},
|
||||
sessionId,
|
||||
|
||||
Reference in new issue
Block a user