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,324 @@
1
+ import asyncio
2
+ import uuid
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Callable, Generic, Protocol, TypeVar
5
+
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+ SignalValueT = TypeVar("SignalValueT")
9
+
10
+
11
+ class Signal(BaseModel, Generic[SignalValueT]):
12
+ """Represents a signal that can be sent to a workflow."""
13
+
14
+ name: str
15
+ description: str | None = "Workflow Signal"
16
+ payload: SignalValueT | None = None
17
+ metadata: dict[str, Any] | None = None
18
+ workflow_id: str | None = None
19
+
20
+ model_config = ConfigDict(arbitrary_types_allowed=True)
21
+
22
+
23
+ class SignalRegistration(BaseModel):
24
+ """Tracks registration of a signal handler."""
25
+
26
+ signal_name: str
27
+ unique_name: str
28
+ workflow_id: str | None = None
29
+
30
+ model_config = ConfigDict(arbitrary_types_allowed=True)
31
+
32
+
33
+ class SignalHandler(Protocol, Generic[SignalValueT]):
34
+ """Protocol for handling signals."""
35
+
36
+ @abstractmethod
37
+ async def signal(self, signal: Signal[SignalValueT]) -> None:
38
+ """Emit a signal to all waiting handlers and registered callbacks."""
39
+
40
+ @abstractmethod
41
+ async def wait_for_signal(
42
+ self,
43
+ signal: Signal[SignalValueT],
44
+ timeout_seconds: int | None = None,
45
+ ) -> SignalValueT:
46
+ """Wait for a signal to be emitted."""
47
+
48
+ def on_signal(self, signal_name: str) -> Callable:
49
+ """
50
+ Decorator to register a handler for a signal.
51
+
52
+ Example:
53
+ @signal_handler.on_signal("approval_needed")
54
+ async def handle_approval(value: str):
55
+ print(f"Got approval signal with value: {value}")
56
+ """
57
+
58
+
59
+ class PendingSignal(BaseModel, Generic[SignalValueT]):
60
+ """Tracks a waiting signal handler and its event."""
61
+
62
+ registration: SignalRegistration
63
+ event: asyncio.Event | None = None
64
+ value: SignalValueT | None = None
65
+
66
+ model_config = ConfigDict(arbitrary_types_allowed=True)
67
+
68
+
69
+ class BaseSignalHandler(ABC, Generic[SignalValueT]):
70
+ """Base class implementing common signal handling functionality."""
71
+
72
+ def __init__(self) -> None:
73
+ # Map signal_name -> list of PendingSignal objects
74
+ self._pending_signals: dict[str, list[PendingSignal]] = {}
75
+ # Map signal_name -> list of (unique_name, handler) tuples
76
+ self._handlers: dict[str, list[tuple[str, Callable]]] = {}
77
+ self._lock = asyncio.Lock()
78
+
79
+ async def cleanup(self, signal_name: str | None = None) -> None:
80
+ """Clean up handlers and registrations for a signal or all signals."""
81
+ async with self._lock:
82
+ if signal_name:
83
+ if signal_name in self._handlers:
84
+ del self._handlers[signal_name]
85
+ if signal_name in self._pending_signals:
86
+ del self._pending_signals[signal_name]
87
+ else:
88
+ self._handlers.clear()
89
+ self._pending_signals.clear()
90
+
91
+ def validate_signal(self, signal: Signal[SignalValueT]) -> None:
92
+ """Validate signal properties."""
93
+ if not signal.name:
94
+ raise ValueError("Signal name is required")
95
+ # Subclasses can override to add more validation
96
+
97
+ def on_signal(self, signal_name: str) -> Callable:
98
+ """Register a handler for a signal."""
99
+
100
+ def decorator(func: Callable) -> Callable:
101
+ unique_name = f"{signal_name}_{uuid.uuid4()}"
102
+
103
+ async def wrapped(value: SignalValueT) -> None:
104
+ try:
105
+ if asyncio.iscoroutinefunction(func):
106
+ await func(value)
107
+ else:
108
+ func(value)
109
+ except Exception as e:
110
+ # Log the error but don't fail the entire signal handling
111
+ print(f"Error in signal handler {signal_name}: {str(e)}")
112
+
113
+ self._handlers.setdefault(signal_name, []).append((unique_name, wrapped))
114
+ return wrapped
115
+
116
+ return decorator
117
+
118
+ @abstractmethod
119
+ async def signal(self, signal: Signal[SignalValueT]) -> None:
120
+ """Emit a signal to all waiting handlers and registered callbacks."""
121
+
122
+ @abstractmethod
123
+ async def wait_for_signal(
124
+ self,
125
+ signal: Signal[SignalValueT],
126
+ timeout_seconds: int | None = None,
127
+ ) -> SignalValueT:
128
+ """Wait for a signal to be emitted."""
129
+
130
+
131
+ class ConsoleSignalHandler(SignalHandler[str]):
132
+ """Simple console-based signal handling (blocks on input)."""
133
+
134
+ def __init__(self) -> None:
135
+ self._pending_signals: dict[str, list[PendingSignal]] = {}
136
+ self._handlers: dict[str, list[Callable]] = {}
137
+
138
+ async def wait_for_signal(self, signal, timeout_seconds=None):
139
+ """Block and wait for console input."""
140
+ print(f"\n[SIGNAL: {signal.name}] {signal.description}")
141
+ if timeout_seconds:
142
+ print(f"(Timeout in {timeout_seconds} seconds)")
143
+
144
+ # Use asyncio.get_event_loop().run_in_executor to make input non-blocking
145
+ loop = asyncio.get_event_loop()
146
+ if timeout_seconds is not None:
147
+ try:
148
+ value = await asyncio.wait_for(
149
+ loop.run_in_executor(None, input, "Enter value: "), timeout_seconds
150
+ )
151
+ except asyncio.TimeoutError:
152
+ print("\nTimeout waiting for input")
153
+ raise
154
+ else:
155
+ value = await loop.run_in_executor(None, input, "Enter value: ")
156
+
157
+ return value
158
+
159
+ # value = input(f"[SIGNAL: {signal.name}] {signal.description}: ")
160
+ # return value
161
+
162
+ def on_signal(self, signal_name):
163
+ def decorator(func):
164
+ async def wrapped(value: SignalValueT) -> None:
165
+ if asyncio.iscoroutinefunction(func):
166
+ await func(value)
167
+ else:
168
+ func(value)
169
+
170
+ self._handlers.setdefault(signal_name, []).append(wrapped)
171
+ return wrapped
172
+
173
+ return decorator
174
+
175
+ async def signal(self, signal) -> None:
176
+ print(f"[SIGNAL SENT: {signal.name}] Value: {signal.payload}")
177
+
178
+ handlers = self._handlers.get(signal.name, [])
179
+ await asyncio.gather(*(handler(signal) for handler in handlers), return_exceptions=True)
180
+
181
+ # Notify any waiting coroutines
182
+ if signal.name in self._pending_signals:
183
+ for ps in self._pending_signals[signal.name]:
184
+ ps.value = signal.payload
185
+ if ps.event is not None:
186
+ ps.event.set()
187
+
188
+
189
+ class AsyncioSignalHandler(BaseSignalHandler[SignalValueT]):
190
+ """
191
+ Asyncio-based signal handling using an internal dictionary of asyncio Events.
192
+ """
193
+
194
+ async def wait_for_signal(self, signal, timeout_seconds: int | None = None) -> SignalValueT:
195
+ event = asyncio.Event()
196
+ unique_name = str(uuid.uuid4())
197
+
198
+ registration = SignalRegistration(
199
+ signal_name=signal.name,
200
+ unique_name=unique_name,
201
+ workflow_id=signal.workflow_id,
202
+ )
203
+
204
+ pending_signal: PendingSignal[SignalValueT] = PendingSignal(
205
+ registration=registration, event=event
206
+ )
207
+
208
+ async with self._lock:
209
+ # Add to pending signals
210
+ self._pending_signals.setdefault(signal.name, []).append(pending_signal)
211
+
212
+ try:
213
+ # Wait for signal
214
+ if timeout_seconds is not None:
215
+ await asyncio.wait_for(event.wait(), timeout_seconds)
216
+ else:
217
+ await event.wait()
218
+
219
+ # After event is set, value should be populated
220
+ if pending_signal.value is None:
221
+ raise ValueError(f"Signal {signal.name} was received but value is None")
222
+ return pending_signal.value
223
+ except asyncio.TimeoutError as e:
224
+ raise TimeoutError(f"Timeout waiting for signal {signal.name}") from e
225
+ finally:
226
+ async with self._lock:
227
+ # Remove from pending signals
228
+ if signal.name in self._pending_signals:
229
+ self._pending_signals[signal.name] = [
230
+ ps
231
+ for ps in self._pending_signals[signal.name]
232
+ if ps.registration.unique_name != unique_name
233
+ ]
234
+ if not self._pending_signals[signal.name]:
235
+ del self._pending_signals[signal.name]
236
+
237
+ def on_signal(self, signal_name):
238
+ def decorator(func):
239
+ async def wrapped(value: SignalValueT) -> None:
240
+ if asyncio.iscoroutinefunction(func):
241
+ await func(value)
242
+ else:
243
+ func(value)
244
+
245
+ self._handlers.setdefault(signal_name, []).append(wrapped)
246
+ return wrapped
247
+
248
+ return decorator
249
+
250
+ async def signal(self, signal) -> None:
251
+ async with self._lock:
252
+ # Notify any waiting coroutines
253
+ if signal.name in self._pending_signals:
254
+ pending = self._pending_signals[signal.name]
255
+ for ps in pending:
256
+ ps.value = signal.payload
257
+ if ps.event is not None:
258
+ ps.event.set()
259
+
260
+ # Notify any registered handler functions
261
+ tasks = []
262
+ handlers = self._handlers.get(signal.name, [])
263
+ for _, handler in handlers:
264
+ tasks.append(handler(signal))
265
+
266
+ await asyncio.gather(*tasks, return_exceptions=True)
267
+
268
+
269
+ # TODO: saqadri - check if we need to do anything to combine this and AsyncioSignalHandler
270
+ class LocalSignalStore:
271
+ """
272
+ Simple in-memory structure that allows coroutines to wait for a signal
273
+ and triggers them when a signal is emitted.
274
+ """
275
+
276
+ def __init__(self) -> None:
277
+ # For each signal_name, store a list of futures that are waiting for it
278
+ self._waiters: dict[str, list[asyncio.Future]] = {}
279
+
280
+ async def emit(self, signal_name: str, payload: Any) -> None:
281
+ # If we have waiting futures, set their result
282
+ if signal_name in self._waiters:
283
+ for future in self._waiters[signal_name]:
284
+ if not future.done():
285
+ future.set_result(payload)
286
+ self._waiters[signal_name].clear()
287
+
288
+ async def wait_for(self, signal_name: str, timeout_seconds: int | None = None) -> Any:
289
+ loop = asyncio.get_running_loop()
290
+ future = loop.create_future()
291
+
292
+ self._waiters.setdefault(signal_name, []).append(future)
293
+
294
+ if timeout_seconds is not None:
295
+ try:
296
+ return await asyncio.wait_for(future, timeout=timeout_seconds)
297
+ except asyncio.TimeoutError:
298
+ # remove the fut from list
299
+ if not future.done():
300
+ self._waiters[signal_name].remove(future)
301
+ raise
302
+ else:
303
+ return await future
304
+
305
+
306
+ class SignalWaitCallback(Protocol):
307
+ """Protocol for callbacks that are triggered when a workflow pauses waiting for a given signal."""
308
+
309
+ async def __call__(
310
+ self,
311
+ signal_name: str,
312
+ request_id: str | None = None,
313
+ workflow_id: str | None = None,
314
+ metadata: dict[str, Any] | None = None,
315
+ ) -> None:
316
+ """
317
+ Receive a notification that a workflow is pausing on a signal.
318
+
319
+ Args:
320
+ signal_name: The name of the signal the workflow is pausing on.
321
+ workflow_id: The ID of the workflow that is pausing (if using a workflow engine).
322
+ metadata: Additional metadata about the signal.
323
+ """
324
+ ...