fix(copilot): handle stream ending without text + PostToolUse logging

When the SDK CLI exits without sending a ResultMessage (parallel tool
execution), the frontend never gets StreamFinish and tools appear stuck.
Now detect StopAsyncIteration and emit StreamFinish as a fallback.

Also add INFO-level PostToolUse hook logging to trace whether the SDK
fires hooks and stashes output for built-in tools like WebSearch.
This commit is contained in:
Zamil Majdy
2026-02-20 13:12:05 +07:00
parent 8c2363ea88
commit d937c6839a
2 changed files with 38 additions and 2 deletions

View File

@@ -246,15 +246,33 @@ def create_security_hooks(
"""
_ = context
tool_name = cast(str, input_data.get("tool_name", ""))
logger.debug(f"[SDK] Tool success: {tool_name}, tool_use_id={tool_use_id}")
is_builtin = not tool_name.startswith(MCP_TOOL_PREFIX)
logger.info(
"[SDK] PostToolUse: %s (builtin=%s, tool_use_id=%s)",
tool_name,
is_builtin,
(tool_use_id or "")[:12],
)
# Stash output for SDK built-in tools so the response adapter can
# emit StreamToolOutputAvailable even when the CLI doesn't surface
# a separate UserMessage with ToolResultBlock content.
if not tool_name.startswith(MCP_TOOL_PREFIX):
if is_builtin:
tool_response = input_data.get("tool_response")
if tool_response is not None:
resp_preview = str(tool_response)[:100]
logger.info(
"[SDK] Stashing builtin output for %s (%d chars): %s...",
tool_name,
len(str(tool_response)),
resp_preview,
)
stash_pending_tool_output(tool_name, tool_response)
else:
logger.warning(
"[SDK] PostToolUse for builtin %s but tool_response is None",
tool_name,
)
return cast(SyncHookJSONOutput, {})

View File

@@ -24,6 +24,7 @@ from ..response_model import (
StreamBaseResponse,
StreamError,
StreamFinish,
StreamFinishStep,
StreamHeartbeat,
StreamStart,
StreamTextDelta,
@@ -885,6 +886,23 @@ async def stream_chat_completion_sdk(
)
yield response
# If the stream ended without a ResultMessage (no
# StreamFinish), the SDK CLI exited unexpectedly. Close
# the open step and emit StreamFinish so the frontend
# transitions to the "ready" state.
if not stream_completed:
logger.warning(
"[SDK] [%s] Stream ended without ResultMessage "
"(StopAsyncIteration) — emitting StreamFinish",
session_id[:12],
)
if adapter.step_open:
yield StreamFinishStep()
adapter.step_open = False
adapter._end_text_if_open([])
yield StreamFinish()
stream_completed = True
if (
assistant_response.content or assistant_response.tool_calls
) and not has_appended_assistant: