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,481 @@
1
+ """
2
+ Transports for the Logger module for MCP Agent, including:
3
+ - Local + optional remote event transport
4
+ - Async event bus
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import traceback
10
+ from abc import ABC, abstractmethod
11
+ from pathlib import Path
12
+ from typing import Protocol
13
+
14
+ import aiohttp
15
+ from opentelemetry import trace
16
+ from rich import print
17
+ from rich.json import JSON
18
+ from rich.text import Text
19
+
20
+ from fast_agent.config import LoggerSettings
21
+ from fast_agent.core.logging.events import Event, EventFilter
22
+ from fast_agent.core.logging.json_serializer import JSONSerializer
23
+ from fast_agent.core.logging.listeners import EventListener, LifecycleAwareListener
24
+ from fast_agent.ui.console import console
25
+
26
+
27
+ class EventTransport(Protocol):
28
+ """
29
+ Pluggable interface for sending events to a remote or external system
30
+ (Kafka, RabbitMQ, REST, etc.).
31
+ """
32
+
33
+ async def send_event(self, event: Event) -> None:
34
+ """
35
+ Send an event to the external system.
36
+ Args:
37
+ event: Event to send.
38
+ """
39
+ ...
40
+
41
+
42
+ class FilteredEventTransport(EventTransport, ABC):
43
+ """
44
+ Event transport that filters events based on a filter before sending.
45
+ """
46
+
47
+ def __init__(self, event_filter: EventFilter | None = None) -> None:
48
+ self.filter = event_filter
49
+
50
+ async def send_event(self, event: Event) -> None:
51
+ if not self.filter or self.filter.matches(event):
52
+ await self.send_matched_event(event)
53
+
54
+ @abstractmethod
55
+ async def send_matched_event(self, event: Event):
56
+ """Send an event to the external system."""
57
+
58
+
59
+ class NoOpTransport(FilteredEventTransport):
60
+ """Default transport that does nothing (purely local)."""
61
+
62
+ async def send_matched_event(self, event) -> None:
63
+ """Do nothing."""
64
+ pass
65
+
66
+
67
+ class ConsoleTransport(FilteredEventTransport):
68
+ """Simple transport that prints events to console."""
69
+
70
+ def __init__(self, event_filter: EventFilter | None = None) -> None:
71
+ super().__init__(event_filter=event_filter)
72
+ # Use shared console instances
73
+ self._serializer = JSONSerializer()
74
+ self.log_level_styles: dict[str, str] = {
75
+ "info": "bold green",
76
+ "debug": "dim white",
77
+ "warning": "bold yellow",
78
+ "error": "bold red",
79
+ }
80
+
81
+ async def send_matched_event(self, event: Event) -> None:
82
+ # Map log levels to styles
83
+ style = self.log_level_styles.get(event.type, "white")
84
+
85
+ # Use the appropriate console based on event type
86
+ # output_console = error_console if event.type == "error" else console
87
+ output_console = console
88
+
89
+ # Create namespace without None
90
+ namespace = event.namespace
91
+ if event.name:
92
+ namespace = f"{namespace}.{event.name}"
93
+
94
+ log_text = Text.assemble(
95
+ (f"[{event.type.upper()}] ", style),
96
+ (f"{event.timestamp.replace(microsecond=0).isoformat()} ", "cyan"),
97
+ (f"{namespace} ", "magenta"),
98
+ (f"- {event.message}", "white"),
99
+ )
100
+ output_console.print(log_text)
101
+
102
+ # Print additional data as JSON if available
103
+ if event.data:
104
+ serialized_data = self._serializer(event.data)
105
+ output_console.print(JSON.from_data(serialized_data))
106
+
107
+
108
+ class FileTransport(FilteredEventTransport):
109
+ """Transport that writes events to a file with proper formatting."""
110
+
111
+ def __init__(
112
+ self,
113
+ filepath: str | Path,
114
+ event_filter: EventFilter | None = None,
115
+ mode: str = "a",
116
+ encoding: str = "utf-8",
117
+ ) -> None:
118
+ """Initialize FileTransport.
119
+
120
+ Args:
121
+ filepath: Path to the log file. If relative, the current working directory will be used
122
+ event_filter: Optional filter for events
123
+ mode: File open mode ('a' for append, 'w' for write)
124
+ encoding: File encoding to use
125
+ """
126
+ super().__init__(event_filter=event_filter)
127
+ self.filepath = Path(filepath)
128
+ self.mode = mode
129
+ self.encoding = encoding
130
+ self._serializer = JSONSerializer()
131
+
132
+ # Create directory if it doesn't exist
133
+ self.filepath.parent.mkdir(parents=True, exist_ok=True)
134
+
135
+ async def send_matched_event(self, event: Event) -> None:
136
+ """Write matched event to log file asynchronously.
137
+
138
+ Args:
139
+ event: Event to write to file
140
+ """
141
+ # Format the log entry
142
+ namespace = event.namespace
143
+ if event.name:
144
+ namespace = f"{namespace}.{event.name}"
145
+
146
+ log_entry = {
147
+ "level": event.type.upper(),
148
+ "timestamp": event.timestamp.isoformat(),
149
+ "namespace": namespace,
150
+ "message": event.message,
151
+ }
152
+
153
+ # Add event data if present
154
+ if event.data:
155
+ log_entry["data"] = self._serializer(event.data)
156
+
157
+ try:
158
+ with open(self.filepath, mode=self.mode, encoding=self.encoding) as f:
159
+ # Write the log entry as compact JSON (JSONL format)
160
+ f.write(json.dumps(log_entry, separators=(",", ":")) + "\n")
161
+ f.flush() # Ensure writing to disk
162
+ except IOError as e:
163
+ # Log error without recursion
164
+ print(f"Error writing to log file {self.filepath}: {e}")
165
+
166
+ async def close(self) -> None:
167
+ """Clean up resources if needed."""
168
+ pass # File handles are automatically closed after each write
169
+
170
+ @property
171
+ def is_closed(self) -> bool:
172
+ """Check if transport is closed."""
173
+ return False # Since we open/close per write
174
+
175
+
176
+ class HTTPTransport(FilteredEventTransport):
177
+ """
178
+ Sends events to an HTTP endpoint in batches.
179
+ Useful for sending to remote logging services like Elasticsearch, etc.
180
+ """
181
+
182
+ def __init__(
183
+ self,
184
+ endpoint: str,
185
+ headers: dict[str, str] = None,
186
+ batch_size: int = 100,
187
+ timeout: float = 5.0,
188
+ event_filter: EventFilter | None = None,
189
+ ) -> None:
190
+ super().__init__(event_filter=event_filter)
191
+ self.endpoint = endpoint
192
+ self.headers = headers or {}
193
+ self.batch_size = batch_size
194
+ self.timeout = timeout
195
+
196
+ self.batch: list[Event] = []
197
+ self.lock = asyncio.Lock()
198
+ self._session: aiohttp.ClientSession | None = None
199
+ self._serializer = JSONSerializer()
200
+
201
+ async def start(self) -> None:
202
+ """Initialize HTTP session."""
203
+ if not self._session:
204
+ self._session = aiohttp.ClientSession(
205
+ headers=self.headers, timeout=aiohttp.ClientTimeout(total=self.timeout)
206
+ )
207
+
208
+ async def stop(self) -> None:
209
+ """Close HTTP session and flush any remaining events."""
210
+ if self.batch:
211
+ await self._flush()
212
+ if self._session:
213
+ await self._session.close()
214
+ self._session = None
215
+
216
+ async def send_matched_event(self, event: Event) -> None:
217
+ """Add event to batch, flush if batch is full."""
218
+ async with self.lock:
219
+ self.batch.append(event)
220
+ if len(self.batch) >= self.batch_size:
221
+ await self._flush()
222
+
223
+ async def _flush(self) -> None:
224
+ """Send batch of events to HTTP endpoint."""
225
+ if not self.batch:
226
+ return
227
+
228
+ if not self._session:
229
+ await self.start()
230
+
231
+ try:
232
+ # Convert events to JSON-serializable dicts
233
+ events_data = [
234
+ {
235
+ "timestamp": event.timestamp.isoformat(),
236
+ "type": event.type,
237
+ "name": event.name,
238
+ "namespace": event.namespace,
239
+ "message": event.message,
240
+ "data": self._serializer(event.data),
241
+ "trace_id": event.trace_id,
242
+ "span_id": event.span_id,
243
+ "context": event.context.dict() if event.context else None,
244
+ }
245
+ for event in self.batch
246
+ ]
247
+
248
+ async with self._session.post(self.endpoint, json=events_data) as response:
249
+ if response.status >= 400:
250
+ text = await response.text()
251
+ print(
252
+ f"Error sending log events to {self.endpoint}. "
253
+ f"Status: {response.status}, Response: {text}"
254
+ )
255
+ except Exception as e:
256
+ print(f"Error sending log events to {self.endpoint}: {e}")
257
+ finally:
258
+ self.batch.clear()
259
+
260
+
261
+ class AsyncEventBus:
262
+ """
263
+ Async event bus with local in-process listeners + optional remote transport.
264
+ Also injects distributed tracing (trace_id, span_id) if there's a current span.
265
+ """
266
+
267
+ _instance = None
268
+
269
+ def __init__(self, transport: EventTransport | None = None) -> None:
270
+ self.transport: EventTransport = transport or NoOpTransport()
271
+ self.listeners: dict[str, EventListener] = {}
272
+ self._queue: asyncio.Queue | None = None
273
+ self._task: asyncio.Task | None = None
274
+ self._running = False
275
+
276
+ @classmethod
277
+ def get(cls, transport: EventTransport | None = None) -> "AsyncEventBus":
278
+ """Get the singleton instance of the event bus."""
279
+ if cls._instance is None:
280
+ cls._instance = cls(transport=transport)
281
+ elif transport is not None:
282
+ # Update transport if provided
283
+ cls._instance.transport = transport
284
+ return cls._instance
285
+
286
+ @classmethod
287
+ def reset(cls) -> None:
288
+ """
289
+ Reset the singleton instance.
290
+ This is primarily useful for testing scenarios where you need to ensure
291
+ a clean state between tests.
292
+ """
293
+ if cls._instance:
294
+ # Signal shutdown
295
+ cls._instance._running = False
296
+
297
+ # Clear the singleton instance
298
+ cls._instance = None
299
+
300
+ async def start(self) -> None:
301
+ """Start the event bus and all lifecycle-aware listeners."""
302
+ if self._running:
303
+ return
304
+
305
+ try:
306
+ asyncio.get_running_loop()
307
+ except RuntimeError:
308
+ loop = asyncio.new_event_loop()
309
+ asyncio.set_event_loop(loop)
310
+
311
+ self._queue = asyncio.Queue()
312
+
313
+ # Start each lifecycle-aware listener
314
+ for listener in self.listeners.values():
315
+ if isinstance(listener, LifecycleAwareListener):
316
+ await listener.start()
317
+
318
+ # Start processing
319
+ self._running = True
320
+ self._task = asyncio.create_task(self._process_events())
321
+
322
+ async def stop(self) -> None:
323
+ """Stop the event bus and all lifecycle-aware listeners."""
324
+ if not self._running:
325
+ return
326
+
327
+ # Signal processing to stop
328
+ self._running = False
329
+
330
+ # Try to process remaining items with a timeout
331
+ if not self._queue.empty():
332
+ try:
333
+ # Give some time for remaining items to be processed
334
+ await asyncio.wait_for(self._queue.join(), timeout=5.0)
335
+ except asyncio.TimeoutError:
336
+ # If we timeout, drain the queue to prevent deadlock
337
+ while not self._queue.empty():
338
+ try:
339
+ self._queue.get_nowait()
340
+ self._queue.task_done()
341
+ except asyncio.QueueEmpty:
342
+ break
343
+ except Exception as e:
344
+ print(f"Error during queue cleanup: {e}")
345
+ self._queue = None
346
+
347
+ # Cancel and wait for task with timeout
348
+ if self._task and not self._task.done():
349
+ self._task.cancel()
350
+ try:
351
+ # Wait for task to complete with timeout
352
+ await asyncio.wait_for(self._task, timeout=5.0)
353
+ except (asyncio.CancelledError, asyncio.TimeoutError):
354
+ pass # Task was cancelled or timed out
355
+ except Exception as e:
356
+ print(f"Error cancelling process task: {e}")
357
+ self._task = None
358
+
359
+ # Stop each lifecycle-aware listener
360
+ for listener in self.listeners.values():
361
+ if isinstance(listener, LifecycleAwareListener):
362
+ try:
363
+ await asyncio.wait_for(listener.stop(), timeout=3.0)
364
+ except asyncio.TimeoutError:
365
+ print(f"Timeout stopping listener: {listener}")
366
+ except Exception as e:
367
+ print(f"Error stopping listener: {e}")
368
+
369
+ async def emit(self, event: Event) -> None:
370
+ """Emit an event to all listeners and transport."""
371
+ if not self._running:
372
+ return
373
+
374
+ # Inject current tracing info if available
375
+ span = trace.get_current_span()
376
+ if span.is_recording():
377
+ ctx = span.get_span_context()
378
+ event.trace_id = f"{ctx.trace_id:032x}"
379
+ event.span_id = f"{ctx.span_id:016x}"
380
+
381
+ # Forward to transport first (immediate processing)
382
+ try:
383
+ await self.transport.send_event(event)
384
+ except Exception as e:
385
+ print(f"Error in transport.send_event: {e}")
386
+
387
+ # Then queue for listeners
388
+ await self._queue.put(event)
389
+
390
+ def add_listener(self, name: str, listener: EventListener) -> None:
391
+ """Add a listener to the event bus."""
392
+ self.listeners[name] = listener
393
+
394
+ def remove_listener(self, name: str) -> None:
395
+ """Remove a listener from the event bus."""
396
+ self.listeners.pop(name, None)
397
+
398
+ async def _process_events(self) -> None:
399
+ """Process events from the queue until stopped."""
400
+ while self._running:
401
+ event = None
402
+ try:
403
+ # Use wait_for with a timeout to allow checking running state
404
+ try:
405
+ event = await asyncio.wait_for(self._queue.get(), timeout=0.1)
406
+ except asyncio.TimeoutError:
407
+ continue
408
+
409
+ # Process the event through all listeners
410
+ tasks = []
411
+ for listener in self.listeners.values():
412
+ try:
413
+ tasks.append(listener.handle_event(event))
414
+ except Exception as e:
415
+ print(f"Error creating listener task: {e}")
416
+
417
+ if tasks:
418
+ results = await asyncio.gather(*tasks, return_exceptions=True)
419
+ for r in results:
420
+ if isinstance(r, Exception):
421
+ print(f"Error in listener: {r}")
422
+ print(
423
+ f"Stacktrace: {''.join(traceback.format_exception(type(r), r, r.__traceback__))}"
424
+ )
425
+
426
+ except asyncio.CancelledError:
427
+ # TODO -- added _queue assertion; is that necessary?
428
+ if event is not None and self._queue is not None:
429
+ self._queue.task_done()
430
+ raise
431
+ except Exception as e:
432
+ print(f"Error in event processing loop: {e}")
433
+ # Mark task done for this event
434
+ if event is not None and self._queue is not None:
435
+ self._queue.task_done()
436
+
437
+ # Process remaining events in queue
438
+ if self._queue:
439
+ while not self._queue.empty():
440
+ try:
441
+ event = self._queue.get_nowait()
442
+ tasks = []
443
+ for listener in self.listeners.values():
444
+ try:
445
+ tasks.append(listener.handle_event(event))
446
+ except Exception:
447
+ pass
448
+ if tasks:
449
+ await asyncio.gather(*tasks, return_exceptions=True)
450
+ self._queue.task_done()
451
+ except asyncio.QueueEmpty:
452
+ break
453
+
454
+
455
+ def create_transport(
456
+ settings: LoggerSettings, event_filter: EventFilter | None = None
457
+ ) -> EventTransport:
458
+ """Create event transport based on settings."""
459
+ if settings.type == "none":
460
+ return NoOpTransport(event_filter=event_filter)
461
+ elif settings.type == "console":
462
+ return ConsoleTransport(event_filter=event_filter)
463
+ elif settings.type == "file":
464
+ if not settings.path:
465
+ raise ValueError("File path required for file transport")
466
+ return FileTransport(
467
+ filepath=settings.path,
468
+ event_filter=event_filter,
469
+ )
470
+ elif settings.type == "http":
471
+ if not settings.http_endpoint:
472
+ raise ValueError("HTTP endpoint required for HTTP transport")
473
+ return HTTPTransport(
474
+ endpoint=settings.http_endpoint,
475
+ headers=settings.http_headers,
476
+ batch_size=settings.batch_size,
477
+ timeout=settings.http_timeout,
478
+ event_filter=event_filter,
479
+ )
480
+ else:
481
+ raise ValueError(f"Unsupported transport type: {settings.type}")
@@ -0,0 +1,9 @@
1
+ """
2
+ Compatibility shim: expose Prompt under fast_agent.core.prompt during migration.
3
+
4
+ Canonical location: fast_agent.mcp.prompt.Prompt
5
+ """
6
+
7
+ from fast_agent.mcp.prompt import Prompt
8
+
9
+ __all__ = ["Prompt"]
@@ -0,0 +1,183 @@
1
+ """
2
+ Helpers for applying template variables to system prompts after initial bootstrap.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import platform
8
+ import re
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Mapping, MutableMapping
11
+
12
+ if TYPE_CHECKING:
13
+ from fast_agent.skills import SkillManifest
14
+
15
+
16
+ def apply_template_variables(
17
+ template: str | None, variables: Mapping[str, str | None] | None
18
+ ) -> str | None:
19
+ """
20
+ Apply a mapping of template variables to the provided template string.
21
+
22
+ This helper intentionally performs no work when either the template or variables
23
+ are empty so callers can safely execute it during both the initial and late
24
+ initialization passes without accidentally stripping placeholders too early.
25
+
26
+ Supports both simple variable substitution and file template patterns:
27
+ - {{variable}} - Simple variable replacement
28
+ - {{file:relative/path}} - Reads file contents (relative to workspaceRoot, errors if missing)
29
+ - {{file_silent:relative/path}} - Reads file contents (relative to workspaceRoot, empty if missing)
30
+ """
31
+ if not template or not variables:
32
+ return template
33
+
34
+ resolved = template
35
+
36
+ # Get workspaceRoot for file resolution
37
+ workspace_root = variables.get("workspaceRoot")
38
+
39
+ # Apply {{file:...}} templates (relative paths required, resolved from workspaceRoot)
40
+ file_pattern = re.compile(r"\{\{file:([^}]+)\}\}")
41
+
42
+ def replace_file(match):
43
+ file_path_str = match.group(1).strip()
44
+ file_path = Path(file_path_str).expanduser()
45
+
46
+ # Enforce relative paths
47
+ if file_path.is_absolute():
48
+ raise ValueError(
49
+ f"File template paths must be relative, got absolute path: {file_path_str}"
50
+ )
51
+
52
+ # Resolve against workspaceRoot if available
53
+ if workspace_root:
54
+ resolved_path = (Path(workspace_root) / file_path).resolve()
55
+ else:
56
+ resolved_path = file_path.resolve()
57
+
58
+ return resolved_path.read_text(encoding="utf-8")
59
+
60
+ resolved = file_pattern.sub(replace_file, resolved)
61
+
62
+ # Apply {{file_silent:...}} templates (missing files become empty strings)
63
+ file_silent_pattern = re.compile(r"\{\{file_silent:([^}]+)\}\}")
64
+
65
+ def replace_file_silent(match):
66
+ file_path_str = match.group(1).strip()
67
+ file_path = Path(file_path_str).expanduser()
68
+
69
+ # Enforce relative paths
70
+ if file_path.is_absolute():
71
+ raise ValueError(
72
+ f"File template paths must be relative, got absolute path: {file_path_str}"
73
+ )
74
+
75
+ # Resolve against workspaceRoot if available
76
+ if workspace_root:
77
+ resolved_path = (Path(workspace_root) / file_path).resolve()
78
+ else:
79
+ resolved_path = file_path.resolve()
80
+
81
+ try:
82
+ return resolved_path.read_text(encoding="utf-8")
83
+ except FileNotFoundError:
84
+ return ""
85
+
86
+ resolved = file_silent_pattern.sub(replace_file_silent, resolved)
87
+
88
+ # Apply simple variable substitutions
89
+ for key, value in variables.items():
90
+ if value is None:
91
+ continue
92
+ placeholder = f"{{{{{key}}}}}"
93
+ if placeholder in resolved:
94
+ resolved = resolved.replace(placeholder, value)
95
+
96
+ return resolved
97
+
98
+
99
+ def load_skills_for_context(
100
+ workspace_root: str | None, skills_directory_override: str | None = None
101
+ ) -> list["SkillManifest"]:
102
+ """
103
+ Load skill manifests from the workspace root or override directory.
104
+
105
+ Args:
106
+ workspace_root: The workspace root directory
107
+ skills_directory_override: Optional override for skills directory (relative to workspace_root)
108
+
109
+ Returns:
110
+ List of SkillManifest objects
111
+ """
112
+ from fast_agent.skills.registry import SkillRegistry
113
+
114
+ if not workspace_root:
115
+ return []
116
+
117
+ base_dir = Path(workspace_root)
118
+
119
+ # If override is provided, treat it as relative to workspace_root
120
+ override_dir = None
121
+ if skills_directory_override:
122
+ override_path = Path(skills_directory_override)
123
+ # If it's absolute, use as-is; otherwise make relative to workspace_root
124
+ if override_path.is_absolute():
125
+ override_dir = override_path
126
+ else:
127
+ override_dir = base_dir / override_path
128
+
129
+ registry = SkillRegistry(base_dir=base_dir, override_directory=override_dir)
130
+ return registry.load_manifests()
131
+
132
+
133
+ def enrich_with_environment_context(
134
+ context: MutableMapping[str, str],
135
+ cwd: str | None,
136
+ client_info: Mapping[str, str] | None,
137
+ skills_directory_override: str | None = None,
138
+ ) -> None:
139
+ """
140
+ Populate the provided context mapping with environment details used for template replacement.
141
+
142
+ Args:
143
+ context: The context mapping to populate
144
+ cwd: The current working directory (workspace root)
145
+ client_info: Client information mapping
146
+ skills_directory_override: Optional override for skills directory
147
+ """
148
+ if cwd:
149
+ context["workspaceRoot"] = cwd
150
+
151
+ server_platform = platform.platform()
152
+ python_version = platform.python_version()
153
+
154
+ # Provide individual placeholders for automation
155
+ if server_platform:
156
+ context["hostPlatform"] = server_platform
157
+ context["pythonVer"] = python_version
158
+
159
+ # Load and format agent skills
160
+ if cwd:
161
+ from fast_agent.skills.registry import format_skills_for_prompt
162
+
163
+ skill_manifests = load_skills_for_context(cwd, skills_directory_override)
164
+ skills_text = format_skills_for_prompt(skill_manifests)
165
+ context["agentSkills"] = skills_text
166
+
167
+ env_lines: list[str] = []
168
+ if cwd:
169
+ env_lines.append(f"Workspace root: {cwd}")
170
+ if client_info:
171
+ display_name = client_info.get("title") or client_info.get("name")
172
+ version = client_info.get("version")
173
+ if display_name:
174
+ if version and version != "unknown":
175
+ env_lines.append(f"Client: {display_name} {version}")
176
+ else:
177
+ env_lines.append(f"Client: {display_name}")
178
+ if server_platform:
179
+ env_lines.append(f"Host platform: {server_platform}")
180
+
181
+ if env_lines:
182
+ formatted = "Environment:\n- " + "\n- ".join(env_lines)
183
+ context["env"] = formatted