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,931 @@
1
+ import asyncio
2
+ import inspect
3
+ import json
4
+ import os
5
+ import time
6
+ from abc import abstractmethod
7
+ from contextvars import ContextVar
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ Awaitable,
12
+ Callable,
13
+ Generic,
14
+ Type,
15
+ TypeVar,
16
+ Union,
17
+ cast,
18
+ )
19
+
20
+ from mcp import Tool
21
+ from mcp.types import (
22
+ GetPromptResult,
23
+ PromptMessage,
24
+ TextContent,
25
+ )
26
+ from openai import NotGiven
27
+ from openai.lib._parsing import type_to_response_format_param as _type_to_response_format
28
+ from pydantic_core import from_json
29
+ from rich import print as rich_print
30
+
31
+ from fast_agent.constants import (
32
+ CONTROL_MESSAGE_SAVE_HISTORY,
33
+ DEFAULT_MAX_ITERATIONS,
34
+ FAST_AGENT_TIMING,
35
+ )
36
+ from fast_agent.context_dependent import ContextDependent
37
+ from fast_agent.core.exceptions import AgentConfigError, ProviderKeyError, ServerConfigError
38
+ from fast_agent.core.logging.logger import get_logger
39
+ from fast_agent.core.prompt import Prompt
40
+ from fast_agent.event_progress import ProgressAction
41
+ from fast_agent.interfaces import (
42
+ FastAgentLLMProtocol,
43
+ ModelT,
44
+ )
45
+ from fast_agent.llm.memory import Memory, SimpleMemory
46
+ from fast_agent.llm.model_database import ModelDatabase
47
+ from fast_agent.llm.provider_types import Provider
48
+ from fast_agent.llm.stream_types import StreamChunk
49
+ from fast_agent.llm.usage_tracking import TurnUsage, UsageAccumulator
50
+ from fast_agent.mcp.helpers.content_helpers import get_text
51
+ from fast_agent.types import PromptMessageExtended, RequestParams
52
+
53
+ # Define type variables locally
54
+ MessageParamT = TypeVar("MessageParamT")
55
+ MessageT = TypeVar("MessageT")
56
+
57
+ # Forward reference for type annotations
58
+ if TYPE_CHECKING:
59
+ from fast_agent.context import Context
60
+
61
+
62
+ # Context variable for storing MCP metadata
63
+ _mcp_metadata_var: ContextVar[dict[str, Any] | None] = ContextVar("mcp_metadata", default=None)
64
+
65
+
66
+ def deep_merge(dict1: dict[Any, Any], dict2: dict[Any, Any]) -> dict[Any, Any]:
67
+ """
68
+ Recursively merges `dict2` into `dict1` in place.
69
+
70
+ If a key exists in both dictionaries and their values are dictionaries,
71
+ the function merges them recursively. Otherwise, the value from `dict2`
72
+ overwrites or is added to `dict1`.
73
+
74
+ Args:
75
+ dict1 (Dict): The dictionary to be updated.
76
+ dict2 (Dict): The dictionary to merge into `dict1`.
77
+
78
+ Returns:
79
+ Dict: The updated `dict1`.
80
+ """
81
+ for key in dict2:
82
+ if key in dict1 and isinstance(dict1[key], dict) and isinstance(dict2[key], dict):
83
+ deep_merge(dict1[key], dict2[key])
84
+ else:
85
+ dict1[key] = dict2[key]
86
+ return dict1
87
+
88
+
89
+ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT, MessageT]):
90
+ # Common parameter names used across providers
91
+ PARAM_MESSAGES = "messages"
92
+ PARAM_MODEL = "model"
93
+ PARAM_MAX_TOKENS = "maxTokens"
94
+ PARAM_SYSTEM_PROMPT = "systemPrompt"
95
+ PARAM_STOP_SEQUENCES = "stopSequences"
96
+ PARAM_PARALLEL_TOOL_CALLS = "parallel_tool_calls"
97
+ PARAM_METADATA = "metadata"
98
+ PARAM_USE_HISTORY = "use_history"
99
+ PARAM_MAX_ITERATIONS = "max_iterations"
100
+ PARAM_TEMPLATE_VARS = "template_vars"
101
+ PARAM_MCP_METADATA = "mcp_metadata"
102
+
103
+ # Base set of fields that should always be excluded
104
+ BASE_EXCLUDE_FIELDS = {PARAM_METADATA}
105
+
106
+ """
107
+ Implementation of the Llm Protocol - intended be subclassed for Provider
108
+ or behaviour specific reasons. Contains convenience and template methods.
109
+ """
110
+
111
+ def __init__(
112
+ self,
113
+ provider: Provider,
114
+ instruction: str | None = None,
115
+ name: str | None = None,
116
+ request_params: RequestParams | None = None,
117
+ context: Union["Context", None] = None,
118
+ model: str | None = None,
119
+ api_key: str | None = None,
120
+ **kwargs: dict[str, Any],
121
+ ) -> None:
122
+ """
123
+
124
+ Args:
125
+ provider: LLM API Provider
126
+ instruction: System prompt for the LLM
127
+ name: Name for the LLM (usually attached Agent name)
128
+ request_params: RequestParams to configure LLM behaviour
129
+ context: Application context
130
+ model: Optional model name override
131
+ **kwargs: Additional provider-specific parameters
132
+ """
133
+ # Extract request_params before super() call
134
+ self._init_request_params = request_params
135
+ super().__init__(context=context, **kwargs)
136
+ self.logger = get_logger(__name__)
137
+ self.executor = self.context.executor
138
+ self.name: str = name or "fast-agent"
139
+ self.instruction = instruction
140
+ self._provider = provider
141
+ # memory contains provider specific API types.
142
+ self.history: Memory[MessageParamT] = SimpleMemory[MessageParamT]()
143
+
144
+ # Initialize the display component
145
+ from fast_agent.ui.console_display import ConsoleDisplay
146
+
147
+ self.display = ConsoleDisplay(config=self.context.config)
148
+
149
+ # Initialize default parameters, passing model info
150
+ model_kwargs = kwargs.copy()
151
+ if model:
152
+ model_kwargs["model"] = model
153
+ self.default_request_params = self._initialize_default_params(model_kwargs)
154
+
155
+ # Merge with provided params if any
156
+ if self._init_request_params:
157
+ self.default_request_params = self._merge_request_params(
158
+ self.default_request_params, self._init_request_params
159
+ )
160
+
161
+ # Cache effective model name for type-safe access
162
+ self._model_name: str | None = self.default_request_params.model
163
+
164
+ self.verb = kwargs.get("verb")
165
+
166
+ self._init_api_key = api_key
167
+
168
+ # Initialize usage tracking
169
+ self._usage_accumulator = UsageAccumulator()
170
+ self._stream_listeners: set[Callable[[StreamChunk], None]] = set()
171
+ self._tool_stream_listeners: set[Callable[[str, dict[str, Any] | None], None]] = set()
172
+ self.retry_count = self._resolve_retry_count()
173
+ self.retry_backoff_seconds: float = 10.0
174
+
175
+ def _initialize_default_params(self, kwargs: dict) -> RequestParams:
176
+ """Initialize default parameters for the LLM.
177
+ Should be overridden by provider implementations to set provider-specific defaults."""
178
+ # Get model-aware default max tokens
179
+ model = kwargs.get("model")
180
+ max_tokens = ModelDatabase.get_default_max_tokens(model)
181
+
182
+ return RequestParams(
183
+ model=model,
184
+ maxTokens=max_tokens,
185
+ systemPrompt=self.instruction,
186
+ parallel_tool_calls=True,
187
+ max_iterations=DEFAULT_MAX_ITERATIONS,
188
+ use_history=True,
189
+ )
190
+
191
+
192
+
193
+ async def _execute_with_retry(
194
+ self,
195
+ func: Callable[..., Awaitable[Any]],
196
+ *args: Any,
197
+ on_final_error: Callable[[Exception], Awaitable[Any] | Any] | None = None,
198
+ **kwargs: Any,
199
+ ) -> Any:
200
+ """
201
+ Executes a function with robust retry logic for transient API errors.
202
+ """
203
+ retries = max(0, int(self.retry_count))
204
+
205
+ def _is_fatal_error(e: Exception) -> bool:
206
+ if isinstance(e, (KeyboardInterrupt, AgentConfigError, ServerConfigError)):
207
+ return True
208
+ if isinstance(e, ProviderKeyError):
209
+ msg = str(e).lower()
210
+ # Retry on Rate Limits (429, Quota, Overloaded)
211
+ keywords = ["429", "503", "quota", "exhausted", "overloaded", "unavailable", "timeout"]
212
+ if any(k in msg for k in keywords):
213
+ return False
214
+ return True
215
+ return False
216
+
217
+ last_error = None
218
+
219
+ for attempt in range(retries + 1):
220
+ try:
221
+ # Await the async function
222
+ return await func(*args, **kwargs)
223
+ except Exception as e:
224
+ if _is_fatal_error(e):
225
+ raise e
226
+
227
+ last_error = e
228
+ if attempt < retries:
229
+ wait_time = self.retry_backoff_seconds * (attempt + 1)
230
+
231
+ # Try to import progress_display safely
232
+ try:
233
+ from fast_agent.ui.progress_display import progress_display
234
+ with progress_display.paused():
235
+ rich_print(f"\n[yellow]⚠ Provider Error: {str(e)[:300]}...[/yellow]")
236
+ rich_print(f"[dim]⟳ Retrying in {wait_time}s... (Attempt {attempt+1}/{retries})[/dim]")
237
+ except ImportError:
238
+ print(f"⚠ Provider Error: {str(e)[:300]}...")
239
+ print(f"⟳ Retrying in {wait_time}s... (Attempt {attempt+1}/{retries})")
240
+
241
+ await asyncio.sleep(wait_time)
242
+
243
+ if last_error:
244
+ handler = on_final_error or getattr(self, "_handle_retry_failure", None)
245
+ if handler:
246
+ handled = handler(last_error)
247
+ if inspect.isawaitable(handled):
248
+ handled = await handled
249
+ if handled is not None:
250
+ return handled
251
+
252
+ raise last_error
253
+
254
+ # This line satisfies Pylance that we never implicitly return None
255
+ raise RuntimeError("Retry loop finished without success or exception")
256
+
257
+ def _handle_retry_failure(self, error: Exception) -> Any | None:
258
+ """
259
+ Optional hook for providers to convert an exhausted retry into a user-facing response.
260
+
261
+ Return a non-None value to short-circuit raising the final exception.
262
+ """
263
+ return None
264
+
265
+ def _resolve_retry_count(self) -> int:
266
+ """Resolve retries from config first, then env, defaulting to 0."""
267
+ config_retries = None
268
+ try:
269
+ config_retries = getattr(self.context.config, "llm_retries", None)
270
+ except Exception:
271
+ config_retries = None
272
+
273
+ if config_retries is not None:
274
+ try:
275
+ return int(config_retries)
276
+ except (TypeError, ValueError):
277
+ pass
278
+
279
+ env_retries = os.getenv("FAST_AGENT_RETRIES")
280
+ if env_retries is not None:
281
+ try:
282
+ return int(env_retries)
283
+ except (TypeError, ValueError):
284
+ pass
285
+
286
+ return 0
287
+
288
+
289
+ async def generate(
290
+ self,
291
+ messages: list[PromptMessageExtended],
292
+ request_params: RequestParams | None = None,
293
+ tools: list[Tool] | None = None,
294
+ ) -> PromptMessageExtended:
295
+ """
296
+ Generate a completion using normalized message lists.
297
+
298
+ This is the primary LLM interface that works directly with
299
+ list[PromptMessageExtended] for efficient internal usage.
300
+
301
+ Args:
302
+ messages: List of PromptMessageExtended objects
303
+ request_params: Optional parameters to configure the LLM request
304
+ tools: Optional list of tools available to the LLM
305
+
306
+ Returns:
307
+ A PromptMessageExtended containing the Assistant response
308
+
309
+ Raises:
310
+ asyncio.CancelledError: If the operation is cancelled via task.cancel()
311
+ """
312
+ # TODO -- create a "fast-agent" control role rather than magic strings
313
+
314
+ if messages[-1].first_text().startswith(CONTROL_MESSAGE_SAVE_HISTORY):
315
+ parts: list[str] = messages[-1].first_text().split(" ", 1)
316
+ if len(parts) > 1:
317
+ filename: str = parts[1].strip()
318
+ else:
319
+ from datetime import datetime
320
+
321
+ timestamp = datetime.now().strftime("%y_%m_%d_%H_%M")
322
+ filename = f"{timestamp}-conversation.json"
323
+ await self._save_history(filename, messages)
324
+ return Prompt.assistant(f"History saved to {filename}")
325
+
326
+ # Store MCP metadata in context variable
327
+ final_request_params = self.get_request_params(request_params)
328
+ if final_request_params.mcp_metadata:
329
+ _mcp_metadata_var.set(final_request_params.mcp_metadata)
330
+
331
+ # The caller supplies the full conversation to send
332
+ full_history = messages
333
+
334
+ # Track timing for this generation
335
+ start_time = time.perf_counter()
336
+ assistant_response: PromptMessageExtended = await self._execute_with_retry(
337
+ self._apply_prompt_provider_specific,
338
+ full_history,
339
+ request_params,
340
+ tools
341
+ )
342
+ end_time = time.perf_counter()
343
+ duration_ms = round((end_time - start_time) * 1000, 2)
344
+
345
+ # Add timing data to channels only if not already present
346
+ # (preserves original timing when loading saved history)
347
+ channels = dict(assistant_response.channels or {})
348
+ if FAST_AGENT_TIMING not in channels:
349
+ timing_data = {
350
+ "start_time": start_time,
351
+ "end_time": end_time,
352
+ "duration_ms": duration_ms,
353
+ }
354
+ channels[FAST_AGENT_TIMING] = [TextContent(type="text", text=json.dumps(timing_data))]
355
+ assistant_response.channels = channels
356
+
357
+ self.usage_accumulator.count_tools(len(assistant_response.tool_calls or {}))
358
+
359
+ return assistant_response
360
+
361
+ @abstractmethod
362
+ async def _apply_prompt_provider_specific(
363
+ self,
364
+ multipart_messages: list["PromptMessageExtended"],
365
+ request_params: RequestParams | None = None,
366
+ tools: list[Tool] | None = None,
367
+ is_template: bool = False,
368
+ ) -> PromptMessageExtended:
369
+ """
370
+ Provider-specific implementation of apply_prompt_template.
371
+ This default implementation handles basic text content for any LLM type.
372
+ Provider-specific subclasses should override this method to handle
373
+ multimodal content appropriately.
374
+
375
+ Args:
376
+ multipart_messages: List of PromptMessageExtended objects parsed from the prompt template
377
+ request_params: Optional parameters to configure the LLM request
378
+ tools: Optional list of tools available to the LLM
379
+ is_template: Whether this is a template application
380
+
381
+ Returns:
382
+ String representation of the assistant's response if generated,
383
+ or the last assistant message in the prompt
384
+ """
385
+
386
+ async def structured(
387
+ self,
388
+ messages: list[PromptMessageExtended],
389
+ model: Type[ModelT],
390
+ request_params: RequestParams | None = None,
391
+ ) -> tuple[ModelT | None, PromptMessageExtended]:
392
+ """
393
+ Generate a structured response using normalized message lists.
394
+
395
+ This is the primary LLM interface for structured output that works directly with
396
+ list[PromptMessageExtended] for efficient internal usage.
397
+
398
+ Args:
399
+ messages: List of PromptMessageExtended objects
400
+ model: The Pydantic model class to parse the response into
401
+ request_params: Optional parameters to configure the LLM request
402
+
403
+ Returns:
404
+ Tuple of (parsed model instance or None, assistant response message)
405
+ """
406
+
407
+ # Store MCP metadata in context variable
408
+ final_request_params = self.get_request_params(request_params)
409
+
410
+ # TODO -- this doesn't need to go here anymore.
411
+ if final_request_params.mcp_metadata:
412
+ _mcp_metadata_var.set(final_request_params.mcp_metadata)
413
+
414
+ full_history = messages
415
+
416
+ # Track timing for this structured generation
417
+ start_time = time.perf_counter()
418
+ result_or_response = await self._execute_with_retry(
419
+ self._apply_prompt_provider_specific_structured,
420
+ full_history,
421
+ model,
422
+ request_params,
423
+ on_final_error=self._handle_retry_failure,
424
+ )
425
+ if isinstance(result_or_response, PromptMessageExtended):
426
+ result, assistant_response = self._structured_from_multipart(result_or_response, model)
427
+ else:
428
+ result, assistant_response = result_or_response
429
+ end_time = time.perf_counter()
430
+ duration_ms = round((end_time - start_time) * 1000, 2)
431
+
432
+ # Add timing data to channels only if not already present
433
+ # (preserves original timing when loading saved history)
434
+ channels = dict(assistant_response.channels or {})
435
+ if FAST_AGENT_TIMING not in channels:
436
+ timing_data = {
437
+ "start_time": start_time,
438
+ "end_time": end_time,
439
+ "duration_ms": duration_ms,
440
+ }
441
+ channels[FAST_AGENT_TIMING] = [TextContent(type="text", text=json.dumps(timing_data))]
442
+ assistant_response.channels = channels
443
+
444
+ return result, assistant_response
445
+
446
+ @staticmethod
447
+ def model_to_response_format(
448
+ model: Type[Any],
449
+ ) -> Any:
450
+ """
451
+ Convert a pydantic model to the appropriate response format schema.
452
+ This allows for reuse in multiple provider implementations.
453
+
454
+ Args:
455
+ model: The pydantic model class to convert to a schema
456
+
457
+ Returns:
458
+ Provider-agnostic schema representation or NotGiven if conversion fails
459
+ """
460
+ return _type_to_response_format(model)
461
+
462
+ @staticmethod
463
+ def model_to_schema_str(
464
+ model: Type[Any],
465
+ ) -> str:
466
+ """
467
+ Convert a pydantic model to a schema string representation.
468
+ This provides a simpler interface for provider implementations
469
+ that need a string representation.
470
+
471
+ Args:
472
+ model: The pydantic model class to convert to a schema
473
+
474
+ Returns:
475
+ Schema as a string, or empty string if conversion fails
476
+ """
477
+ import json
478
+
479
+ try:
480
+ schema = model.model_json_schema()
481
+ return json.dumps(schema)
482
+ except Exception:
483
+ return ""
484
+
485
+ async def _apply_prompt_provider_specific_structured(
486
+ self,
487
+ multipart_messages: list[PromptMessageExtended],
488
+ model: Type[ModelT],
489
+ request_params: RequestParams | None = None,
490
+ ) -> tuple[ModelT | None, PromptMessageExtended]:
491
+ """Base class attempts to parse JSON - subclasses can use provider specific functionality"""
492
+
493
+ request_params = self.get_request_params(request_params)
494
+
495
+ if not request_params.response_format:
496
+ schema = self.model_to_response_format(model)
497
+ if schema is not NotGiven:
498
+ request_params.response_format = schema
499
+
500
+ result: PromptMessageExtended = await self._apply_prompt_provider_specific(
501
+ multipart_messages, request_params
502
+ )
503
+ return self._structured_from_multipart(result, model)
504
+
505
+ def _structured_from_multipart(
506
+ self, message: PromptMessageExtended, model: Type[ModelT]
507
+ ) -> tuple[ModelT | None, PromptMessageExtended]:
508
+ """Parse the content of a PromptMessage and return the structured model and message itself"""
509
+ try:
510
+ text = get_text(message.content[-1]) or ""
511
+ text = self._prepare_structured_text(text)
512
+ json_data = from_json(text, allow_partial=True)
513
+ validated_model = model.model_validate(json_data)
514
+ return cast("ModelT", validated_model), message
515
+ except ValueError as e:
516
+ logger = get_logger(__name__)
517
+ logger.warning(f"Failed to parse structured response: {str(e)}")
518
+ return None, message
519
+
520
+ def _prepare_structured_text(self, text: str) -> str:
521
+ """Hook for subclasses to adjust structured output text before parsing."""
522
+ return text
523
+
524
+ def record_templates(self, templates: list[PromptMessageExtended]) -> None:
525
+ """Hook for providers that need template visibility (e.g., caching)."""
526
+ return
527
+
528
+ def _precall(self, multipart_messages: list[PromptMessageExtended]) -> None:
529
+ """Pre-call hook to modify the message before sending it to the provider."""
530
+ # No-op placeholder; history is managed by the agent
531
+
532
+ def chat_turn(self) -> int:
533
+ """Return the current chat turn number"""
534
+ return 1 + len(self._usage_accumulator.turns)
535
+
536
+ def prepare_provider_arguments(
537
+ self,
538
+ base_args: dict,
539
+ request_params: RequestParams,
540
+ exclude_fields: set | None = None,
541
+ ) -> dict:
542
+ """
543
+ Prepare arguments for provider API calls by merging request parameters.
544
+
545
+ Args:
546
+ base_args: Base arguments dictionary with provider-specific required parameters
547
+ params: The RequestParams object containing all parameters
548
+ exclude_fields: Set of field names to exclude from params. If None, uses BASE_EXCLUDE_FIELDS.
549
+
550
+ Returns:
551
+ Complete arguments dictionary with all applicable parameters
552
+ """
553
+ # Start with base arguments
554
+ arguments = base_args.copy()
555
+
556
+ # Combine base exclusions with provider-specific exclusions
557
+ final_exclude_fields = self.BASE_EXCLUDE_FIELDS.copy()
558
+ if exclude_fields:
559
+ final_exclude_fields.update(exclude_fields)
560
+
561
+ # Add all fields from params that aren't explicitly excluded
562
+ # Ensure model_dump only includes set fields if that's the desired behavior,
563
+ # or adjust exclude_unset=True/False as needed.
564
+ # Default Pydantic v2 model_dump is exclude_unset=False
565
+ params_dict = request_params.model_dump(exclude=final_exclude_fields)
566
+
567
+ for key, value in params_dict.items():
568
+ # Only add if not None and not already in base_args (base_args take precedence)
569
+ # or if None is a valid value for the provider, this logic might need adjustment.
570
+ if value is not None and key not in arguments:
571
+ arguments[key] = value
572
+ elif value is not None and key in arguments and arguments[key] is None:
573
+ # Allow overriding a None in base_args with a set value from params
574
+ arguments[key] = value
575
+
576
+ # Finally, add any metadata fields as a last layer of overrides
577
+ # This ensures metadata can override anything previously set if keys conflict.
578
+ if request_params.metadata:
579
+ arguments.update(request_params.metadata)
580
+
581
+ return arguments
582
+
583
+ def _merge_request_params(
584
+ self, default_params: RequestParams, provided_params: RequestParams
585
+ ) -> RequestParams:
586
+ """Merge default and provided request parameters"""
587
+
588
+ merged = deep_merge(
589
+ default_params.model_dump(),
590
+ provided_params.model_dump(exclude_unset=True),
591
+ )
592
+ final_params = RequestParams(**merged)
593
+
594
+ return final_params
595
+
596
+ def get_request_params(
597
+ self,
598
+ request_params: RequestParams | None = None,
599
+ ) -> RequestParams:
600
+ """
601
+ Get request parameters with merged-in defaults and overrides.
602
+ Args:
603
+ request_params: The request parameters to use as overrides.
604
+ default: The default request parameters to use as the base.
605
+ If unspecified, self.default_request_params will be used.
606
+ """
607
+
608
+ # If user provides overrides, merge them with defaults
609
+ if request_params:
610
+ return self._merge_request_params(self.default_request_params, request_params)
611
+
612
+ return self.default_request_params.model_copy()
613
+
614
+ @classmethod
615
+ def convert_message_to_message_param(
616
+ cls, message: MessageT, **kwargs: dict[str, Any]
617
+ ) -> MessageParamT:
618
+ """Convert a response object to an input parameter object to allow LLM calls to be chained."""
619
+ # Many LLM implementations will allow the same type for input and output messages
620
+ return cast("MessageParamT", message)
621
+
622
+ def _finalize_turn_usage(self, turn_usage: "TurnUsage") -> None:
623
+ """Set tool call count on TurnUsage and add to accumulator."""
624
+ self._usage_accumulator.add_turn(turn_usage)
625
+
626
+ def _log_chat_progress(self, chat_turn: int | None = None, model: str | None = None) -> None:
627
+ """Log a chat progress event"""
628
+ # Determine action type based on verb
629
+ if hasattr(self, "verb") and self.verb:
630
+ # Use verb directly regardless of type
631
+ act = self.verb
632
+ else:
633
+ act = ProgressAction.CHATTING
634
+
635
+ data = {
636
+ "progress_action": act,
637
+ "model": model,
638
+ "agent_name": self.name,
639
+ "chat_turn": chat_turn if chat_turn is not None else None,
640
+ }
641
+ self.logger.debug("Chat in progress", data=data)
642
+
643
+ def _update_streaming_progress(self, content: str, model: str, estimated_tokens: int) -> int:
644
+ """Update streaming progress with token estimation and formatting.
645
+
646
+ Args:
647
+ content: The text content from the streaming event
648
+ model: The model name
649
+ estimated_tokens: Current token count to update
650
+
651
+ Returns:
652
+ Updated estimated token count
653
+ """
654
+ # Rough estimate: 1 token per 4 characters (OpenAI's typical ratio)
655
+ text_length = len(content)
656
+ additional_tokens = max(1, text_length // 4)
657
+ new_total = estimated_tokens + additional_tokens
658
+
659
+ # Format token count for display
660
+ token_str = str(new_total).rjust(5)
661
+
662
+ # Emit progress event
663
+ data = {
664
+ "progress_action": ProgressAction.STREAMING,
665
+ "model": model,
666
+ "agent_name": self.name,
667
+ "chat_turn": self.chat_turn(),
668
+ "details": token_str.strip(), # Token count goes in details for STREAMING action
669
+ }
670
+ self.logger.info("Streaming progress", data=data)
671
+
672
+ return new_total
673
+
674
+ def add_stream_listener(self, listener: Callable[[StreamChunk], None]) -> Callable[[], None]:
675
+ """
676
+ Register a callback invoked with streaming text chunks.
677
+
678
+ Args:
679
+ listener: Callable receiving a StreamChunk emitted by the provider.
680
+
681
+ Returns:
682
+ A function that removes the listener when called.
683
+ """
684
+ self._stream_listeners.add(listener)
685
+
686
+ def remove() -> None:
687
+ self._stream_listeners.discard(listener)
688
+
689
+ return remove
690
+
691
+ def _notify_stream_listeners(self, chunk: StreamChunk) -> None:
692
+ """Notify registered listeners with a streaming chunk."""
693
+ if not chunk.text:
694
+ return
695
+ for listener in list(self._stream_listeners):
696
+ try:
697
+ listener(chunk)
698
+ except Exception:
699
+ self.logger.exception("Stream listener raised an exception")
700
+
701
+ def add_tool_stream_listener(
702
+ self, listener: Callable[[str, dict[str, Any] | None], None]
703
+ ) -> Callable[[], None]:
704
+ """Register a callback invoked with tool streaming events.
705
+
706
+ Args:
707
+ listener: Callable receiving event_type (str) and optional info dict.
708
+
709
+ Returns:
710
+ A function that removes the listener when called.
711
+ """
712
+
713
+ self._tool_stream_listeners.add(listener)
714
+
715
+ def remove() -> None:
716
+ self._tool_stream_listeners.discard(listener)
717
+
718
+ return remove
719
+
720
+ def _notify_tool_stream_listeners(
721
+ self, event_type: str, payload: dict[str, Any] | None = None
722
+ ) -> None:
723
+ """Notify listeners about tool streaming lifecycle events."""
724
+
725
+ data = payload or {}
726
+ for listener in list(self._tool_stream_listeners):
727
+ try:
728
+ listener(event_type, data)
729
+ except Exception:
730
+ self.logger.exception("Tool stream listener raised an exception")
731
+
732
+ def _log_chat_finished(self, model: str | None = None) -> None:
733
+ """Log a chat finished event"""
734
+ data = {
735
+ "progress_action": ProgressAction.READY,
736
+ "model": model,
737
+ "agent_name": self.name,
738
+ }
739
+ self.logger.debug("Chat finished", data=data)
740
+
741
+ def _convert_prompt_messages(self, prompt_messages: list[PromptMessage]) -> list[MessageParamT]:
742
+ """
743
+ Convert prompt messages to this LLM's specific message format.
744
+ To be implemented by concrete LLM classes.
745
+ """
746
+ raise NotImplementedError("Must be implemented by subclass")
747
+
748
+ def _convert_to_provider_format(
749
+ self, messages: list[PromptMessageExtended]
750
+ ) -> list[MessageParamT]:
751
+ """
752
+ Convert provided messages to provider-specific format.
753
+ Called fresh on EVERY API call - no caching.
754
+
755
+ Args:
756
+ messages: List of PromptMessageExtended
757
+
758
+ Returns:
759
+ List of provider-specific message objects
760
+ """
761
+ return self._convert_extended_messages_to_provider(messages)
762
+
763
+ @abstractmethod
764
+ def _convert_extended_messages_to_provider(
765
+ self, messages: list[PromptMessageExtended]
766
+ ) -> list[MessageParamT]:
767
+ """
768
+ Convert PromptMessageExtended list to provider-specific format.
769
+ Must be implemented by each provider.
770
+
771
+ Args:
772
+ messages: List of PromptMessageExtended objects
773
+
774
+ Returns:
775
+ List of provider-specific message parameter objects
776
+ """
777
+ raise NotImplementedError("Must be implemented by subclass")
778
+
779
+ async def show_prompt_loaded(
780
+ self,
781
+ prompt_name: str,
782
+ description: str | None = None,
783
+ message_count: int = 0,
784
+ arguments: dict[str, str] | None = None,
785
+ ) -> None:
786
+ """
787
+ Display information about a loaded prompt template.
788
+
789
+ Args:
790
+ prompt_name: The name of the prompt
791
+ description: Optional description of the prompt
792
+ message_count: Number of messages in the prompt
793
+ arguments: Optional dictionary of arguments passed to the prompt
794
+ """
795
+ await self.display.show_prompt_loaded(
796
+ prompt_name=prompt_name,
797
+ description=description,
798
+ message_count=message_count,
799
+ agent_name=self.name,
800
+ arguments=arguments,
801
+ )
802
+
803
+ async def apply_prompt_template(self, prompt_result: GetPromptResult, prompt_name: str) -> str:
804
+ """
805
+ Apply a prompt template by adding it to the conversation history.
806
+ If the last message in the prompt is from a user, automatically
807
+ generate an assistant response.
808
+
809
+ Args:
810
+ prompt_result: The GetPromptResult containing prompt messages
811
+ prompt_name: The name of the prompt being applied
812
+
813
+ Returns:
814
+ String representation of the assistant's response if generated,
815
+ or the last assistant message in the prompt
816
+ """
817
+ from fast_agent.types import PromptMessageExtended
818
+
819
+ # Check if we have any messages
820
+ if not prompt_result.messages:
821
+ return "Prompt contains no messages"
822
+
823
+ # Extract arguments if they were stored in the result
824
+ arguments = getattr(prompt_result, "arguments", None)
825
+
826
+ # Display information about the loaded prompt
827
+ await self.show_prompt_loaded(
828
+ prompt_name=prompt_name,
829
+ description=prompt_result.description,
830
+ message_count=len(prompt_result.messages),
831
+ arguments=arguments,
832
+ )
833
+
834
+ # Convert to PromptMessageExtended objects and delegate
835
+ multipart_messages = PromptMessageExtended.parse_get_prompt_result(prompt_result)
836
+ result = await self._apply_prompt_provider_specific(
837
+ multipart_messages, None, is_template=True
838
+ )
839
+ return result.first_text()
840
+
841
+ async def _save_history(self, filename: str, messages: list[PromptMessageExtended]) -> None:
842
+ """
843
+ Save the Message History to a file in a format determined by the file extension.
844
+
845
+ Uses JSON format for .json files (MCP SDK compatible format) and
846
+ delimited text format for other extensions.
847
+ """
848
+ from fast_agent.mcp.prompt_serialization import save_messages
849
+
850
+ # Drop control messages like ***SAVE_HISTORY before persisting
851
+ filtered = [
852
+ msg.model_copy(deep=True)
853
+ for msg in messages
854
+ if not msg.first_text().startswith(CONTROL_MESSAGE_SAVE_HISTORY)
855
+ ]
856
+
857
+ # Save messages using the unified save function that auto-detects format
858
+ save_messages(filtered, filename)
859
+
860
+ @property
861
+ def message_history(self) -> list[PromptMessageExtended]:
862
+ """
863
+ Return the agent's message history as PromptMessageExtended objects.
864
+
865
+ This history can be used to transfer state between agents or for
866
+ analysis and debugging purposes.
867
+
868
+ Returns:
869
+ List of PromptMessageExtended objects representing the conversation history
870
+ """
871
+ return []
872
+
873
+ def pop_last_message(self) -> PromptMessageExtended | None:
874
+ """Remove and return the most recent message from the conversation history."""
875
+ return None
876
+
877
+ def clear(self, *, clear_prompts: bool = False) -> None:
878
+ """Reset stored message history while optionally retaining prompt templates."""
879
+
880
+ self.history.clear(clear_prompts=clear_prompts)
881
+
882
+ def _api_key(self):
883
+ if self._init_api_key:
884
+ return self._init_api_key
885
+
886
+ from fast_agent.llm.provider_key_manager import ProviderKeyManager
887
+
888
+ assert self.provider
889
+ return ProviderKeyManager.get_api_key(self.provider.value, self.context.config)
890
+
891
+ @property
892
+ def usage_accumulator(self):
893
+ return self._usage_accumulator
894
+
895
+ def get_usage_summary(self) -> dict:
896
+ """
897
+ Get a summary of usage statistics for this LLM instance.
898
+
899
+ Returns:
900
+ Dictionary containing usage statistics including tokens, cache metrics,
901
+ and context window utilization.
902
+ """
903
+ return self._usage_accumulator.get_summary()
904
+
905
+ @property
906
+ def provider(self) -> Provider:
907
+ """
908
+ Return the LLM provider type.
909
+
910
+ Returns:
911
+ The Provider enum value representing the LLM provider
912
+ """
913
+ return self._provider
914
+
915
+ @property
916
+ def model_name(self) -> str | None:
917
+ """Return the effective model name, if set."""
918
+ return self._model_name
919
+
920
+ @property
921
+ def model_info(self):
922
+ """Return resolved model information with capabilities.
923
+
924
+ Uses a lightweight resolver backed by the ModelDatabase and provides
925
+ text/document/vision flags, context window, etc.
926
+ """
927
+ from fast_agent.llm.model_info import ModelInfo
928
+
929
+ if not self._model_name:
930
+ return None
931
+ return ModelInfo.from_name(self._model_name, self._provider)