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,98 @@
1
+ """
2
+ Simplified converter between MCP sampling types and PromptMessageExtended.
3
+ This replaces the more complex provider-specific converters with direct conversions.
4
+ """
5
+
6
+
7
+ from mcp.types import (
8
+ CreateMessageRequestParams,
9
+ CreateMessageResult,
10
+ SamplingMessage,
11
+ TextContent,
12
+ )
13
+
14
+ from fast_agent.types import PromptMessageExtended, RequestParams
15
+ from fast_agent.types.llm_stop_reason import LlmStopReason
16
+
17
+
18
+ class SamplingConverter:
19
+ """
20
+ Simplified converter between MCP sampling types and internal LLM types.
21
+
22
+ This handles converting between:
23
+ - SamplingMessage and PromptMessageExtended
24
+ - CreateMessageRequestParams and RequestParams
25
+ - LLM responses and CreateMessageResult
26
+ """
27
+
28
+ @staticmethod
29
+ def sampling_message_to_prompt_message(
30
+ message: SamplingMessage,
31
+ ) -> PromptMessageExtended:
32
+ """
33
+ Convert a SamplingMessage to a PromptMessageExtended.
34
+
35
+ Args:
36
+ message: MCP SamplingMessage to convert
37
+
38
+ Returns:
39
+ PromptMessageExtended suitable for use with LLMs
40
+ """
41
+ return PromptMessageExtended(role=message.role, content=[message.content])
42
+
43
+ @staticmethod
44
+ def extract_request_params(params: CreateMessageRequestParams) -> RequestParams:
45
+ """
46
+ Extract parameters from CreateMessageRequestParams into RequestParams.
47
+
48
+ Args:
49
+ params: MCP request parameters
50
+
51
+ Returns:
52
+ RequestParams suitable for use with LLM.generate_prompt
53
+ """
54
+ return RequestParams(
55
+ maxTokens=params.maxTokens,
56
+ systemPrompt=params.systemPrompt,
57
+ temperature=params.temperature,
58
+ stopSequences=params.stopSequences,
59
+ modelPreferences=params.modelPreferences,
60
+ # Add any other parameters needed
61
+ )
62
+
63
+ @staticmethod
64
+ def error_result(error_message: str, model: str | None = None) -> CreateMessageResult:
65
+ """
66
+ Create an error result.
67
+
68
+ Args:
69
+ error_message: Error message text
70
+ model: Optional model identifier
71
+
72
+ Returns:
73
+ CreateMessageResult with error information
74
+ """
75
+ return CreateMessageResult(
76
+ role="assistant",
77
+ content=TextContent(type="text", text=error_message),
78
+ model=model or "unknown",
79
+ stopReason=LlmStopReason.ERROR.value,
80
+ )
81
+
82
+ @staticmethod
83
+ def convert_messages(
84
+ messages: list[SamplingMessage],
85
+ ) -> list[PromptMessageExtended]:
86
+ """
87
+ Convert multiple SamplingMessages to PromptMessageExtended objects.
88
+
89
+ This properly combines consecutive messages with the same role into a single
90
+ multipart message, which is required by APIs like Anthropic.
91
+
92
+ Args:
93
+ messages: List of SamplingMessages to convert
94
+
95
+ Returns:
96
+ List of PromptMessageExtended objects with consecutive same-role messages combined
97
+ """
98
+ return [SamplingConverter.sampling_message_to_prompt_message(msg) for msg in messages]
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class StreamChunk:
6
+ """Typed streaming chunk emitted by providers."""
7
+
8
+ text: str
9
+ is_reasoning: bool = False
@@ -0,0 +1,445 @@
1
+ """
2
+ Usage tracking system for LLM providers with comprehensive cache support.
3
+
4
+ This module provides unified usage tracking across Anthropic, OpenAI, and Google providers,
5
+ including detailed cache metrics and context window management.
6
+ """
7
+
8
+ import time
9
+ from typing import Union
10
+
11
+ # Proper type imports for each provider
12
+ try:
13
+ from anthropic.types import Usage as AnthropicUsage
14
+ except Exception: # pragma: no cover - optional dependency
15
+ AnthropicUsage = object # type: ignore
16
+
17
+ try:
18
+ from google.genai.types import GenerateContentResponseUsageMetadata as GoogleUsage
19
+ except Exception: # pragma: no cover - optional dependency
20
+ GoogleUsage = object # type: ignore
21
+
22
+ try:
23
+ from openai.types.completion_usage import CompletionUsage as OpenAIUsage
24
+ except Exception: # pragma: no cover - optional dependency
25
+ OpenAIUsage = object # type: ignore
26
+ from pydantic import BaseModel, Field, computed_field
27
+
28
+ from fast_agent.llm.model_database import ModelDatabase
29
+ from fast_agent.llm.provider_types import Provider
30
+
31
+
32
+ # Fast-agent specific usage type for synthetic providers
33
+ class FastAgentUsage(BaseModel):
34
+ """Usage data for fast-agent providers (passthrough, playback, slow)"""
35
+
36
+ input_chars: int = Field(description="Characters in input messages")
37
+ output_chars: int = Field(description="Characters in output messages")
38
+ model_type: str = Field(description="Type of fast-agent model (passthrough/playbook/slow)")
39
+ tool_calls: int = Field(default=0, description="Number of tool calls made")
40
+ delay_seconds: float = Field(default=0.0, description="Artificial delays added")
41
+
42
+
43
+ # Union type for raw usage data from any provider
44
+ ProviderUsage = Union[AnthropicUsage, OpenAIUsage, GoogleUsage, FastAgentUsage]
45
+
46
+
47
+ class ModelContextWindows:
48
+ """Context window sizes and cache configurations for various models"""
49
+
50
+ @classmethod
51
+ def get_context_window(cls, model: str) -> int | None:
52
+ return ModelDatabase.get_context_window(model)
53
+
54
+
55
+ class CacheUsage(BaseModel):
56
+ """Cache-specific usage metrics"""
57
+
58
+ cache_read_tokens: int = Field(default=0, description="Tokens read from cache")
59
+ cache_write_tokens: int = Field(default=0, description="Tokens written to cache")
60
+ cache_hit_tokens: int = Field(default=0, description="Total tokens served from cache")
61
+
62
+ @computed_field
63
+ @property
64
+ def total_cache_tokens(self) -> int:
65
+ """Total cache-related tokens"""
66
+ return self.cache_read_tokens + self.cache_write_tokens + self.cache_hit_tokens
67
+
68
+ @computed_field
69
+ @property
70
+ def has_cache_activity(self) -> bool:
71
+ """Whether any cache activity occurred"""
72
+ return self.total_cache_tokens > 0
73
+
74
+
75
+ class TurnUsage(BaseModel):
76
+ """Usage data for a single turn/completion with cache support"""
77
+
78
+ provider: Provider
79
+ model: str
80
+ input_tokens: int
81
+ output_tokens: int
82
+ total_tokens: int
83
+ timestamp: float = Field(default_factory=time.time)
84
+
85
+ # Cache-specific metrics
86
+ cache_usage: CacheUsage = Field(default_factory=CacheUsage)
87
+
88
+ # Provider-specific token types
89
+ tool_use_tokens: int = Field(default=0, description="Tokens used for tool calling prompts")
90
+ reasoning_tokens: int = Field(default=0, description="Tokens used for reasoning/thinking")
91
+
92
+ # Tool call count for this turn
93
+ tool_calls: int = Field(default=0, description="Number of tool calls made in this turn")
94
+
95
+ # Raw usage data from provider (preserves all original data)
96
+ raw_usage: ProviderUsage
97
+
98
+ @computed_field
99
+ @property
100
+ def current_context_tokens(self) -> int:
101
+ """Current context size after this turn (total input including cache + output)"""
102
+ # For Anthropic: input_tokens + cache_read_tokens represents total input context
103
+ total_input = (
104
+ self.input_tokens
105
+ + self.cache_usage.cache_read_tokens
106
+ + self.cache_usage.cache_write_tokens
107
+ )
108
+ return total_input + self.output_tokens
109
+
110
+ @computed_field
111
+ @property
112
+ def effective_input_tokens(self) -> int:
113
+ """Input tokens actually processed (new tokens, not from cache)"""
114
+ # For Anthropic: input_tokens already excludes cached content
115
+ # For other providers: subtract cache hits from input_tokens
116
+ if self.provider == Provider.ANTHROPIC:
117
+ return self.input_tokens
118
+ else:
119
+ return max(0, self.input_tokens - self.cache_usage.cache_hit_tokens)
120
+
121
+ @computed_field
122
+ @property
123
+ def display_input_tokens(self) -> int:
124
+ """Input tokens to display for 'Last turn' (total submitted tokens)"""
125
+ # For Anthropic: input_tokens excludes cache, so add cache tokens
126
+ if self.provider == Provider.ANTHROPIC:
127
+ return (
128
+ self.input_tokens
129
+ + self.cache_usage.cache_read_tokens
130
+ + self.cache_usage.cache_write_tokens
131
+ )
132
+ else:
133
+ # For OpenAI/Google: input_tokens already includes cached tokens
134
+ return self.input_tokens
135
+
136
+ def set_tool_calls(self, count: int) -> None:
137
+ """Set the number of tool calls made in this turn"""
138
+ # Use object.__setattr__ since this is a Pydantic model
139
+ object.__setattr__(self, "tool_calls", count)
140
+
141
+ @classmethod
142
+ def from_anthropic(cls, usage: AnthropicUsage, model: str) -> "TurnUsage":
143
+ # Extract cache tokens with proper null handling
144
+ cache_creation_tokens = getattr(usage, "cache_creation_input_tokens", 0) or 0
145
+ cache_read_tokens = getattr(usage, "cache_read_input_tokens", 0) or 0
146
+
147
+ cache_usage = CacheUsage(
148
+ cache_read_tokens=cache_read_tokens, # Tokens read from cache (90% discount)
149
+ cache_write_tokens=cache_creation_tokens, # Tokens written to cache (25% surcharge)
150
+ )
151
+
152
+ return cls(
153
+ provider=Provider.ANTHROPIC,
154
+ model=model,
155
+ input_tokens=usage.input_tokens,
156
+ output_tokens=usage.output_tokens,
157
+ total_tokens=usage.input_tokens + usage.output_tokens,
158
+ cache_usage=cache_usage,
159
+ raw_usage=usage, # Store the original Anthropic usage object
160
+ )
161
+
162
+ @classmethod
163
+ def from_openai(cls, usage: OpenAIUsage, model: str) -> "TurnUsage":
164
+ # Extract cache tokens with proper null handling
165
+ cached_tokens = 0
166
+ if hasattr(usage, "prompt_tokens_details") and usage.prompt_tokens_details:
167
+ cached_tokens = getattr(usage.prompt_tokens_details, "cached_tokens", 0) or 0
168
+
169
+ cache_usage = CacheUsage(
170
+ cache_hit_tokens=cached_tokens # These are tokens served from cache (50% discount)
171
+ )
172
+
173
+ return cls(
174
+ provider=Provider.OPENAI,
175
+ model=model,
176
+ input_tokens=usage.prompt_tokens,
177
+ output_tokens=usage.completion_tokens,
178
+ total_tokens=usage.total_tokens,
179
+ cache_usage=cache_usage,
180
+ raw_usage=usage, # Store the original OpenAI usage object
181
+ )
182
+
183
+ @classmethod
184
+ def from_google(cls, usage: GoogleUsage, model: str) -> "TurnUsage":
185
+ # Extract token counts with proper null handling
186
+ prompt_tokens = getattr(usage, "prompt_token_count", 0) or 0
187
+ candidates_tokens = getattr(usage, "candidates_token_count", 0) or 0
188
+ total_tokens = getattr(usage, "total_token_count", 0) or 0
189
+ cached_content_tokens = getattr(usage, "cached_content_token_count", 0) or 0
190
+
191
+ # Extract additional Google-specific token types
192
+ tool_use_tokens = getattr(usage, "tool_use_prompt_token_count", 0) or 0
193
+ thinking_tokens = getattr(usage, "thoughts_token_count", 0) or 0
194
+
195
+ # Google cache tokens are read hits (75% discount on Gemini 2.5)
196
+ cache_usage = CacheUsage(cache_hit_tokens=cached_content_tokens)
197
+
198
+ return cls(
199
+ provider=Provider.GOOGLE,
200
+ model=model,
201
+ input_tokens=prompt_tokens,
202
+ output_tokens=candidates_tokens,
203
+ total_tokens=total_tokens,
204
+ cache_usage=cache_usage,
205
+ tool_use_tokens=tool_use_tokens,
206
+ reasoning_tokens=thinking_tokens,
207
+ raw_usage=usage, # Store the original Google usage object
208
+ )
209
+
210
+ @classmethod
211
+ def from_fast_agent(cls, usage: FastAgentUsage, model: str) -> "TurnUsage":
212
+ # For fast-agent providers, we use characters as "tokens"
213
+ # This provides a consistent unit of measurement across all providers
214
+ input_tokens = usage.input_chars
215
+ output_tokens = usage.output_chars
216
+ total_tokens = input_tokens + output_tokens
217
+
218
+ # Fast-agent providers don't have cache functionality
219
+ cache_usage = CacheUsage()
220
+
221
+ return cls(
222
+ provider=Provider.FAST_AGENT,
223
+ model=model,
224
+ input_tokens=input_tokens,
225
+ output_tokens=output_tokens,
226
+ total_tokens=total_tokens,
227
+ cache_usage=cache_usage,
228
+ raw_usage=usage, # Store the original FastAgentUsage object
229
+ )
230
+
231
+
232
+ class UsageAccumulator(BaseModel):
233
+ """Accumulates usage data across multiple turns with cache analytics"""
234
+
235
+ turns: list[TurnUsage] = Field(default_factory=list)
236
+ model: str | None = None
237
+
238
+ def add_turn(self, turn: TurnUsage) -> None:
239
+ """Add a new turn to the accumulator"""
240
+ self.turns.append(turn)
241
+ if self.model is None:
242
+ self.model = turn.model
243
+
244
+ # add tool call count to the last turn (if present)
245
+ # not ideal way to do it, but works well enough. full history would be available through the
246
+ # message_history; maybe we consolidate there and put turn_usage on the turn.
247
+ def count_tools(self, tool_calls: int) -> None:
248
+ if self.turns and self.turns[-1]:
249
+ self.turns[-1].tool_calls = tool_calls
250
+
251
+ @computed_field
252
+ @property
253
+ def cumulative_input_tokens(self) -> int:
254
+ """Total input tokens charged across all turns (including cache tokens)"""
255
+ return sum(
256
+ turn.input_tokens
257
+ + turn.cache_usage.cache_read_tokens
258
+ + turn.cache_usage.cache_write_tokens
259
+ for turn in self.turns
260
+ )
261
+
262
+ @computed_field
263
+ @property
264
+ def cumulative_output_tokens(self) -> int:
265
+ """Total output tokens charged across all turns"""
266
+ return sum(turn.output_tokens for turn in self.turns)
267
+
268
+ @computed_field
269
+ @property
270
+ def cumulative_billing_tokens(self) -> int:
271
+ """Total tokens charged across all turns (including cache tokens)"""
272
+ return self.cumulative_input_tokens + self.cumulative_output_tokens
273
+
274
+ @computed_field
275
+ @property
276
+ def cumulative_cache_read_tokens(self) -> int:
277
+ """Total tokens read from cache across all turns"""
278
+ return sum(turn.cache_usage.cache_read_tokens for turn in self.turns)
279
+
280
+ @computed_field
281
+ @property
282
+ def cumulative_cache_write_tokens(self) -> int:
283
+ """Total tokens written to cache across all turns"""
284
+ return sum(turn.cache_usage.cache_write_tokens for turn in self.turns)
285
+
286
+ @computed_field
287
+ @property
288
+ def cumulative_tool_calls(self) -> int:
289
+ """Total tool calls made across all turns"""
290
+ return sum(turn.tool_calls for turn in self.turns)
291
+
292
+ @computed_field
293
+ @property
294
+ def cumulative_cache_hit_tokens(self) -> int:
295
+ """Total tokens served from cache across all turns"""
296
+ return sum(turn.cache_usage.cache_hit_tokens for turn in self.turns)
297
+
298
+ @computed_field
299
+ @property
300
+ def cumulative_effective_input_tokens(self) -> int:
301
+ """Total input tokens excluding cache reads across all turns"""
302
+ return sum(turn.effective_input_tokens for turn in self.turns)
303
+
304
+ @computed_field
305
+ @property
306
+ def cumulative_tool_use_tokens(self) -> int:
307
+ """Total tokens used for tool calling prompts across all turns"""
308
+ return sum(turn.tool_use_tokens for turn in self.turns)
309
+
310
+ @computed_field
311
+ @property
312
+ def cumulative_reasoning_tokens(self) -> int:
313
+ """Total tokens used for reasoning/thinking across all turns"""
314
+ return sum(turn.reasoning_tokens for turn in self.turns)
315
+
316
+ @computed_field
317
+ @property
318
+ def cache_hit_rate(self) -> float | None:
319
+ """Percentage of total input context served from cache"""
320
+ cache_tokens = self.cumulative_cache_read_tokens + self.cumulative_cache_hit_tokens
321
+ total_input_context = self.cumulative_input_tokens + cache_tokens
322
+ if total_input_context == 0:
323
+ return None
324
+ return (cache_tokens / total_input_context) * 100
325
+
326
+ @computed_field
327
+ @property
328
+ def current_context_tokens(self) -> int:
329
+ """Current context usage (last turn's context tokens)"""
330
+ if not self.turns:
331
+ return 0
332
+ return self.turns[-1].current_context_tokens
333
+
334
+ @computed_field
335
+ @property
336
+ def context_window_size(self) -> int | None:
337
+ """Get context window size for current model"""
338
+ if self.model:
339
+ return ModelContextWindows.get_context_window(self.model)
340
+ return None
341
+
342
+ @computed_field
343
+ @property
344
+ def context_usage_percentage(self) -> float | None:
345
+ """Percentage of context window used"""
346
+ window_size = self.context_window_size
347
+ if window_size and window_size > 0:
348
+ return (self.current_context_tokens / window_size) * 100
349
+ return None
350
+
351
+ @computed_field
352
+ @property
353
+ def turn_count(self) -> int:
354
+ """Number of turns accumulated"""
355
+ return len(self.turns)
356
+
357
+ def get_cache_summary(self) -> dict[str, Union[int, float, None]]:
358
+ """Get cache-specific metrics summary"""
359
+ return {
360
+ "cumulative_cache_read_tokens": self.cumulative_cache_read_tokens,
361
+ "cumulative_cache_write_tokens": self.cumulative_cache_write_tokens,
362
+ "cumulative_cache_hit_tokens": self.cumulative_cache_hit_tokens,
363
+ "cache_hit_rate_percent": self.cache_hit_rate,
364
+ "cumulative_effective_input_tokens": self.cumulative_effective_input_tokens,
365
+ }
366
+
367
+ def get_summary(self) -> dict[str, Union[int, float, str, None]]:
368
+ """Get comprehensive usage statistics"""
369
+ cache_summary = self.get_cache_summary()
370
+ return {
371
+ "model": self.model,
372
+ "turn_count": self.turn_count,
373
+ "cumulative_input_tokens": self.cumulative_input_tokens,
374
+ "cumulative_output_tokens": self.cumulative_output_tokens,
375
+ "cumulative_billing_tokens": self.cumulative_billing_tokens,
376
+ "cumulative_tool_use_tokens": self.cumulative_tool_use_tokens,
377
+ "cumulative_reasoning_tokens": self.cumulative_reasoning_tokens,
378
+ "cumulative_tool_calls": self.cumulative_tool_calls,
379
+ "current_context_tokens": self.current_context_tokens,
380
+ "context_window_size": self.context_window_size,
381
+ "context_usage_percentage": self.context_usage_percentage,
382
+ **cache_summary,
383
+ }
384
+
385
+
386
+ # Utility functions for fast-agent integration
387
+ def create_fast_agent_usage(
388
+ input_content: str,
389
+ output_content: str,
390
+ model_type: str,
391
+ tool_calls: int = 0,
392
+ delay_seconds: float = 0.0,
393
+ ) -> FastAgentUsage:
394
+ """
395
+ Create FastAgentUsage from message content.
396
+
397
+ Args:
398
+ input_content: Input message content
399
+ output_content: Output message content
400
+ model_type: Type of fast-agent model (passthrough/playback/slow)
401
+ tool_calls: Number of tool calls made
402
+ delay_seconds: Artificial delays added
403
+
404
+ Returns:
405
+ FastAgentUsage object with character counts
406
+ """
407
+ return FastAgentUsage(
408
+ input_chars=len(input_content),
409
+ output_chars=len(output_content),
410
+ model_type=model_type,
411
+ tool_calls=tool_calls,
412
+ delay_seconds=delay_seconds,
413
+ )
414
+
415
+
416
+ def create_turn_usage_from_messages(
417
+ input_content: str,
418
+ output_content: str,
419
+ model: str,
420
+ model_type: str,
421
+ tool_calls: int = 0,
422
+ delay_seconds: float = 0.0,
423
+ ) -> TurnUsage:
424
+ """
425
+ Create TurnUsage directly from message content for fast-agent providers.
426
+
427
+ Args:
428
+ input_content: Input message content
429
+ output_content: Output message content
430
+ model: Model name (e.g., "passthrough", "playback", "slow")
431
+ model_type: Type for internal tracking
432
+ tool_calls: Number of tool calls made
433
+ delay_seconds: Artificial delays added
434
+
435
+ Returns:
436
+ TurnUsage object ready for accumulation
437
+ """
438
+ usage = create_fast_agent_usage(
439
+ input_content=input_content,
440
+ output_content=output_content,
441
+ model_type=model_type,
442
+ tool_calls=tool_calls,
443
+ delay_seconds=delay_seconds,
444
+ )
445
+ return TurnUsage.from_fast_agent(usage, model)
@@ -0,0 +1,56 @@
1
+ """
2
+ MCP utilities and types for fast-agent.
3
+
4
+ Public API:
5
+ - `Prompt`: helper for constructing MCP prompts/messages.
6
+ - `PromptMessageExtended`: canonical message container used internally by providers.
7
+ - Helpers from `fast_agent.mcp.helpers` (re-exported for convenience).
8
+
9
+ Note: Backward compatibility for legacy `PromptMessageMultipart` imports is handled
10
+ via `fast_agent.mcp.prompt_message_multipart`, which subclasses `PromptMessageExtended`.
11
+ """
12
+
13
+ from .common import SEP
14
+ from .helpers import (
15
+ ensure_multipart_messages,
16
+ get_image_data,
17
+ get_resource_text,
18
+ get_resource_uri,
19
+ get_text,
20
+ is_image_content,
21
+ is_resource_content,
22
+ is_resource_link,
23
+ is_text_content,
24
+ normalize_to_extended_list,
25
+ split_thinking_content,
26
+ text_content,
27
+ )
28
+
29
+ __all__ = [
30
+ "Prompt",
31
+ # Common
32
+ "SEP",
33
+ # Helpers
34
+ "get_text",
35
+ "get_image_data",
36
+ "get_resource_uri",
37
+ "is_text_content",
38
+ "is_image_content",
39
+ "is_resource_content",
40
+ "is_resource_link",
41
+ "get_resource_text",
42
+ "ensure_multipart_messages",
43
+ "normalize_to_extended_list",
44
+ "split_thinking_content",
45
+ "text_content",
46
+ ]
47
+
48
+
49
+ def __getattr__(name: str):
50
+ # Lazily import to avoid circular imports with fast_agent.types
51
+ if name == "Prompt":
52
+ from .prompt import Prompt # local import
53
+
54
+ return Prompt
55
+
56
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@@ -0,0 +1,26 @@
1
+ """
2
+ Common constants and utilities shared between modules to avoid circular imports.
3
+ """
4
+
5
+ # Constants
6
+ SEP = "__"
7
+
8
+
9
+ def create_namespaced_name(server_name: str, resource_name: str) -> str:
10
+ """Create a namespaced resource name from server and resource names"""
11
+ return f"{server_name}{SEP}{resource_name}"[:64]
12
+
13
+
14
+ def is_namespaced_name(name: str) -> bool:
15
+ """Check if a name is already namespaced"""
16
+ return SEP in name
17
+
18
+
19
+ def get_server_name(namespaced_name: str) -> str:
20
+ """Extract the server name from a namespaced resource name"""
21
+ return namespaced_name.split(SEP)[0] if SEP in namespaced_name else ""
22
+
23
+
24
+ def get_resource_name(namespaced_name: str) -> str:
25
+ """Extract the resource name from a namespaced resource name"""
26
+ return namespaced_name.split(SEP, 1)[1] if SEP in namespaced_name else namespaced_name