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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,77 @@
1
+ from dataclasses import dataclass
2
+ from typing import List
3
+
4
+
5
+ @dataclass
6
+ class ReasoningSegment:
7
+ """Represents a slice of streamed text and whether it's a reasoning chunk."""
8
+
9
+ text: str
10
+ is_thinking: bool
11
+
12
+
13
+ class ReasoningStreamParser:
14
+ """Incrementally split streamed text into thought vs final answer segments."""
15
+
16
+ def __init__(self) -> None:
17
+ self._buffer = ""
18
+ self._in_think = False
19
+
20
+ @property
21
+ def in_think(self) -> bool:
22
+ """Whether the parser is currently inside a <think>...</think> block."""
23
+ return self._in_think
24
+
25
+ def feed(self, chunk: str) -> list[ReasoningSegment]:
26
+ """Consume a new chunk and return parsed segments."""
27
+ if not chunk:
28
+ return []
29
+
30
+ self._buffer += chunk
31
+ return self._extract_segments()
32
+
33
+ def flush(self) -> list[ReasoningSegment]:
34
+ """Return any remaining buffered text as a final segment."""
35
+ if not self._buffer:
36
+ return []
37
+ remaining = ReasoningSegment(text=self._buffer, is_thinking=self._in_think)
38
+ self._buffer = ""
39
+ return [remaining]
40
+
41
+ def _extract_segments(self) -> list[ReasoningSegment]:
42
+ segments: List[ReasoningSegment] = []
43
+
44
+ while self._buffer:
45
+ if self._in_think:
46
+ closing_index = self._buffer.find("</think>")
47
+ if closing_index == -1:
48
+ segments.append(ReasoningSegment(text=self._buffer, is_thinking=True))
49
+ self._buffer = ""
50
+ break
51
+
52
+ if closing_index > 0:
53
+ segments.append(
54
+ ReasoningSegment(text=self._buffer[:closing_index], is_thinking=True)
55
+ )
56
+
57
+ self._buffer = self._buffer[closing_index + len("</think>") :]
58
+ self._in_think = False
59
+ else:
60
+ opening_index = self._buffer.find("<think>")
61
+ if opening_index == -1:
62
+ segments.append(ReasoningSegment(text=self._buffer, is_thinking=False))
63
+ self._buffer = ""
64
+ break
65
+
66
+ if opening_index > 0:
67
+ segments.append(
68
+ ReasoningSegment(
69
+ text=self._buffer[:opening_index],
70
+ is_thinking=False,
71
+ )
72
+ )
73
+
74
+ self._buffer = self._buffer[opening_index + len("<think>") :]
75
+ self._in_think = True
76
+
77
+ return [segment for segment in segments if segment.text]
@@ -0,0 +1,22 @@
1
+ """Common time and duration helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def format_duration(seconds: float) -> str:
7
+ """Return a concise, human-friendly duration string."""
8
+ seconds = max(seconds, 0.0)
9
+ if seconds < 60:
10
+ return f"{seconds:.2f}s"
11
+
12
+ total_seconds = int(round(seconds))
13
+ minutes, sec = divmod(total_seconds, 60)
14
+ if minutes < 60:
15
+ return f"{minutes}m {sec:02d}s"
16
+
17
+ hours, minutes = divmod(minutes, 60)
18
+ if hours < 24:
19
+ return f"{hours}h {minutes:02d}m"
20
+
21
+ days, hours = divmod(hours, 24)
22
+ return f"{days}d {hours}h {minutes}m"
@@ -0,0 +1,261 @@
1
+ """
2
+ Workflow telemetry helpers for emitting virtual tool progress.
3
+
4
+ This module provides a pluggable abstraction that workflows (router, parallel)
5
+ can use to announce delegation steps without knowing which transport consumes
6
+ the events. Transports that care about surfacing these events (e.g. ACP) can
7
+ install a telemetry implementation that forwards them to tool progress
8
+ notifications, while the default implementation is a no-op.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ from contextlib import AbstractAsyncContextManager
15
+ from dataclasses import dataclass
16
+ from typing import TYPE_CHECKING, Any, Literal, Protocol
17
+
18
+ from mcp.types import ContentBlock, TextContent
19
+
20
+ if TYPE_CHECKING:
21
+ from fast_agent.mcp.tool_execution_handler import ToolExecutionHandler
22
+
23
+
24
+ # Plan entry types for ACP plan mode
25
+ PlanEntryStatus = Literal["pending", "in_progress", "completed"]
26
+ PlanEntryPriority = Literal["high", "medium", "low"]
27
+
28
+
29
+ @dataclass
30
+ class PlanEntry:
31
+ """A task within a plan."""
32
+
33
+ content: str
34
+ priority: PlanEntryPriority
35
+ status: PlanEntryStatus
36
+
37
+
38
+ class PlanTelemetryProvider(Protocol):
39
+ """Provider capable of sending plan updates."""
40
+
41
+ async def update_plan(self, entries: list[PlanEntry]) -> None:
42
+ """Send a plan update with the current list of plan entries."""
43
+ ...
44
+
45
+
46
+ class NoOpPlanTelemetryProvider:
47
+ """Provider that does nothing with plan updates."""
48
+
49
+ async def update_plan(self, entries: list[PlanEntry]) -> None:
50
+ pass
51
+
52
+
53
+ class WorkflowStepHandle(Protocol):
54
+ """Represents a virtual workflow step that can emit progress and completion."""
55
+
56
+ async def update(
57
+ self,
58
+ *,
59
+ message: str | None = None,
60
+ progress: float | None = None,
61
+ total: float | None = None,
62
+ ) -> None:
63
+ """Send an incremental update about this workflow step."""
64
+
65
+ async def finish(
66
+ self,
67
+ success: bool,
68
+ *,
69
+ text: str | None = None,
70
+ content: list[ContentBlock] | None = None,
71
+ error: str | None = None,
72
+ ) -> None:
73
+ """Complete the workflow step with optional success text/content."""
74
+
75
+
76
+ class WorkflowTelemetry(AbstractAsyncContextManager, WorkflowStepHandle):
77
+ """
78
+ Base async context manager returned by telemetry providers.
79
+
80
+ Implementations should override __aenter__/__aexit__ along with update/finish.
81
+ """
82
+
83
+ async def __aenter__(self) -> WorkflowStepHandle:
84
+ return self
85
+
86
+ async def __aexit__(self, exc_type, exc, tb) -> bool:
87
+ # Default no-op exit (override in subclasses to auto-complete)
88
+ return False
89
+
90
+ async def update( # pragma: no cover - default no-op
91
+ self,
92
+ *,
93
+ message: str | None = None,
94
+ progress: float | None = None,
95
+ total: float | None = None,
96
+ ) -> None:
97
+ return None
98
+
99
+ async def finish( # pragma: no cover - default no-op
100
+ self,
101
+ success: bool,
102
+ *,
103
+ text: str | None = None,
104
+ content: list[ContentBlock] | None = None,
105
+ error: str | None = None,
106
+ ) -> None:
107
+ return None
108
+
109
+
110
+ class NullWorkflowTelemetry(WorkflowTelemetry):
111
+ """No-op telemetry implementation used when no transport wants workflow updates."""
112
+
113
+ async def __aexit__(self, exc_type, exc, tb) -> bool:
114
+ return False
115
+
116
+
117
+ class WorkflowTelemetryProvider(Protocol):
118
+ """Provider capable of starting workflow steps."""
119
+
120
+ def start_step(
121
+ self,
122
+ tool_name: str,
123
+ *,
124
+ server_name: str = "workflow",
125
+ arguments: dict[str, Any] | None = None,
126
+ ) -> WorkflowTelemetry: ...
127
+
128
+
129
+ class NoOpWorkflowTelemetryProvider:
130
+ """Provider that always returns a no-op workflow step."""
131
+
132
+ def start_step(
133
+ self,
134
+ tool_name: str,
135
+ *,
136
+ server_name: str = "workflow",
137
+ arguments: dict[str, Any] | None = None,
138
+ ) -> WorkflowTelemetry:
139
+ return NullWorkflowTelemetry()
140
+
141
+
142
+ @dataclass
143
+ class _ToolHandlerWorkflowStep(WorkflowTelemetry):
144
+ handler: ToolExecutionHandler
145
+ tool_name: str
146
+ server_name: str
147
+ arguments: dict[str, Any] | None
148
+
149
+ _tool_call_id: str | None = None
150
+ _finished: bool = False
151
+ _lock: asyncio.Lock = asyncio.Lock()
152
+
153
+ async def __aenter__(self) -> WorkflowStepHandle:
154
+ self._tool_call_id = await self.handler.on_tool_start(
155
+ self.tool_name, self.server_name, self.arguments
156
+ )
157
+ return self
158
+
159
+ async def __aexit__(self, exc_type, exc, tb) -> bool:
160
+ if not self._finished:
161
+ success = exc_type is None
162
+ error_message = str(exc) if exc else None
163
+ await self.finish(success, error=error_message)
164
+ return False
165
+
166
+ async def update(
167
+ self,
168
+ *,
169
+ message: str | None = None,
170
+ progress: float | None = None,
171
+ total: float | None = None,
172
+ ) -> None:
173
+ if not self._tool_call_id or (message is None and progress is None):
174
+ return
175
+ await self.handler.on_tool_progress(
176
+ self._tool_call_id,
177
+ progress if progress is not None else 0.0,
178
+ total,
179
+ message,
180
+ )
181
+
182
+ async def finish(
183
+ self,
184
+ success: bool,
185
+ *,
186
+ text: str | None = None,
187
+ content: list[ContentBlock] | None = None,
188
+ error: str | None = None,
189
+ ) -> None:
190
+ if self._finished or not self._tool_call_id:
191
+ self._finished = True
192
+ return
193
+
194
+ final_content = content
195
+ if final_content is None and text:
196
+ final_content: list[ContentBlock] = [TextContent(type="text", text=text)]
197
+
198
+ await self.handler.on_tool_complete(
199
+ self._tool_call_id,
200
+ success,
201
+ final_content,
202
+ error,
203
+ )
204
+ self._finished = True
205
+
206
+
207
+ class ToolHandlerWorkflowTelemetry(NoOpWorkflowTelemetryProvider):
208
+ """
209
+ Telemetry provider that forwards workflow steps to a ToolExecutionHandler.
210
+ """
211
+
212
+ def __init__(self, handler: ToolExecutionHandler, *, server_name: str = "workflow") -> None:
213
+ self._handler = handler
214
+ self._server_name = server_name
215
+
216
+ def start_step(
217
+ self,
218
+ tool_name: str,
219
+ *,
220
+ server_name: str | None = None,
221
+ arguments: dict[str, Any] | None = None,
222
+ ) -> WorkflowTelemetry:
223
+ effective_server = server_name or self._server_name
224
+ return _ToolHandlerWorkflowStep(
225
+ handler=self._handler,
226
+ tool_name=tool_name,
227
+ server_name=effective_server,
228
+ arguments=arguments,
229
+ )
230
+
231
+
232
+ class ACPPlanTelemetryProvider:
233
+ """
234
+ Telemetry provider that sends plan updates via ACP session/update notifications.
235
+ """
236
+
237
+ def __init__(self, connection: Any, session_id: str) -> None:
238
+ self._connection = connection
239
+ self._session_id = session_id
240
+
241
+ async def update_plan(self, entries: list[PlanEntry]) -> None:
242
+ """Send a plan update with the current list of plan entries."""
243
+ if not self._connection:
244
+ return
245
+
246
+ # Convert PlanEntry to dict format expected by ACP
247
+ plan_entries = [
248
+ {
249
+ "content": entry.content,
250
+ "priority": entry.priority,
251
+ "status": entry.status,
252
+ }
253
+ for entry in entries
254
+ ]
255
+
256
+ plan_update = {
257
+ "sessionUpdate": "plan",
258
+ "entries": plan_entries,
259
+ }
260
+
261
+ await self._connection.session_update(session_id=self._session_id, update=plan_update)