diff --git a/frontend/app/app/api/chat/route.ts b/frontend/app/app/api/chat/route.ts index b4c1e8b..aa21d56 100644 --- a/frontend/app/app/api/chat/route.ts +++ b/frontend/app/app/api/chat/route.ts @@ -32,13 +32,16 @@ type ClawChatResponse = { type ClawRuntimeEvent = { type?: string; run_id?: string; + tool_call_index?: number | null; tool_name?: string; tool_call_id?: string; arguments?: unknown; + arguments_delta?: string; assistant_content?: string; metadata?: Record; ok?: boolean; delta?: string; + finish_reason?: string | null; elapsed_ms?: number; }; @@ -52,6 +55,7 @@ type StreamState = { textStreamed: boolean; phase: "waiting" | "queued" | "running" | "done"; runId?: string; + announcedToolCallKeys: Set; }; type ChatRequestBody = { @@ -107,6 +111,7 @@ export async function POST(req: Request) { textStarted: false, textStreamed: false, phase: "waiting", + announcedToolCallKeys: new Set(), }; writer.write({ type: "start" }); writer.write({ type: "start-step" }); @@ -491,6 +496,28 @@ function writeRuntimeEvent( delta: `\n${phase}${elapsed}。`, }); } + if (event.type === "content_delta" && event.delta) { + writer.write({ + type: "reasoning-delta", + id: "reasoning-1", + delta: event.delta, + }); + } + if (event.type === "tool_call_delta") { + const key = + event.tool_call_id ?? + (event.tool_call_index === null || event.tool_call_index === undefined + ? undefined + : `index:${event.tool_call_index}`); + if (key && !streamState?.announcedToolCallKeys.has(key)) { + streamState?.announcedToolCallKeys.add(key); + writer.write({ + type: "reasoning-delta", + id: "reasoning-1", + delta: `\n准备调用 ${event.tool_name ?? "工具"}。`, + }); + } + } if (event.type === "final_text_start") { if (!streamState?.textStarted) { writer.write({ type: "text-start", id: "text-1" }); diff --git a/src/agent_runtime.py b/src/agent_runtime.py index 1a12cc6..20ec22f 100644 --- a/src/agent_runtime.py +++ b/src/agent_runtime.py @@ -692,7 +692,11 @@ class LocalCodingAgent: self.last_run_result = result return result try: - turn, turn_events = self._query_model(session, tool_specs) + turn, turn_events = self._query_model( + session, + tool_specs, + stream_events=stream_events, + ) except OpenAICompatError as exc: if self._is_prompt_too_long_error(exc) and self._reactive_compact_session( session, @@ -700,7 +704,11 @@ class LocalCodingAgent: turn_index=turn_index, ): try: - turn, turn_events = self._query_model(session, tool_specs) + turn, turn_events = self._query_model( + session, + tool_specs, + stream_events=stream_events, + ) except OpenAICompatError as retry_exc: exc = retry_exc else: @@ -1479,6 +1487,8 @@ class LocalCodingAgent: self, session: AgentSessionState, tool_specs: list[dict[str, object]], + *, + stream_events: list[dict[str, object]] | None = None, ) -> tuple[AssistantTurn, tuple[StreamEvent, ...]]: if not self.runtime_config.stream_model_responses: turn = self.client.complete( @@ -1520,7 +1530,10 @@ class LocalCodingAgent: tool_specs, output_schema=self.runtime_config.output_schema, ): - events.append(event) + if stream_events is None: + events.append(event) + else: + stream_events.append(event.to_dict()) if event.type == 'content_delta': session.append_assistant_delta(assistant_index, event.delta) elif event.type == 'tool_call_delta':