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,90 @@
1
+ """
2
+ Custom exceptions for the FastAgent framework.
3
+ Enables user-friendly error handling for common issues.
4
+ """
5
+
6
+
7
+ class FastAgentError(Exception):
8
+ """Base exception class for FastAgent errors"""
9
+
10
+ def __init__(self, message: str, details: str = "") -> None:
11
+ self.message = message
12
+ self.details = details
13
+ super().__init__(f"{message}\n\n{details}" if details else message)
14
+
15
+
16
+ class ServerConfigError(FastAgentError):
17
+ """Raised when there are issues with MCP server configuration
18
+ Example: Server name referenced in agent.servers[] but not defined in config
19
+ """
20
+
21
+ def __init__(self, message: str, details: str = "") -> None:
22
+ super().__init__(message, details)
23
+
24
+
25
+ class AgentConfigError(FastAgentError):
26
+ """Raised when there are issues with Agent or Workflow configuration
27
+ Example: Parallel fan-in references unknown agent
28
+ """
29
+
30
+ def __init__(self, message: str, details: str = "") -> None:
31
+ super().__init__(message, details)
32
+
33
+
34
+ class ProviderKeyError(FastAgentError):
35
+ """Raised when there are issues with LLM provider API keys
36
+ Example: OpenAI/Anthropic key not configured but model requires it
37
+ """
38
+
39
+ def __init__(self, message: str, details: str = "") -> None:
40
+ super().__init__(message, details)
41
+
42
+
43
+ class ServerInitializationError(FastAgentError):
44
+ """Raised when a server fails to initialize properly."""
45
+
46
+ def __init__(self, message: str, details: str = "") -> None:
47
+ super().__init__(message, details)
48
+
49
+
50
+ class ModelConfigError(FastAgentError):
51
+ """Raised when there are issues with LLM model configuration
52
+ Example: Unknown model name in model specification string
53
+ """
54
+
55
+ def __init__(self, message: str, details: str = "") -> None:
56
+ super().__init__(message, details)
57
+
58
+
59
+ class CircularDependencyError(FastAgentError):
60
+ """Raised when we detect a Circular Dependency in the workflow"""
61
+
62
+ def __init__(self, message: str, details: str = "") -> None:
63
+ super().__init__(message, details)
64
+
65
+
66
+ class PromptExitError(FastAgentError):
67
+ """Raised from enhanced_prompt when the user requests hard exits"""
68
+
69
+ # TODO an exception for flow control :(
70
+ def __init__(self, message: str, details: str = "") -> None:
71
+ super().__init__(message, details)
72
+
73
+
74
+ class ServerSessionTerminatedError(FastAgentError):
75
+ """Raised when a server session has been terminated (e.g., 404 from server).
76
+
77
+ This typically occurs when a remote StreamableHTTP server restarts and the
78
+ session is no longer valid. When reconnect_on_disconnect is enabled, this
79
+ error triggers automatic reconnection.
80
+ """
81
+
82
+ # Error code for session terminated from MCP SDK streamable_http.py
83
+ # Note: The SDK uses positive 32600 (not the standard JSON-RPC -32600)
84
+ # See: https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/client/streamable_http.py
85
+ SESSION_TERMINATED_CODE = 32600
86
+
87
+ def __init__(self, server_name: str, details: str = "") -> None:
88
+ self.server_name = server_name
89
+ message = f"MCP server '{server_name}' session terminated"
90
+ super().__init__(message, details)
File without changes
@@ -0,0 +1,280 @@
1
+ import asyncio
2
+ import contextvars
3
+ import functools
4
+ from abc import ABC, abstractmethod
5
+ from contextlib import asynccontextmanager
6
+ from datetime import timedelta
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ AsyncIterator,
11
+ Callable,
12
+ Coroutine,
13
+ Type,
14
+ TypeVar,
15
+ Union,
16
+ )
17
+
18
+ from pydantic import BaseModel, ConfigDict
19
+
20
+ from fast_agent.context_dependent import ContextDependent
21
+ from fast_agent.core.executor.workflow_signal import (
22
+ AsyncioSignalHandler,
23
+ Signal,
24
+ SignalHandler,
25
+ SignalValueT,
26
+ )
27
+ from fast_agent.core.logging.logger import get_logger
28
+
29
+ if TYPE_CHECKING:
30
+ from fast_agent.context import Context
31
+
32
+ logger = get_logger(__name__)
33
+
34
+ # Type variable for the return type of tasks
35
+ R = TypeVar("R")
36
+
37
+
38
+ class ExecutorConfig(BaseModel):
39
+ """Configuration for executors."""
40
+
41
+ max_concurrent_activities: int | None = None # Unbounded by default
42
+ timeout_seconds: timedelta | None = None # No timeout by default
43
+ retry_policy: dict[str, Any] | None = None
44
+
45
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
46
+
47
+
48
+ class Executor(ABC, ContextDependent):
49
+ """Abstract base class for different execution backends"""
50
+
51
+ def __init__(
52
+ self,
53
+ engine: str,
54
+ config: ExecutorConfig | None = None,
55
+ signal_bus: SignalHandler = None,
56
+ context: Union["Context", None] = None,
57
+ **kwargs,
58
+ ) -> None:
59
+ super().__init__(context=context, **kwargs)
60
+ self.execution_engine = engine
61
+
62
+ if config:
63
+ self.config = config
64
+ else:
65
+ # TODO: saqadri - executor config should be loaded from settings
66
+ # ctx = get_current_context()
67
+ self.config = ExecutorConfig()
68
+
69
+ self.signal_bus = signal_bus
70
+
71
+ @asynccontextmanager
72
+ async def execution_context(self):
73
+ """Context manager for execution setup/teardown."""
74
+ try:
75
+ yield
76
+ except Exception as e:
77
+ # TODO: saqadri - add logging or other error handling here
78
+ raise e
79
+
80
+ @abstractmethod
81
+ async def execute(
82
+ self,
83
+ *tasks: Callable[..., R] | Coroutine[Any, Any, R],
84
+ **kwargs: Any,
85
+ ) -> list[R | BaseException]:
86
+ """Execute a list of tasks and return their results"""
87
+
88
+ @abstractmethod
89
+ async def execute_streaming(
90
+ self,
91
+ *tasks: list[Callable[..., R] | Coroutine[Any, Any, R]],
92
+ **kwargs: Any,
93
+ ) -> AsyncIterator[R | BaseException]:
94
+ """Execute tasks and yield results as they complete"""
95
+
96
+ async def map(
97
+ self,
98
+ func: Callable[..., R],
99
+ inputs: list[Any],
100
+ **kwargs: Any,
101
+ ) -> list[R | BaseException]:
102
+ """
103
+ Run `func(item)` for each item in `inputs` with concurrency limit.
104
+ """
105
+ results: list[R, BaseException] = []
106
+
107
+ async def run(item):
108
+ if self.config.max_concurrent_activities:
109
+ semaphore = asyncio.Semaphore(self.config.max_concurrent_activities)
110
+ async with semaphore:
111
+ return await self.execute(functools.partial(func, item), **kwargs)
112
+ else:
113
+ return await self.execute(functools.partial(func, item), **kwargs)
114
+
115
+ coros = [run(x) for x in inputs]
116
+ # gather all, each returns a single-element list
117
+ list_of_lists = await asyncio.gather(*coros, return_exceptions=True)
118
+
119
+ # Flatten results
120
+ for entry in list_of_lists:
121
+ if isinstance(entry, list):
122
+ results.extend(entry)
123
+ else:
124
+ # Means we got an exception at the gather level
125
+ results.append(entry)
126
+
127
+ return results
128
+
129
+ async def validate_task(self, task: Callable[..., R] | Coroutine[Any, Any, R]) -> None:
130
+ """Validate a task before execution."""
131
+ if not (asyncio.iscoroutine(task) or asyncio.iscoroutinefunction(task)):
132
+ raise TypeError(f"Task must be async: {task}")
133
+
134
+ async def signal(
135
+ self,
136
+ signal_name: str,
137
+ payload: SignalValueT = None,
138
+ signal_description: str | None = None,
139
+ ) -> None:
140
+ """
141
+ Emit a signal.
142
+ """
143
+ signal = Signal[SignalValueT](
144
+ name=signal_name, payload=payload, description=signal_description
145
+ )
146
+ await self.signal_bus.signal(signal)
147
+
148
+ async def wait_for_signal(
149
+ self,
150
+ signal_name: str,
151
+ request_id: str | None = None,
152
+ workflow_id: str | None = None,
153
+ signal_description: str | None = None,
154
+ timeout_seconds: int | None = None,
155
+ signal_type: Type[SignalValueT] = str,
156
+ ) -> SignalValueT:
157
+ """
158
+ Wait until a signal with signal_name is emitted (or timeout).
159
+ Return the signal's payload when triggered, or raise on timeout.
160
+ """
161
+
162
+ # Notify any callbacks that the workflow is about to be paused waiting for a signal
163
+ if self.context.signal_notification:
164
+ await self.context.signal_notification(
165
+ signal_name=signal_name,
166
+ request_id=request_id,
167
+ workflow_id=workflow_id,
168
+ metadata={
169
+ "description": signal_description,
170
+ "timeout_seconds": timeout_seconds,
171
+ "signal_type": signal_type,
172
+ },
173
+ )
174
+
175
+ signal = Signal[signal_type](
176
+ name=signal_name, description=signal_description, workflow_id=workflow_id
177
+ )
178
+ return await self.signal_bus.wait_for_signal(signal)
179
+
180
+
181
+ class AsyncioExecutor(Executor):
182
+ """Default executor using asyncio"""
183
+
184
+ def __init__(
185
+ self,
186
+ config: ExecutorConfig | None = None,
187
+ signal_bus: SignalHandler | None = None,
188
+ ) -> None:
189
+ signal_bus = signal_bus or AsyncioSignalHandler()
190
+ super().__init__(engine="asyncio", config=config, signal_bus=signal_bus)
191
+
192
+ self._activity_semaphore: asyncio.Semaphore | None = None
193
+ if self.config.max_concurrent_activities is not None:
194
+ self._activity_semaphore = asyncio.Semaphore(self.config.max_concurrent_activities)
195
+
196
+ async def _execute_task(
197
+ self, task: Callable[..., R] | Coroutine[Any, Any, R], **kwargs: Any
198
+ ) -> R | BaseException:
199
+ async def run_task(task: Callable[..., R] | Coroutine[Any, Any, R]) -> R:
200
+ try:
201
+ if asyncio.iscoroutine(task):
202
+ return await task
203
+ elif asyncio.iscoroutinefunction(task):
204
+ return await task(**kwargs)
205
+ else:
206
+ # Execute the callable and await if it returns a coroutine
207
+ loop = asyncio.get_running_loop()
208
+ ctx = contextvars.copy_context()
209
+ # If kwargs are provided, wrap the function with partial
210
+ if kwargs:
211
+ wrapped_task = functools.partial(task, **kwargs)
212
+ result = await loop.run_in_executor(None, lambda: ctx.run(wrapped_task))
213
+ else:
214
+ result = await loop.run_in_executor(None, lambda: ctx.run(task))
215
+
216
+ # Handle case where the sync function returns a coroutine
217
+ if asyncio.iscoroutine(result):
218
+ return await result
219
+
220
+ return result
221
+ except Exception as e:
222
+ return e
223
+
224
+ if self._activity_semaphore:
225
+ async with self._activity_semaphore:
226
+ return await run_task(task)
227
+ else:
228
+ return await run_task(task)
229
+
230
+ async def execute(
231
+ self,
232
+ *tasks: Callable[..., R] | Coroutine[Any, Any, R],
233
+ **kwargs: Any,
234
+ ) -> list[R | BaseException]:
235
+ return await asyncio.gather(
236
+ *(self._execute_task(task, **kwargs) for task in tasks),
237
+ return_exceptions=True,
238
+ )
239
+
240
+ async def execute_streaming(
241
+ self,
242
+ *tasks: list[Callable[..., R] | Coroutine[Any, Any, R]],
243
+ **kwargs: Any,
244
+ ) -> AsyncIterator[R | BaseException]:
245
+ # TODO: saqadri - validate if async with self.execution_context() is needed here
246
+ async with self.execution_context():
247
+ # Create futures for all tasks
248
+ futures = [asyncio.create_task(self._execute_task(task, **kwargs)) for task in tasks]
249
+ pending = set(futures)
250
+
251
+ while pending:
252
+ done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
253
+ for future in done:
254
+ yield await future
255
+
256
+ async def signal(
257
+ self,
258
+ signal_name: str,
259
+ payload: SignalValueT = None,
260
+ signal_description: str | None = None,
261
+ ) -> None:
262
+ await super().signal(signal_name, payload, signal_description)
263
+
264
+ async def wait_for_signal(
265
+ self,
266
+ signal_name: str,
267
+ request_id: str | None = None,
268
+ workflow_id: str | None = None,
269
+ signal_description: str | None = None,
270
+ timeout_seconds: int | None = None,
271
+ signal_type: Type[SignalValueT] = str,
272
+ ) -> SignalValueT:
273
+ return await super().wait_for_signal(
274
+ signal_name,
275
+ request_id,
276
+ workflow_id,
277
+ signal_description,
278
+ timeout_seconds,
279
+ signal_type,
280
+ )
@@ -0,0 +1,32 @@
1
+ """
2
+ Keep track of all activities/tasks that the executor needs to run.
3
+ This is used by the workflow engine to dynamically orchestrate a workflow graph.
4
+ The user just writes standard functions annotated with @workflow_task, but behind the scenes a workflow graph is built.
5
+ """
6
+
7
+ from typing import Any, Callable
8
+
9
+
10
+ class ActivityRegistry:
11
+ """Centralized task/activity management with validation and metadata."""
12
+
13
+ def __init__(self) -> None:
14
+ self._activities: dict[str, Callable] = {}
15
+ self._metadata: dict[str, dict[str, Any]] = {}
16
+
17
+ def register(self, name: str, func: Callable, metadata: dict[str, Any] | None = None) -> None:
18
+ if name in self._activities:
19
+ raise ValueError(f"Activity '{name}' is already registered.")
20
+ self._activities[name] = func
21
+ self._metadata[name] = metadata or {}
22
+
23
+ def get_activity(self, name: str) -> Callable:
24
+ if name not in self._activities:
25
+ raise KeyError(f"Activity '{name}' not found.")
26
+ return self._activities[name]
27
+
28
+ def get_metadata(self, name: str) -> dict[str, Any]:
29
+ return self._metadata.get(name, {})
30
+
31
+ def list_activities(self) -> list[str]:
32
+ return list(self._activities.keys())