Skip to content
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f25c2d1
fix(copilot): remove is_long_running hack from agent generation tools
majdyz Feb 21, 2026
66c2416
refactor(copilot): remove async delegation dead code from agent gener…
majdyz Feb 21, 2026
eef3946
test(copilot): fix agent generator tests after removing operation_id/…
majdyz Feb 21, 2026
bfdc1ed
feat(copilot): implement is_long_running property for automatic mini-…
majdyz Feb 21, 2026
1de260c
feat(copilot): make mini-game truly automatic for all long-running tools
majdyz Feb 21, 2026
95afa8c
refactor(copilot): rename LongRunningToolWrapper to ToolWrapper
majdyz Feb 21, 2026
73b6ec3
fix(copilot): remove async delegation from executor, use is_long_runn…
majdyz Feb 21, 2026
c08ba6a
feat(copilot): add StreamLongRunningStart event for long-running tools
majdyz Feb 21, 2026
89785c8
feat(copilot): use stream event instead of hardcoded list for long-ru…
majdyz Feb 21, 2026
deb2bc4
chore: remove accidentally committed sample.logs
majdyz Feb 21, 2026
34b70d0
refactor: remove 'mini-game' from comments, use generic 'UI feedback'
majdyz Feb 21, 2026
35a7f98
fix(copilot): remove async delegation from SDK execution path
majdyz Feb 21, 2026
12d0a1f
fix(copilot): emit StreamLongRunningStart event in SDK path
majdyz Feb 21, 2026
6a7cd84
fix(copilot): use AI SDK DataUIPart format for long-running event
majdyz Feb 21, 2026
04ef290
fix(copilot): add isLongRunning flag directly to StreamToolInputAvail…
majdyz Feb 21, 2026
2447c30
fix(frontend): simplify import paths in LongRunningToolDisplay
majdyz Feb 21, 2026
b4c3bbe
fix(copilot): use providerMetadata for isLongRunning flag
majdyz Feb 21, 2026
2bc6481
fix(frontend): remove message prop from all ToolWrapper calls
majdyz Feb 21, 2026
eead01f
fix(copilot): prevent infinite refetch loop when backend errors
majdyz Feb 21, 2026
e2f32eb
fix(copilot): extract tool name from type field in ToolWrapper
majdyz Feb 21, 2026
f069aa3
fix(copilot): increase stream timeout from 12s to 60s
majdyz Feb 21, 2026
e489ba5
fix(copilot): disable input during submission and fix timeout logic
majdyz Feb 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,492 changes: 2,492 additions & 0 deletions autogpt_platform/backend/.application.logs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions autogpt_platform/backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ migrations/*/rollback*.sql

# Workspace files
workspaces/
sample.logs
21 changes: 21 additions & 0 deletions autogpt_platform/backend/backend/copilot/response_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class ResponseType(str, Enum):
TOOL_INPUT_AVAILABLE = "tool-input-available"
TOOL_OUTPUT_AVAILABLE = "tool-output-available"

# Long-running tool notification (custom extension - uses AI SDK DataUIPart format)
LONG_RUNNING_START = "data-long-running-start"

# Other
ERROR = "error"
USAGE = "usage"
Expand Down Expand Up @@ -145,6 +148,10 @@ class StreamToolInputAvailable(StreamBaseResponse):
input: dict[str, Any] = Field(
default_factory=dict, description="Tool input arguments"
)
providerMetadata: dict[str, Any] | None = Field(
default=None,
description="Provider metadata - used to pass isLongRunning flag to frontend",
)


class StreamToolOutputAvailable(StreamBaseResponse):
Expand Down Expand Up @@ -173,6 +180,20 @@ def to_sse(self) -> str:
return f"data: {json.dumps(data)}\n\n"


class StreamLongRunningStart(StreamBaseResponse):
"""Notification that a long-running tool has started.

Custom extension using AI SDK DataUIPart format. Signals the frontend to show
UI feedback while the tool executes.
"""

type: ResponseType = ResponseType.LONG_RUNNING_START
data: dict[str, Any] = Field(
default_factory=dict,
description="Data for the long-running event containing toolCallId and toolName",
)


# ========== Other ==========


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
StreamToolInputStart,
StreamToolOutputAvailable,
)
from backend.copilot.tools import get_tool

from .tool_adapter import MCP_TOOL_PREFIX, pop_pending_tool_output

Expand Down Expand Up @@ -111,6 +112,15 @@ def convert_message(self, sdk_message: Message) -> list[StreamBaseResponse]:
# instead of "mcp__copilot__find_block".
tool_name = block.name.removeprefix(MCP_TOOL_PREFIX)

# Check if this is a long-running tool to trigger UI feedback
tool = get_tool(tool_name)
is_long_running = tool.is_long_running if tool else False

logger.info(
f"[ADAPTER] Tool: {tool_name}, has_tool={tool is not None}, "
f"is_long_running={is_long_running}"
)

responses.append(
StreamToolInputStart(toolCallId=block.id, toolName=tool_name)
)
Expand All @@ -119,8 +129,15 @@ def convert_message(self, sdk_message: Message) -> list[StreamBaseResponse]:
toolCallId=block.id,
toolName=tool_name,
input=block.input,
providerMetadata=(
{"isLongRunning": True} if is_long_running else None
),
)
)
logger.info(
f"[ADAPTER] Created StreamToolInputAvailable with "
f"providerMetadata={{'isLongRunning': {is_long_running}}}"
)
self.current_tool_calls[block.id] = {"name": tool_name}

elif isinstance(sdk_message, UserMessage):
Expand Down
132 changes: 5 additions & 127 deletions autogpt_platform/backend/backend/copilot/sdk/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from backend.util.exceptions import NotFoundError

from .. import stream_registry
from ..config import ChatConfig
from ..model import (
ChatMessage,
Expand All @@ -31,20 +30,14 @@
StreamToolInputAvailable,
StreamToolOutputAvailable,
)
from ..service import (
_build_system_prompt,
_execute_long_running_tool_with_streaming,
_generate_session_title,
)
from ..tools.models import OperationPendingResponse, OperationStartedResponse
from ..service import _build_system_prompt, _generate_session_title
from ..tools.sandbox import WORKSPACE_PREFIX, make_session_path
from ..tracking import track_user_message
from .response_adapter import SDKResponseAdapter
from .security_hooks import create_security_hooks
from .tool_adapter import (
COPILOT_TOOL_NAMES,
SDK_DISALLOWED_TOOLS,
LongRunningCallback,
create_copilot_mcp_server,
set_execution_context,
wait_for_stash,
Expand Down Expand Up @@ -123,131 +116,16 @@ def available(self) -> bool:
are available from previous turns

### Long-running tools
Long-running tools (create_agent, edit_agent, etc.) are handled
asynchronously. You will receive an immediate response; the actual result
is delivered to the user via a background stream.
Long-running tools (create_agent, edit_agent, etc.) run synchronously
with heartbeats to keep the connection alive. The frontend shows UI feedback
during execution based on stream events.

### Sub-agent tasks
- When using the Task tool, NEVER set `run_in_background` to true.
All tasks must run in the foreground.
"""


def _build_long_running_callback(user_id: str | None) -> LongRunningCallback:
"""Build a callback that delegates long-running tools to the non-SDK infrastructure.

Long-running tools (create_agent, edit_agent, etc.) are delegated to the
existing background infrastructure: stream_registry (Redis Streams),
database persistence, and SSE reconnection. This means results survive
page refreshes / pod restarts, and the frontend shows the proper loading
widget with progress updates.

The returned callback matches the ``LongRunningCallback`` signature:
``(tool_name, args, session) -> MCP response dict``.
"""

async def _callback(
tool_name: str, args: dict[str, Any], session: ChatSession
) -> dict[str, Any]:
operation_id = str(uuid.uuid4())
task_id = str(uuid.uuid4())
tool_call_id = f"sdk-{uuid.uuid4().hex[:12]}"
session_id = session.session_id

# --- Build user-friendly messages (matches non-SDK service) ---
if tool_name == "create_agent":
desc = args.get("description", "")
desc_preview = (desc[:100] + "...") if len(desc) > 100 else desc
pending_msg = (
f"Creating your agent: {desc_preview}"
if desc_preview
else "Creating agent... This may take a few minutes."
)
started_msg = (
"Agent creation started. You can close this tab - "
"check your library in a few minutes."
)
elif tool_name == "edit_agent":
changes = args.get("changes", "")
changes_preview = (changes[:100] + "...") if len(changes) > 100 else changes
pending_msg = (
f"Editing agent: {changes_preview}"
if changes_preview
else "Editing agent... This may take a few minutes."
)
started_msg = (
"Agent edit started. You can close this tab - "
"check your library in a few minutes."
)
else:
pending_msg = f"Running {tool_name}... This may take a few minutes."
started_msg = (
f"{tool_name} started. You can close this tab - "
"check back in a few minutes."
)

# --- Register task in Redis for SSE reconnection ---
await stream_registry.create_task(
task_id=task_id,
session_id=session_id,
user_id=user_id,
tool_call_id=tool_call_id,
tool_name=tool_name,
operation_id=operation_id,
)

# --- Save OperationPendingResponse to chat history ---
pending_message = ChatMessage(
role="tool",
content=OperationPendingResponse(
message=pending_msg,
operation_id=operation_id,
tool_name=tool_name,
).model_dump_json(),
tool_call_id=tool_call_id,
)
session.messages.append(pending_message)
await upsert_chat_session(session)

# --- Spawn background task (reuses non-SDK infrastructure) ---
bg_task = asyncio.create_task(
_execute_long_running_tool_with_streaming(
tool_name=tool_name,
parameters=args,
tool_call_id=tool_call_id,
operation_id=operation_id,
task_id=task_id,
session_id=session_id,
user_id=user_id,
)
)
_background_tasks.add(bg_task)
bg_task.add_done_callback(_background_tasks.discard)
await stream_registry.set_task_asyncio_task(task_id, bg_task)

logger.info(
f"[SDK] Long-running tool {tool_name} delegated to background "
f"(operation_id={operation_id}, task_id={task_id})"
)

# --- Return OperationStartedResponse as MCP tool result ---
# This flows through SDK → response adapter → frontend, triggering
# the loading widget with SSE reconnection support.
started_json = OperationStartedResponse(
message=started_msg,
operation_id=operation_id,
tool_name=tool_name,
task_id=task_id,
).model_dump_json()

return {
"content": [{"type": "text", "text": started_json}],
"isError": False,
}

return _callback


def _resolve_sdk_model() -> str | None:
"""Resolve the model name for the Claude Agent SDK CLI.

Expand Down Expand Up @@ -584,7 +462,7 @@ async def stream_chat_completion_sdk(
set_execution_context(
user_id,
session,
long_running_callback=_build_long_running_callback(user_id),
long_running_callback=None,
)
try:
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
Expand Down
Loading