fix(copilot): use AI SDK DataUIPart format for long-running event

Changed StreamLongRunningStart event type from "long-running-start" to
"data-long-running-start" to match the Vercel AI SDK's DataUIPart format.
This ensures the event is properly added to message.parts and can be
detected by the frontend.

Changes:
- Backend: Update event type to "data-long-running-start"
- Backend: Wrap toolCallId/toolName in a "data" object
- Frontend: Check for "data-long-running-start" type and access data.toolCallId

This follows the AI SDK protocol for custom data events.
This commit is contained in:
Zamil Majdy
2026-02-21 19:03:08 +07:00
parent 12d0a1f13b
commit 6a7cd84b26
4 changed files with 28 additions and 14 deletions

View File

@@ -34,8 +34,8 @@ class ResponseType(str, Enum):
TOOL_INPUT_AVAILABLE = "tool-input-available"
TOOL_OUTPUT_AVAILABLE = "tool-output-available"
# Long-running tool notification (custom extension)
LONG_RUNNING_START = "long-running-start"
# Long-running tool notification (custom extension - uses AI SDK DataUIPart format)
LONG_RUNNING_START = "data-long-running-start"
# Other
ERROR = "error"
@@ -179,15 +179,15 @@ class StreamToolOutputAvailable(StreamBaseResponse):
class StreamLongRunningStart(StreamBaseResponse):
"""Notification that a long-running tool has started.
Custom extension to the AI SDK protocol. Signals the frontend to show
Custom extension using AI SDK DataUIPart format. Signals the frontend to show
UI feedback while the tool executes.
"""
type: ResponseType = ResponseType.LONG_RUNNING_START
toolCallId: str = Field(
..., description="Tool call ID for the long-running operation"
data: dict[str, Any] = Field(
default_factory=dict,
description="Data for the long-running event containing toolCallId and toolName",
)
toolName: str = Field(..., description="Name of the long-running tool")
# ========== Other ==========

View File

@@ -736,10 +736,19 @@ async def stream_chat_completion_sdk(
# Emit long-running notification for tools with is_long_running=True
if isinstance(response, StreamToolInputAvailable):
tool = get_tool(response.toolName)
logger.info(
f"[SDK] Tool check: {response.toolName}, "
f"tool={tool}, is_long_running={tool.is_long_running if tool else 'N/A'}"
)
if tool and tool.is_long_running:
logger.info(
f"[SDK] Emitting StreamLongRunningStart for {response.toolName}"
)
yield StreamLongRunningStart(
toolCallId=response.toolCallId,
toolName=response.toolName,
data={
"toolCallId": response.toolCallId,
"toolName": response.toolName,
}
)
if isinstance(response, StreamTextDelta):

View File

@@ -1428,8 +1428,10 @@ async def _yield_tool_call(
tool = get_tool(tool_name)
if tool and tool.is_long_running:
yield StreamLongRunningStart(
toolCallId=tool_call_id,
toolName=tool_name,
data={
"toolCallId": tool_call_id,
"toolName": tool_name,
}
)
# Run tool execution synchronously with heartbeats

View File

@@ -15,13 +15,16 @@ export function ToolWrapper({ part, message, children }: Props) {
const isStreaming =
part.state === "input-streaming" || part.state === "input-available";
// Check if this tool has a long-running-start event in the message
// Check if this tool has a data-long-running-start event in the message
const isLongRunning = message.parts.some(
(p) =>
p.type === "long-running-start" &&
"toolCallId" in p &&
p.type === "data-long-running-start" &&
"data" in p &&
typeof p.data === "object" &&
p.data !== null &&
"toolCallId" in p.data &&
"toolCallId" in part &&
p.toolCallId === part.toolCallId,
p.data.toolCallId === part.toolCallId,
);
return (