fast-agent-mcp 0.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ACP Tool Progress Tracking
|
|
3
|
+
|
|
4
|
+
Provides integration between MCP tool execution and ACP tool call notifications.
|
|
5
|
+
When MCP tools execute and report progress, this module:
|
|
6
|
+
1. Sends initial tool_call notifications to the ACP client
|
|
7
|
+
2. Updates with progress via tool_call_update notifications
|
|
8
|
+
3. Handles status transitions (pending -> in_progress -> completed/failed)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from acp.contrib import ToolCallTracker
|
|
16
|
+
from acp.helpers import (
|
|
17
|
+
audio_block,
|
|
18
|
+
embedded_blob_resource,
|
|
19
|
+
embedded_text_resource,
|
|
20
|
+
image_block,
|
|
21
|
+
resource_block,
|
|
22
|
+
resource_link_block,
|
|
23
|
+
text_block,
|
|
24
|
+
tool_content,
|
|
25
|
+
)
|
|
26
|
+
from acp.schema import ToolKind
|
|
27
|
+
from mcp.types import (
|
|
28
|
+
AudioContent,
|
|
29
|
+
BlobResourceContents,
|
|
30
|
+
EmbeddedResource,
|
|
31
|
+
ImageContent,
|
|
32
|
+
ResourceLink,
|
|
33
|
+
TextContent,
|
|
34
|
+
TextResourceContents,
|
|
35
|
+
)
|
|
36
|
+
from mcp.types import (
|
|
37
|
+
ContentBlock as MCPContentBlock,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from fast_agent.core.logging.logger import get_logger
|
|
41
|
+
from fast_agent.mcp.common import get_resource_name, get_server_name, is_namespaced_name
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from acp import AgentSideConnection
|
|
45
|
+
|
|
46
|
+
logger = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ACPToolProgressManager:
|
|
50
|
+
"""
|
|
51
|
+
Manages tool call progress notifications for ACP clients.
|
|
52
|
+
|
|
53
|
+
Implements the ToolExecutionHandler protocol to provide lifecycle hooks
|
|
54
|
+
for tool execution. Sends sessionUpdate notifications to ACP clients as
|
|
55
|
+
tools execute and report progress.
|
|
56
|
+
|
|
57
|
+
Uses the SDK's ToolCallTracker for state management and notification generation.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, connection: "AgentSideConnection", session_id: str) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Initialize the progress manager.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
connection: The ACP connection to send notifications on
|
|
66
|
+
session_id: The ACP session ID for this manager
|
|
67
|
+
"""
|
|
68
|
+
self._connection = connection
|
|
69
|
+
self._session_id = session_id
|
|
70
|
+
# Use SDK's ToolCallTracker for state management
|
|
71
|
+
self._tracker = ToolCallTracker()
|
|
72
|
+
# Map ACP tool_call_id → external_id for reverse lookups
|
|
73
|
+
self._tool_call_id_to_external_id: dict[str, str] = {}
|
|
74
|
+
# Map tool_call_id → simple title (server/tool) for progress updates
|
|
75
|
+
self._simple_titles: dict[str, str] = {}
|
|
76
|
+
# Map tool_call_id → full title (with args) for completion
|
|
77
|
+
self._full_titles: dict[str, str] = {}
|
|
78
|
+
# Track tool_use_id from stream events to avoid duplicate notifications
|
|
79
|
+
self._stream_tool_use_ids: dict[str, str] = {} # tool_use_id → external_id
|
|
80
|
+
# Track pending stream notification tasks
|
|
81
|
+
self._stream_tasks: dict[str, asyncio.Task] = {} # tool_use_id → task
|
|
82
|
+
# Track stream chunk counts for title updates
|
|
83
|
+
self._stream_chunk_counts: dict[str, int] = {} # tool_use_id → chunk count
|
|
84
|
+
# Track base titles for streaming tools (before chunk count suffix)
|
|
85
|
+
self._stream_base_titles: dict[str, str] = {} # tool_use_id → base title
|
|
86
|
+
self._lock = asyncio.Lock()
|
|
87
|
+
|
|
88
|
+
async def get_tool_call_id_for_tool_use(self, tool_use_id: str) -> str | None:
|
|
89
|
+
"""
|
|
90
|
+
Get the ACP toolCallId for a given LLM tool_use_id.
|
|
91
|
+
|
|
92
|
+
This is used by the permission handler to ensure the permission request
|
|
93
|
+
references the same toolCallId as any existing streaming notification.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
tool_use_id: The LLM's tool use ID
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The ACP toolCallId if a streaming notification was already sent, None otherwise
|
|
100
|
+
"""
|
|
101
|
+
# Check if there's a pending stream notification task for this tool_use_id
|
|
102
|
+
# If so, wait for it to complete so the toolCallId is available
|
|
103
|
+
task = self._stream_tasks.get(tool_use_id)
|
|
104
|
+
if task and not task.done():
|
|
105
|
+
try:
|
|
106
|
+
await task
|
|
107
|
+
except Exception:
|
|
108
|
+
pass # Ignore errors, just ensure task completed
|
|
109
|
+
|
|
110
|
+
# Now look up the toolCallId
|
|
111
|
+
external_id = self._stream_tool_use_ids.get(tool_use_id)
|
|
112
|
+
if external_id:
|
|
113
|
+
# Look up the toolCallId from the tracker
|
|
114
|
+
async with self._lock:
|
|
115
|
+
try:
|
|
116
|
+
model = self._tracker.tool_call_model(external_id)
|
|
117
|
+
if model and hasattr(model, "toolCallId"):
|
|
118
|
+
return model.toolCallId
|
|
119
|
+
except Exception:
|
|
120
|
+
# Swallow and fall back to local mapping
|
|
121
|
+
pass
|
|
122
|
+
# Fallback: check our own mapping
|
|
123
|
+
for tool_call_id, ext_id in self._tool_call_id_to_external_id.items():
|
|
124
|
+
if ext_id == external_id:
|
|
125
|
+
return tool_call_id
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def handle_tool_stream_event(self, event_type: str, info: dict[str, Any] | None = None) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Handle tool stream events from the LLM during streaming.
|
|
131
|
+
|
|
132
|
+
This gets called when the LLM streams tool use blocks, BEFORE tool execution.
|
|
133
|
+
Sends early ACP notifications so clients see tool calls immediately.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
event_type: Type of stream event ("start", "delta", "text", "stop")
|
|
137
|
+
info: Event payload containing tool_name, tool_use_id, etc.
|
|
138
|
+
"""
|
|
139
|
+
if event_type == "start" and info:
|
|
140
|
+
tool_name = info.get("tool_name")
|
|
141
|
+
tool_use_id = info.get("tool_use_id")
|
|
142
|
+
|
|
143
|
+
if tool_name and tool_use_id:
|
|
144
|
+
# Generate external_id SYNCHRONOUSLY to avoid race with delta events
|
|
145
|
+
external_id = str(uuid.uuid4())
|
|
146
|
+
self._stream_tool_use_ids[tool_use_id] = external_id
|
|
147
|
+
|
|
148
|
+
# Schedule async notification sending and store the task
|
|
149
|
+
task = asyncio.create_task(
|
|
150
|
+
self._send_stream_start_notification(tool_name, tool_use_id, external_id)
|
|
151
|
+
)
|
|
152
|
+
# Store task reference so we can await it in on_tool_start if needed
|
|
153
|
+
self._stream_tasks[tool_use_id] = task
|
|
154
|
+
|
|
155
|
+
elif event_type == "delta" and info:
|
|
156
|
+
tool_use_id = info.get("tool_use_id")
|
|
157
|
+
chunk = info.get("chunk")
|
|
158
|
+
|
|
159
|
+
if tool_use_id and chunk:
|
|
160
|
+
# Schedule async notification with accumulated arguments
|
|
161
|
+
asyncio.create_task(self._send_stream_delta_notification(tool_use_id, chunk))
|
|
162
|
+
|
|
163
|
+
async def _send_stream_start_notification(
|
|
164
|
+
self, tool_name: str, tool_use_id: str, external_id: str
|
|
165
|
+
) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Send early ACP notification when tool stream starts.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
tool_name: Name of the tool being called (may be namespaced like "server__tool")
|
|
171
|
+
tool_use_id: LLM's tool use ID
|
|
172
|
+
external_id: Pre-generated external ID for SDK tracker
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
# Parse the tool name if it's namespaced (e.g., "acp_filesystem__write_text_file")
|
|
176
|
+
if is_namespaced_name(tool_name):
|
|
177
|
+
server_name = get_server_name(tool_name)
|
|
178
|
+
base_tool_name = get_resource_name(tool_name)
|
|
179
|
+
else:
|
|
180
|
+
server_name = None
|
|
181
|
+
base_tool_name = tool_name
|
|
182
|
+
|
|
183
|
+
# Infer tool kind (without arguments yet)
|
|
184
|
+
kind = self._infer_tool_kind(base_tool_name, None)
|
|
185
|
+
|
|
186
|
+
# Create title with server name if available
|
|
187
|
+
if server_name:
|
|
188
|
+
title = f"{server_name}/{base_tool_name}"
|
|
189
|
+
else:
|
|
190
|
+
title = base_tool_name
|
|
191
|
+
|
|
192
|
+
# Use SDK tracker to create the tool call start notification
|
|
193
|
+
async with self._lock:
|
|
194
|
+
tool_call_start = self._tracker.start(
|
|
195
|
+
external_id=external_id,
|
|
196
|
+
title=title,
|
|
197
|
+
kind=kind,
|
|
198
|
+
status="pending",
|
|
199
|
+
raw_input=None, # Don't have args yet
|
|
200
|
+
)
|
|
201
|
+
# Store mapping from ACP tool_call_id to external_id
|
|
202
|
+
self._tool_call_id_to_external_id[tool_call_start.toolCallId] = external_id
|
|
203
|
+
# Initialize streaming state for this tool
|
|
204
|
+
self._stream_base_titles[tool_use_id] = title
|
|
205
|
+
self._stream_chunk_counts[tool_use_id] = 0
|
|
206
|
+
|
|
207
|
+
# Send initial notification
|
|
208
|
+
await self._connection.session_update(
|
|
209
|
+
session_id=self._session_id, update=tool_call_start
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
logger.debug(
|
|
213
|
+
f"Sent early stream tool call notification: {tool_call_start.toolCallId}",
|
|
214
|
+
name="acp_tool_stream_start",
|
|
215
|
+
tool_call_id=tool_call_start.toolCallId,
|
|
216
|
+
external_id=external_id,
|
|
217
|
+
base_tool_name=base_tool_name,
|
|
218
|
+
server_name=server_name,
|
|
219
|
+
tool_use_id=tool_use_id,
|
|
220
|
+
)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(
|
|
223
|
+
f"Error sending stream tool_call notification: {e}",
|
|
224
|
+
name="acp_tool_stream_error",
|
|
225
|
+
exc_info=True,
|
|
226
|
+
)
|
|
227
|
+
finally:
|
|
228
|
+
# Clean up task reference
|
|
229
|
+
if tool_use_id in self._stream_tasks:
|
|
230
|
+
del self._stream_tasks[tool_use_id]
|
|
231
|
+
|
|
232
|
+
async def _send_stream_delta_notification(self, tool_use_id: str, chunk: str) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Send ACP notification with tool argument chunk as it streams.
|
|
235
|
+
|
|
236
|
+
Accumulates chunks into content and updates title with chunk count.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
tool_use_id: LLM's tool use ID
|
|
240
|
+
chunk: JSON fragment chunk
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
async with self._lock:
|
|
244
|
+
external_id = self._stream_tool_use_ids.get(tool_use_id)
|
|
245
|
+
if not external_id:
|
|
246
|
+
# No start notification sent yet, skip this chunk
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Increment chunk count and build title with count
|
|
250
|
+
self._stream_chunk_counts[tool_use_id] = (
|
|
251
|
+
self._stream_chunk_counts.get(tool_use_id, 0) + 1
|
|
252
|
+
)
|
|
253
|
+
chunk_count = self._stream_chunk_counts[tool_use_id]
|
|
254
|
+
base_title = self._stream_base_titles.get(tool_use_id, "Tool")
|
|
255
|
+
title_with_count = f"{base_title} (streaming: {chunk_count} chunks)"
|
|
256
|
+
|
|
257
|
+
# Use SDK's append_stream_text to accumulate chunks into content
|
|
258
|
+
update = self._tracker.append_stream_text(
|
|
259
|
+
external_id=external_id,
|
|
260
|
+
text=chunk,
|
|
261
|
+
title=title_with_count,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Only send notifications after 25 chunks to avoid UI noise for small calls
|
|
265
|
+
if chunk_count < 25:
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
# Send notification outside the lock
|
|
269
|
+
await self._connection.session_update(session_id=self._session_id, update=update)
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.debug(
|
|
273
|
+
f"Error sending stream delta notification: {e}",
|
|
274
|
+
name="acp_tool_stream_delta_error",
|
|
275
|
+
tool_use_id=tool_use_id,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Tool kind patterns: mapping from ToolKind to keyword patterns
|
|
279
|
+
_TOOL_KIND_PATTERNS: dict[ToolKind, tuple[str, ...]] = {
|
|
280
|
+
"read": ("read", "get", "fetch", "list", "show"),
|
|
281
|
+
"edit": ("write", "edit", "update", "modify", "patch"),
|
|
282
|
+
"delete": ("delete", "remove", "clear", "clean", "rm"),
|
|
283
|
+
"move": ("move", "rename", "mv"),
|
|
284
|
+
"search": ("search", "find", "query", "grep"),
|
|
285
|
+
"execute": ("execute", "run", "exec", "command", "bash", "shell"),
|
|
286
|
+
"think": ("think", "plan", "reason"),
|
|
287
|
+
"fetch": ("fetch", "download", "http", "request"),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
def _infer_tool_kind(self, tool_name: str, arguments: dict[str, Any] | None) -> ToolKind:
|
|
291
|
+
"""
|
|
292
|
+
Infer the tool kind from the tool name and arguments.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
tool_name: Name of the tool being called
|
|
296
|
+
arguments: Tool arguments (reserved for future use)
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
The inferred ToolKind
|
|
300
|
+
"""
|
|
301
|
+
name_lower = tool_name.lower()
|
|
302
|
+
|
|
303
|
+
for kind, patterns in self._TOOL_KIND_PATTERNS.items():
|
|
304
|
+
if any(pattern in name_lower for pattern in patterns):
|
|
305
|
+
return kind
|
|
306
|
+
|
|
307
|
+
return "other"
|
|
308
|
+
|
|
309
|
+
def _convert_mcp_content_to_acp(self, content: list[MCPContentBlock] | None) -> list | None:
|
|
310
|
+
"""
|
|
311
|
+
Convert MCP content blocks to ACP tool call content using SDK helpers.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
content: List of MCP content blocks (TextContent, ImageContent, etc.)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
List of ContentToolCallContent blocks, or None if no content
|
|
318
|
+
"""
|
|
319
|
+
if not content:
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
acp_content = []
|
|
323
|
+
|
|
324
|
+
for block in content:
|
|
325
|
+
try:
|
|
326
|
+
match block:
|
|
327
|
+
case TextContent():
|
|
328
|
+
acp_content.append(tool_content(text_block(block.text)))
|
|
329
|
+
|
|
330
|
+
case ImageContent():
|
|
331
|
+
acp_content.append(tool_content(image_block(block.data, block.mimeType)))
|
|
332
|
+
|
|
333
|
+
case AudioContent():
|
|
334
|
+
acp_content.append(tool_content(audio_block(block.data, block.mimeType)))
|
|
335
|
+
|
|
336
|
+
case ResourceLink():
|
|
337
|
+
# Use URI as the name for resource links
|
|
338
|
+
acp_content.append(
|
|
339
|
+
tool_content(
|
|
340
|
+
resource_link_block(
|
|
341
|
+
name=str(block.uri),
|
|
342
|
+
uri=str(block.uri),
|
|
343
|
+
mime_type=getattr(block, "mimeType", None),
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
case EmbeddedResource():
|
|
349
|
+
# Use SDK's resource_block helper with embedded resource contents
|
|
350
|
+
match block.resource:
|
|
351
|
+
case TextResourceContents():
|
|
352
|
+
embedded_res = embedded_text_resource(
|
|
353
|
+
uri=str(block.resource.uri),
|
|
354
|
+
text=block.resource.text,
|
|
355
|
+
mime_type=block.resource.mimeType,
|
|
356
|
+
)
|
|
357
|
+
case BlobResourceContents():
|
|
358
|
+
embedded_res = embedded_blob_resource(
|
|
359
|
+
uri=str(block.resource.uri),
|
|
360
|
+
blob=block.resource.blob,
|
|
361
|
+
mime_type=block.resource.mimeType,
|
|
362
|
+
)
|
|
363
|
+
case _:
|
|
364
|
+
continue # Skip unsupported resource types
|
|
365
|
+
acp_content.append(tool_content(resource_block(embedded_res)))
|
|
366
|
+
|
|
367
|
+
case _:
|
|
368
|
+
logger.warning(
|
|
369
|
+
f"Unknown content type: {type(block).__name__}",
|
|
370
|
+
name="acp_unknown_content_type",
|
|
371
|
+
)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
logger.error(
|
|
374
|
+
f"Error converting content block {type(block).__name__}: {e}",
|
|
375
|
+
name="acp_content_conversion_error",
|
|
376
|
+
exc_info=True,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return acp_content if acp_content else None
|
|
380
|
+
|
|
381
|
+
async def on_tool_start(
|
|
382
|
+
self,
|
|
383
|
+
tool_name: str,
|
|
384
|
+
server_name: str,
|
|
385
|
+
arguments: dict[str, Any] | None = None,
|
|
386
|
+
tool_use_id: str | None = None,
|
|
387
|
+
) -> str:
|
|
388
|
+
"""
|
|
389
|
+
Called when a tool execution starts.
|
|
390
|
+
|
|
391
|
+
Implements ToolExecutionHandler.on_tool_start protocol method.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
tool_name: Name of the tool being called
|
|
395
|
+
server_name: Name of the MCP server providing the tool
|
|
396
|
+
arguments: Tool arguments
|
|
397
|
+
tool_use_id: LLM's tool use ID (for matching with stream events)
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
The tool call ID for tracking
|
|
401
|
+
"""
|
|
402
|
+
# Check if we already sent a stream notification for this tool_use_id
|
|
403
|
+
existing_external_id = None
|
|
404
|
+
if tool_use_id:
|
|
405
|
+
# If there's a pending stream task, await it first
|
|
406
|
+
pending_task = self._stream_tasks.get(tool_use_id)
|
|
407
|
+
if pending_task and not pending_task.done():
|
|
408
|
+
logger.debug(
|
|
409
|
+
f"Waiting for pending stream notification task to complete: {tool_use_id}",
|
|
410
|
+
name="acp_tool_await_stream_task",
|
|
411
|
+
tool_use_id=tool_use_id,
|
|
412
|
+
)
|
|
413
|
+
try:
|
|
414
|
+
await pending_task
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.warning(
|
|
417
|
+
f"Stream notification task failed: {e}",
|
|
418
|
+
name="acp_stream_task_failed",
|
|
419
|
+
tool_use_id=tool_use_id,
|
|
420
|
+
exc_info=True,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
async with self._lock:
|
|
424
|
+
existing_external_id = self._stream_tool_use_ids.get(tool_use_id)
|
|
425
|
+
if existing_external_id:
|
|
426
|
+
logger.debug(
|
|
427
|
+
f"Found existing stream notification for tool_use_id: {tool_use_id}",
|
|
428
|
+
name="acp_tool_execution_match",
|
|
429
|
+
tool_use_id=tool_use_id,
|
|
430
|
+
external_id=existing_external_id,
|
|
431
|
+
)
|
|
432
|
+
else:
|
|
433
|
+
logger.debug(
|
|
434
|
+
f"No stream notification found for tool_use_id: {tool_use_id}",
|
|
435
|
+
name="acp_tool_execution_no_match",
|
|
436
|
+
tool_use_id=tool_use_id,
|
|
437
|
+
available_ids=list(self._stream_tool_use_ids.keys()),
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Infer tool kind
|
|
441
|
+
kind = self._infer_tool_kind(tool_name, arguments)
|
|
442
|
+
|
|
443
|
+
# Create title
|
|
444
|
+
title = f"{server_name}/{tool_name}"
|
|
445
|
+
if arguments:
|
|
446
|
+
# Include key argument info in title
|
|
447
|
+
arg_str = ", ".join(f"{k}={v}" for k, v in list(arguments.items())[:2])
|
|
448
|
+
if len(arg_str) > 50:
|
|
449
|
+
arg_str = arg_str[:47] + "..."
|
|
450
|
+
title = f"{title}({arg_str})"
|
|
451
|
+
|
|
452
|
+
# Use SDK tracker to create or update the tool call notification
|
|
453
|
+
async with self._lock:
|
|
454
|
+
if existing_external_id:
|
|
455
|
+
# Get final chunk count before clearing
|
|
456
|
+
final_chunk_count = self._stream_chunk_counts.get(tool_use_id or "", 0)
|
|
457
|
+
|
|
458
|
+
# Update title with streamed count only if we showed streaming progress
|
|
459
|
+
if final_chunk_count >= 25:
|
|
460
|
+
title = f"{title} (streamed {final_chunk_count} chunks)"
|
|
461
|
+
|
|
462
|
+
# Update the existing stream notification with full details
|
|
463
|
+
# Clear streaming content by setting content=[] since we now have full rawInput
|
|
464
|
+
tool_call_update = self._tracker.progress(
|
|
465
|
+
external_id=existing_external_id,
|
|
466
|
+
title=title, # Update with server_name and args
|
|
467
|
+
kind=kind, # Re-infer with arguments
|
|
468
|
+
status="in_progress", # Move from pending to in_progress
|
|
469
|
+
raw_input=arguments, # Add complete arguments
|
|
470
|
+
content=[], # Clear streaming content
|
|
471
|
+
)
|
|
472
|
+
tool_call_id = tool_call_update.toolCallId
|
|
473
|
+
|
|
474
|
+
# Ensure mapping exists - progress() may return different ID than start()
|
|
475
|
+
# or the stream notification task may not have stored it yet
|
|
476
|
+
self._tool_call_id_to_external_id[tool_call_id] = existing_external_id
|
|
477
|
+
# Store simple title (server/tool) for progress updates - no args
|
|
478
|
+
self._simple_titles[tool_call_id] = f"{server_name}/{tool_name}"
|
|
479
|
+
# Store full title (with args) for completion
|
|
480
|
+
self._full_titles[tool_call_id] = title
|
|
481
|
+
|
|
482
|
+
# Clean up streaming state since we're now in execution
|
|
483
|
+
if tool_use_id:
|
|
484
|
+
self._stream_chunk_counts.pop(tool_use_id, None)
|
|
485
|
+
self._stream_base_titles.pop(tool_use_id, None)
|
|
486
|
+
self._stream_tool_use_ids.pop(tool_use_id, None)
|
|
487
|
+
|
|
488
|
+
logger.debug(
|
|
489
|
+
f"Updated stream tool call with execution details: {tool_call_id}",
|
|
490
|
+
name="acp_tool_execution_update",
|
|
491
|
+
tool_call_id=tool_call_id,
|
|
492
|
+
external_id=existing_external_id,
|
|
493
|
+
tool_name=tool_name,
|
|
494
|
+
server_name=server_name,
|
|
495
|
+
tool_use_id=tool_use_id,
|
|
496
|
+
)
|
|
497
|
+
else:
|
|
498
|
+
# No stream notification - create new one (normal path)
|
|
499
|
+
external_id = str(uuid.uuid4())
|
|
500
|
+
tool_call_start = self._tracker.start(
|
|
501
|
+
external_id=external_id,
|
|
502
|
+
title=title,
|
|
503
|
+
kind=kind,
|
|
504
|
+
status="pending",
|
|
505
|
+
raw_input=arguments,
|
|
506
|
+
)
|
|
507
|
+
# Store mapping from ACP tool_call_id to external_id for later lookups
|
|
508
|
+
self._tool_call_id_to_external_id[tool_call_start.toolCallId] = external_id
|
|
509
|
+
tool_call_id = tool_call_start.toolCallId
|
|
510
|
+
tool_call_update = tool_call_start
|
|
511
|
+
# Store simple title (server/tool) for progress updates - no args
|
|
512
|
+
self._simple_titles[tool_call_id] = f"{server_name}/{tool_name}"
|
|
513
|
+
# Store full title (with args) for completion
|
|
514
|
+
self._full_titles[tool_call_id] = title
|
|
515
|
+
|
|
516
|
+
logger.debug(
|
|
517
|
+
f"Started tool call tracking: {tool_call_id}",
|
|
518
|
+
name="acp_tool_call_start",
|
|
519
|
+
tool_call_id=tool_call_id,
|
|
520
|
+
external_id=external_id,
|
|
521
|
+
tool_name=tool_name,
|
|
522
|
+
server_name=server_name,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Send notification (either new start or update)
|
|
526
|
+
try:
|
|
527
|
+
await self._connection.session_update(
|
|
528
|
+
session_id=self._session_id, update=tool_call_update
|
|
529
|
+
)
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logger.error(
|
|
532
|
+
f"Error sending tool_call notification: {e}",
|
|
533
|
+
name="acp_tool_call_error",
|
|
534
|
+
exc_info=True,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Return the ACP tool_call_id for caller to track
|
|
538
|
+
return tool_call_id
|
|
539
|
+
|
|
540
|
+
async def on_tool_permission_denied(
|
|
541
|
+
self,
|
|
542
|
+
tool_name: str,
|
|
543
|
+
server_name: str,
|
|
544
|
+
tool_use_id: str | None,
|
|
545
|
+
error: str | None = None,
|
|
546
|
+
) -> None:
|
|
547
|
+
"""
|
|
548
|
+
Called when tool execution is denied before it starts.
|
|
549
|
+
|
|
550
|
+
Uses any pending stream-start notification to mark the call as failed
|
|
551
|
+
so ACP clients see the cancellation/denial.
|
|
552
|
+
"""
|
|
553
|
+
if not tool_use_id:
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
# Wait for any pending stream notification to finish
|
|
557
|
+
pending_task = self._stream_tasks.get(tool_use_id)
|
|
558
|
+
if pending_task and not pending_task.done():
|
|
559
|
+
try:
|
|
560
|
+
await pending_task
|
|
561
|
+
except Exception as e: # noqa: BLE001
|
|
562
|
+
logger.warning(
|
|
563
|
+
f"Stream notification task failed for denied tool: {e}",
|
|
564
|
+
name="acp_permission_denied_stream_task_failed",
|
|
565
|
+
tool_use_id=tool_use_id,
|
|
566
|
+
exc_info=True,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
async with self._lock:
|
|
570
|
+
external_id = self._stream_tool_use_ids.get(tool_use_id)
|
|
571
|
+
|
|
572
|
+
if not external_id:
|
|
573
|
+
# No stream notification; nothing to update
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
update_data = self._tracker.progress(
|
|
578
|
+
external_id=external_id,
|
|
579
|
+
status="failed",
|
|
580
|
+
content=[tool_content(text_block(error))] if error else None,
|
|
581
|
+
)
|
|
582
|
+
except Exception as e: # noqa: BLE001
|
|
583
|
+
logger.error(
|
|
584
|
+
f"Error creating permission-denied update: {e}",
|
|
585
|
+
name="acp_permission_denied_update_error",
|
|
586
|
+
exc_info=True,
|
|
587
|
+
)
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
# Send the failure notification
|
|
591
|
+
try:
|
|
592
|
+
await self._connection.session_update(session_id=self._session_id, update=update_data)
|
|
593
|
+
except Exception as e: # noqa: BLE001
|
|
594
|
+
logger.error(
|
|
595
|
+
f"Error sending permission-denied notification: {e}",
|
|
596
|
+
name="acp_permission_denied_notification_error",
|
|
597
|
+
exc_info=True,
|
|
598
|
+
)
|
|
599
|
+
finally:
|
|
600
|
+
# Clean up tracker and mappings
|
|
601
|
+
async with self._lock:
|
|
602
|
+
self._tracker.forget(external_id)
|
|
603
|
+
self._stream_tool_use_ids.pop(tool_use_id, None)
|
|
604
|
+
self._stream_chunk_counts.pop(tool_use_id, None)
|
|
605
|
+
self._stream_base_titles.pop(tool_use_id, None)
|
|
606
|
+
|
|
607
|
+
async def on_tool_progress(
|
|
608
|
+
self,
|
|
609
|
+
tool_call_id: str,
|
|
610
|
+
progress: float,
|
|
611
|
+
total: float | None = None,
|
|
612
|
+
message: str | None = None,
|
|
613
|
+
) -> None:
|
|
614
|
+
"""
|
|
615
|
+
Called when tool execution reports progress.
|
|
616
|
+
|
|
617
|
+
Implements ToolExecutionHandler.on_tool_progress protocol method.
|
|
618
|
+
Updates the title with progress percentage and/or message.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
tool_call_id: The tool call ID
|
|
622
|
+
progress: Current progress value
|
|
623
|
+
total: Total value for progress calculation (optional)
|
|
624
|
+
message: Optional progress message
|
|
625
|
+
"""
|
|
626
|
+
# Look up external_id from tool_call_id
|
|
627
|
+
async with self._lock:
|
|
628
|
+
external_id = self._tool_call_id_to_external_id.get(tool_call_id)
|
|
629
|
+
if not external_id:
|
|
630
|
+
logger.warning(
|
|
631
|
+
f"Tool call {tool_call_id} not found for progress update",
|
|
632
|
+
name="acp_tool_progress_not_found",
|
|
633
|
+
)
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
# Build updated title with progress info (using simple title without args)
|
|
637
|
+
simple_title = self._simple_titles.get(tool_call_id, "Tool")
|
|
638
|
+
title_parts = [simple_title]
|
|
639
|
+
|
|
640
|
+
# Add progress indicator
|
|
641
|
+
if total is not None and total > 0:
|
|
642
|
+
# Show progress/total format (e.g., [50/100])
|
|
643
|
+
title_parts.append(f"[{progress:.0f}/{total:.0f}]")
|
|
644
|
+
else:
|
|
645
|
+
# Show just progress value (e.g., [50])
|
|
646
|
+
title_parts.append(f"[{progress:.0f}]")
|
|
647
|
+
|
|
648
|
+
# Add message if present
|
|
649
|
+
if message:
|
|
650
|
+
title_parts.append(f"- {message}")
|
|
651
|
+
|
|
652
|
+
updated_title = " ".join(title_parts)
|
|
653
|
+
|
|
654
|
+
# Use SDK tracker to create progress update with updated title
|
|
655
|
+
# Note: We don't include content since the title now shows the progress message
|
|
656
|
+
try:
|
|
657
|
+
update_data = self._tracker.progress(
|
|
658
|
+
external_id=external_id,
|
|
659
|
+
status="in_progress",
|
|
660
|
+
title=updated_title,
|
|
661
|
+
)
|
|
662
|
+
except Exception as e:
|
|
663
|
+
logger.error(
|
|
664
|
+
f"Error creating progress update: {e}",
|
|
665
|
+
name="acp_progress_creation_error",
|
|
666
|
+
exc_info=True,
|
|
667
|
+
)
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
# Send progress update
|
|
671
|
+
try:
|
|
672
|
+
await self._connection.session_update(session_id=self._session_id, update=update_data)
|
|
673
|
+
|
|
674
|
+
logger.debug(
|
|
675
|
+
f"Updated tool call progress: {tool_call_id}",
|
|
676
|
+
name="acp_tool_progress_update",
|
|
677
|
+
progress=progress,
|
|
678
|
+
total=total,
|
|
679
|
+
progress_message=message,
|
|
680
|
+
title=updated_title,
|
|
681
|
+
)
|
|
682
|
+
except Exception as e:
|
|
683
|
+
logger.error(
|
|
684
|
+
f"Error sending tool_call_update notification: {e}",
|
|
685
|
+
name="acp_tool_progress_error",
|
|
686
|
+
exc_info=True,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
async def on_tool_complete(
|
|
690
|
+
self,
|
|
691
|
+
tool_call_id: str,
|
|
692
|
+
success: bool,
|
|
693
|
+
content: list[MCPContentBlock] | None = None,
|
|
694
|
+
error: str | None = None,
|
|
695
|
+
) -> None:
|
|
696
|
+
"""
|
|
697
|
+
Called when tool execution completes.
|
|
698
|
+
|
|
699
|
+
Implements ToolExecutionHandler.on_tool_complete protocol method.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
tool_call_id: The tool call ID
|
|
703
|
+
success: Whether the tool execution succeeded
|
|
704
|
+
content: Optional content blocks (text, images, etc.) if successful
|
|
705
|
+
error: Optional error message if failed
|
|
706
|
+
"""
|
|
707
|
+
# Look up external_id from tool_call_id
|
|
708
|
+
async with self._lock:
|
|
709
|
+
external_id = self._tool_call_id_to_external_id.get(tool_call_id)
|
|
710
|
+
if not external_id:
|
|
711
|
+
logger.warning(
|
|
712
|
+
f"Tool call {tool_call_id} not found for completion",
|
|
713
|
+
name="acp_tool_complete_not_found",
|
|
714
|
+
)
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
# Build content blocks
|
|
718
|
+
logger.debug(
|
|
719
|
+
f"on_tool_complete called: {tool_call_id}",
|
|
720
|
+
name="acp_tool_complete_entry",
|
|
721
|
+
success=success,
|
|
722
|
+
has_content=content is not None,
|
|
723
|
+
content_types=[type(c).__name__ for c in (content or [])],
|
|
724
|
+
has_error=error is not None,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
if error:
|
|
728
|
+
# Error case: convert error string to text content using SDK helper
|
|
729
|
+
content_blocks = [tool_content(text_block(error))]
|
|
730
|
+
raw_output = error
|
|
731
|
+
elif content:
|
|
732
|
+
# Success case with structured content: convert MCP content to ACP using SDK helpers
|
|
733
|
+
content_blocks = self._convert_mcp_content_to_acp(content)
|
|
734
|
+
# For rawOutput, extract just text content for backward compatibility
|
|
735
|
+
text_parts = [c.text for c in content if isinstance(c, TextContent)]
|
|
736
|
+
raw_output = "\n".join(text_parts) if text_parts else None
|
|
737
|
+
else:
|
|
738
|
+
# No content or error
|
|
739
|
+
content_blocks = None
|
|
740
|
+
raw_output = None
|
|
741
|
+
|
|
742
|
+
# Determine status
|
|
743
|
+
status = "completed" if success else "failed"
|
|
744
|
+
|
|
745
|
+
# Use SDK tracker to create completion update
|
|
746
|
+
try:
|
|
747
|
+
async with self._lock:
|
|
748
|
+
# Restore full title with parameters for completion
|
|
749
|
+
full_title = self._full_titles.get(tool_call_id)
|
|
750
|
+
update_data = self._tracker.progress(
|
|
751
|
+
external_id=external_id,
|
|
752
|
+
status=status,
|
|
753
|
+
title=full_title, # Restore original title with args
|
|
754
|
+
content=content_blocks,
|
|
755
|
+
raw_output=raw_output,
|
|
756
|
+
)
|
|
757
|
+
except Exception as e:
|
|
758
|
+
logger.error(
|
|
759
|
+
f"Error creating completion update: {e}",
|
|
760
|
+
name="acp_completion_creation_error",
|
|
761
|
+
exc_info=True,
|
|
762
|
+
)
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
# Send completion notification
|
|
766
|
+
try:
|
|
767
|
+
await self._connection.session_update(session_id=self._session_id, update=update_data)
|
|
768
|
+
|
|
769
|
+
logger.info(
|
|
770
|
+
f"Completed tool call: {tool_call_id}",
|
|
771
|
+
name="acp_tool_call_complete",
|
|
772
|
+
status=status,
|
|
773
|
+
content_blocks=len(content_blocks) if content_blocks else 0,
|
|
774
|
+
)
|
|
775
|
+
except Exception as e:
|
|
776
|
+
logger.error(
|
|
777
|
+
f"Error sending tool_call completion notification: {e}",
|
|
778
|
+
name="acp_tool_complete_error",
|
|
779
|
+
exc_info=True,
|
|
780
|
+
)
|
|
781
|
+
finally:
|
|
782
|
+
# Clean up tracker using SDK's forget method
|
|
783
|
+
async with self._lock:
|
|
784
|
+
self._tracker.forget(external_id)
|
|
785
|
+
self._tool_call_id_to_external_id.pop(tool_call_id, None)
|
|
786
|
+
self._simple_titles.pop(tool_call_id, None)
|
|
787
|
+
self._full_titles.pop(tool_call_id, None)
|
|
788
|
+
|
|
789
|
+
async def cleanup_session_tools(self, session_id: str) -> None:
|
|
790
|
+
"""
|
|
791
|
+
Clean up all tool trackers for a session.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
session_id: The session ID to clean up
|
|
795
|
+
"""
|
|
796
|
+
# The SDK tracker doesn't maintain session associations,
|
|
797
|
+
# so we just clear our mapping
|
|
798
|
+
async with self._lock:
|
|
799
|
+
count = len(self._tool_call_id_to_external_id)
|
|
800
|
+
# Forget all tracked tools
|
|
801
|
+
tracker_calls = getattr(self._tracker, "_calls", {})
|
|
802
|
+
for external_id in list(tracker_calls.keys()):
|
|
803
|
+
self._tracker.forget(external_id)
|
|
804
|
+
self._tool_call_id_to_external_id.clear()
|
|
805
|
+
self._simple_titles.clear()
|
|
806
|
+
self._full_titles.clear()
|
|
807
|
+
self._stream_tool_use_ids.clear()
|
|
808
|
+
self._stream_chunk_counts.clear()
|
|
809
|
+
self._stream_base_titles.clear()
|
|
810
|
+
|
|
811
|
+
logger.debug(
|
|
812
|
+
f"Cleaned up {count} tool trackers for session {session_id}",
|
|
813
|
+
name="acp_tool_cleanup",
|
|
814
|
+
)
|