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,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]
|
fast_agent/utils/time.py
ADDED
|
@@ -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)
|