fast-agent-mcp 0.3.16__py3-none-any.whl → 0.3.17__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.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

@@ -157,7 +157,7 @@ class McpAgent(ABC, ToolAgent):
157
157
  if skills_configured:
158
158
  modes.append("skills")
159
159
  if shell_flag_requested:
160
- modes.append("command switch")
160
+ modes.append("switch")
161
161
  self._shell_access_modes = tuple(modes)
162
162
  self._bash_tool = self._shell_runtime.tool
163
163
  if self._shell_runtime_enabled:
@@ -21,6 +21,8 @@ GO_SPECIFIC_OPTIONS = {
21
21
  "-c",
22
22
  "--shell",
23
23
  "-x",
24
+ "--skills",
25
+ "--skills-dir",
24
26
  }
25
27
 
26
28
  # Known subcommands that should not trigger auto-routing
fast_agent/cli/main.py CHANGED
@@ -8,7 +8,7 @@ from fast_agent.cli.terminal import Application
8
8
  from fast_agent.ui.console import console as shared_console
9
9
 
10
10
  app = typer.Typer(
11
- help="fast-agent - Build effective agents using Model Context Protocol",
11
+ help="Use `fast-agent go --help` for interactive shell arguments and options.",
12
12
  add_completion=False, # We'll add this later when we have more commands
13
13
  )
14
14
 
fast_agent/interfaces.py CHANGED
@@ -18,6 +18,7 @@ from typing import (
18
18
  Type,
19
19
  TypeVar,
20
20
  Union,
21
+ runtime_checkable,
21
22
  )
22
23
 
23
24
  from a2a.types import AgentCard
@@ -59,6 +60,7 @@ class ModelFactoryFunctionProtocol(Protocol):
59
60
  def __call__(self, model: str | None = None) -> LLMFactoryProtocol: ...
60
61
 
61
62
 
63
+ @runtime_checkable
62
64
  class FastAgentLLMProtocol(Protocol):
63
65
  """Protocol defining the interface for LLMs"""
64
66
 
@@ -111,6 +113,7 @@ class FastAgentLLMProtocol(Protocol):
111
113
  def clear(self, *, clear_prompts: bool = False) -> None: ...
112
114
 
113
115
 
116
+ @runtime_checkable
114
117
  class LlmAgentProtocol(Protocol):
115
118
  """Protocol defining the minimal interface for LLM agents."""
116
119
 
@@ -132,6 +135,7 @@ class LlmAgentProtocol(Protocol):
132
135
  def pop_last_message(self) -> PromptMessageExtended | None: ...
133
136
 
134
137
 
138
+ @runtime_checkable
135
139
  class AgentProtocol(LlmAgentProtocol, Protocol):
136
140
  """Standard agent interface with flexible input types."""
137
141
 
@@ -87,7 +87,7 @@ class ModelDatabase:
87
87
  )
88
88
 
89
89
  GEMINI_PRO = ModelParameters(
90
- context_window=2097152, max_output_tokens=8192, tokenizes=GOOGLE_MULTIMODAL
90
+ context_window=1_048_576, max_output_tokens=65_536, tokenizes=GOOGLE_MULTIMODAL
91
91
  )
92
92
 
93
93
  QWEN_STANDARD = ModelParameters(
@@ -245,6 +245,9 @@ class ModelDatabase:
245
245
  "gemini-2.5-pro-preview": GEMINI_2_5_PRO,
246
246
  "gemini-2.5-flash-preview-05-20": GEMINI_FLASH,
247
247
  "gemini-2.5-pro-preview-05-06": GEMINI_PRO,
248
+ "gemini-2.5-pro": GEMINI_PRO,
249
+ "gemini-2.5-flash-preview-09-2025": GEMINI_FLASH,
250
+ "gemini-2.5-flash": GEMINI_FLASH,
248
251
  # xAI Grok Models
249
252
  "grok-4-fast-reasoning": GROK_4_VLM,
250
253
  "grok-4-fast-non-reasoning": GROK_4_VLM,
@@ -90,7 +90,9 @@ class ModelFactory:
90
90
  "deepseek-chat": Provider.DEEPSEEK,
91
91
  "gemini-2.0-flash": Provider.GOOGLE,
92
92
  "gemini-2.5-flash-preview-05-20": Provider.GOOGLE,
93
+ "gemini-2.5-flash-preview-09-2025": Provider.GOOGLE,
93
94
  "gemini-2.5-pro-preview-05-06": Provider.GOOGLE,
95
+ "gemini-2.5-pro": Provider.GOOGLE,
94
96
  "grok-4": Provider.XAI,
95
97
  "grok-4-0709": Provider.XAI,
96
98
  "grok-3": Provider.XAI,
@@ -120,8 +122,8 @@ class ModelFactory:
120
122
  "deepseekv3": "deepseek-chat",
121
123
  "deepseek": "deepseek-chat",
122
124
  "gemini2": "gemini-2.0-flash",
123
- "gemini25": "gemini-2.5-flash-preview-05-20",
124
- "gemini25pro": "gemini-2.5-pro-preview-05-06",
125
+ "gemini25": "gemini-2.5-flash-preview-09-2025",
126
+ "gemini25pro": "gemini-2.5-pro",
125
127
  "kimi": "groq.moonshotai/kimi-k2-instruct-0905",
126
128
  "gpt-oss": "groq.openai/gpt-oss-120b",
127
129
  "gpt-oss-20b": "groq.openai/gpt-oss-20b",
@@ -8,14 +8,15 @@ capabilities (Text/Document/Vision), backed by the model database.
8
8
  from __future__ import annotations
9
9
 
10
10
  from dataclasses import dataclass
11
- from typing import TYPE_CHECKING, List, Optional, Union
11
+ from typing import TYPE_CHECKING, List, Optional
12
12
 
13
13
  from fast_agent.llm.model_database import ModelDatabase
14
+ from fast_agent.llm.model_factory import ModelFactory
14
15
  from fast_agent.llm.provider_types import Provider
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  # Import behind TYPE_CHECKING to avoid import cycles at runtime
18
- from fast_agent.interfaces import AgentProtocol, FastAgentLLMProtocol
19
+ from fast_agent.interfaces import FastAgentLLMProtocol
19
20
 
20
21
 
21
22
  @dataclass(frozen=True)
@@ -32,16 +33,24 @@ class ModelInfo:
32
33
 
33
34
  @property
34
35
  def supports_text(self) -> bool:
36
+ if "text/plain" in (self.tokenizes or []):
37
+ return True
35
38
  return ModelDatabase.supports_mime(self.name, "text/plain")
36
39
 
37
40
  @property
38
41
  def supports_document(self) -> bool:
39
42
  # Document support currently keyed off PDF support
43
+ if "application/pdf" in (self.tokenizes or []):
44
+ return True
40
45
  return ModelDatabase.supports_mime(self.name, "pdf")
41
46
 
42
47
  @property
43
48
  def supports_vision(self) -> bool:
44
49
  # Any common image format indicates vision support
50
+ tokenizes = self.tokenizes or []
51
+ if any(mt in tokenizes for mt in ("image/jpeg", "image/png", "image/webp")):
52
+ return True
53
+
45
54
  return any(
46
55
  ModelDatabase.supports_mime(self.name, mt)
47
56
  for mt in ("image/jpeg", "image/png", "image/webp")
@@ -62,14 +71,15 @@ class ModelInfo:
62
71
 
63
72
  @classmethod
64
73
  def from_name(cls, name: str, provider: Provider | None = None) -> Optional["ModelInfo"]:
65
- params = ModelDatabase.get_model_params(name)
74
+ canonical_name = ModelFactory.MODEL_ALIASES.get(name, name)
75
+ params = ModelDatabase.get_model_params(canonical_name)
66
76
  if not params:
67
77
  # Unknown model: return a conservative default that supports text only.
68
78
  # This matches the desired behavior for TDV display fallbacks.
69
79
  if provider is None:
70
80
  provider = Provider.GENERIC
71
81
  return ModelInfo(
72
- name=name,
82
+ name=canonical_name,
73
83
  provider=provider,
74
84
  context_window=None,
75
85
  max_output_tokens=None,
@@ -78,49 +88,15 @@ class ModelInfo:
78
88
  reasoning=None,
79
89
  )
80
90
 
91
+ if provider is None:
92
+ provider = ModelFactory.DEFAULT_PROVIDERS.get(canonical_name, Provider.GENERIC)
93
+
81
94
  return ModelInfo(
82
- name=name,
83
- provider=provider or Provider.GENERIC,
95
+ name=canonical_name,
96
+ provider=provider,
84
97
  context_window=params.context_window,
85
98
  max_output_tokens=params.max_output_tokens,
86
99
  tokenizes=params.tokenizes,
87
100
  json_mode=params.json_mode,
88
101
  reasoning=params.reasoning,
89
102
  )
90
-
91
-
92
- def get_model_info(
93
- subject: Union["AgentProtocol", "FastAgentLLMProtocol", str, None],
94
- provider: Provider | None = None,
95
- ) -> Optional[ModelInfo]:
96
- """Resolve a ModelInfo from an Agent, LLM, or model name.
97
-
98
- Keeps the public API small while enabling type-safe access to model
99
- capabilities across the codebase.
100
- """
101
- if subject is None:
102
- return None
103
-
104
- # Agent → LLM
105
- try:
106
- from fast_agent.interfaces import AgentProtocol as _AgentProtocol
107
- except Exception:
108
- _AgentProtocol = None # type: ignore
109
-
110
- if _AgentProtocol and isinstance(subject, _AgentProtocol): # type: ignore[arg-type]
111
- return ModelInfo.from_llm(subject.llm)
112
-
113
- # LLM → ModelInfo
114
- try:
115
- from fast_agent.interfaces import FastAgentLLMProtocol as _LLMProtocol
116
- except Exception:
117
- _LLMProtocol = None # type: ignore
118
-
119
- if _LLMProtocol and isinstance(subject, _LLMProtocol): # type: ignore[arg-type]
120
- return ModelInfo.from_llm(subject)
121
-
122
- # String model name
123
- if isinstance(subject, str):
124
- return ModelInfo.from_name(subject, provider)
125
-
126
- return None
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import secrets
2
3
  from typing import Dict, List
3
4
 
@@ -49,8 +50,6 @@ class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
49
50
 
50
51
  def __init__(self, *args, **kwargs) -> None:
51
52
  super().__init__(*args, provider=Provider.GOOGLE, **kwargs)
52
- # Initialize the google.genai client
53
- self._google_client = self._initialize_google_client()
54
53
  # Initialize the converter
55
54
  self._converter = GoogleConverter()
56
55
 
@@ -109,6 +108,218 @@ class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
109
108
  # Include other relevant default parameters
110
109
  )
111
110
 
111
+ async def _stream_generate_content(
112
+ self,
113
+ *,
114
+ model: str,
115
+ contents: List[types.Content],
116
+ config: types.GenerateContentConfig,
117
+ client: genai.Client,
118
+ ) -> types.GenerateContentResponse | None:
119
+ """Stream Gemini responses and return the final aggregated completion."""
120
+ try:
121
+ response_stream = await client.aio.models.generate_content_stream(
122
+ model=model,
123
+ contents=contents,
124
+ config=config,
125
+ )
126
+ except AttributeError:
127
+ # Older SDKs might not expose streaming; fall back to non-streaming.
128
+ return None
129
+ except errors.APIError:
130
+ raise
131
+ except Exception as exc: # pragma: no cover - defensive fallback
132
+ self.logger.warning(
133
+ "Google streaming failed during setup; falling back to non-streaming",
134
+ exc_info=exc,
135
+ )
136
+ return None
137
+
138
+ return await self._consume_google_stream(response_stream, model=model)
139
+
140
+ async def _consume_google_stream(
141
+ self,
142
+ response_stream,
143
+ *,
144
+ model: str,
145
+ ) -> types.GenerateContentResponse | None:
146
+ """Consume the async streaming iterator and aggregate the final response."""
147
+ estimated_tokens = 0
148
+ timeline: List[tuple[str, int | None, str]] = []
149
+ tool_streams: Dict[int, Dict[str, str]] = {}
150
+ active_tool_index: int | None = None
151
+ tool_counter = 0
152
+ usage_metadata = None
153
+ last_chunk: types.GenerateContentResponse | None = None
154
+
155
+ try:
156
+ async for chunk in response_stream:
157
+ last_chunk = chunk
158
+ if getattr(chunk, "usage_metadata", None):
159
+ usage_metadata = chunk.usage_metadata
160
+
161
+ if not getattr(chunk, "candidates", None):
162
+ continue
163
+
164
+ candidate = chunk.candidates[0]
165
+ content = getattr(candidate, "content", None)
166
+ if content is None or not getattr(content, "parts", None):
167
+ continue
168
+
169
+ for part in content.parts:
170
+ if getattr(part, "text", None):
171
+ text = part.text or ""
172
+ if text:
173
+ if timeline and timeline[-1][0] == "text":
174
+ prev_type, prev_index, prev_text = timeline[-1]
175
+ timeline[-1] = (prev_type, prev_index, prev_text + text)
176
+ else:
177
+ timeline.append(("text", None, text))
178
+ estimated_tokens = self._update_streaming_progress(
179
+ text,
180
+ model,
181
+ estimated_tokens,
182
+ )
183
+ self._notify_tool_stream_listeners(
184
+ "text",
185
+ {
186
+ "chunk": text,
187
+ "streams_arguments": False,
188
+ },
189
+ )
190
+
191
+ if getattr(part, "function_call", None):
192
+ function_call = part.function_call
193
+ name = getattr(function_call, "name", None) or "tool"
194
+ args = getattr(function_call, "args", None) or {}
195
+
196
+ if active_tool_index is None:
197
+ active_tool_index = tool_counter
198
+ tool_counter += 1
199
+ tool_use_id = f"tool_{self.chat_turn()}_{active_tool_index}"
200
+ tool_streams[active_tool_index] = {
201
+ "name": name,
202
+ "tool_use_id": tool_use_id,
203
+ "buffer": "",
204
+ }
205
+ self._notify_tool_stream_listeners(
206
+ "start",
207
+ {
208
+ "tool_name": name,
209
+ "tool_use_id": tool_use_id,
210
+ "index": active_tool_index,
211
+ "streams_arguments": False,
212
+ },
213
+ )
214
+ timeline.append(("tool_call", active_tool_index, ""))
215
+
216
+ stream_info = tool_streams.get(active_tool_index)
217
+ if not stream_info:
218
+ continue
219
+
220
+ try:
221
+ serialized_args = json.dumps(args, separators=(",", ":"))
222
+ except Exception:
223
+ serialized_args = str(args)
224
+
225
+ previous = stream_info.get("buffer", "")
226
+ if isinstance(previous, str) and serialized_args.startswith(previous):
227
+ delta = serialized_args[len(previous) :]
228
+ else:
229
+ delta = serialized_args
230
+ stream_info["buffer"] = serialized_args
231
+
232
+ if delta:
233
+ self._notify_tool_stream_listeners(
234
+ "delta",
235
+ {
236
+ "tool_name": stream_info["name"],
237
+ "tool_use_id": stream_info["tool_use_id"],
238
+ "index": active_tool_index,
239
+ "chunk": delta,
240
+ "streams_arguments": False,
241
+ },
242
+ )
243
+
244
+ finish_reason = getattr(candidate, "finish_reason", None)
245
+ if finish_reason:
246
+ finish_value = str(finish_reason).split(".")[-1].upper()
247
+ if finish_value in {"FUNCTION_CALL", "STOP"} and active_tool_index is not None:
248
+ stream_info = tool_streams.get(active_tool_index)
249
+ if stream_info:
250
+ self._notify_tool_stream_listeners(
251
+ "stop",
252
+ {
253
+ "tool_name": stream_info["name"],
254
+ "tool_use_id": stream_info["tool_use_id"],
255
+ "index": active_tool_index,
256
+ "streams_arguments": False,
257
+ },
258
+ )
259
+ active_tool_index = None
260
+ finally:
261
+ stream_close = getattr(response_stream, "aclose", None)
262
+ if callable(stream_close):
263
+ try:
264
+ await stream_close()
265
+ except Exception:
266
+ pass
267
+
268
+ if active_tool_index is not None:
269
+ stream_info = tool_streams.get(active_tool_index)
270
+ if stream_info:
271
+ self._notify_tool_stream_listeners(
272
+ "stop",
273
+ {
274
+ "tool_name": stream_info["name"],
275
+ "tool_use_id": stream_info["tool_use_id"],
276
+ "index": active_tool_index,
277
+ "streams_arguments": False,
278
+ },
279
+ )
280
+
281
+ if not timeline and last_chunk is None:
282
+ return None
283
+
284
+ final_parts: List[types.Part] = []
285
+ for entry_type, index, payload in timeline:
286
+ if entry_type == "text":
287
+ final_parts.append(types.Part.from_text(text=payload))
288
+ elif entry_type == "tool_call" and index is not None:
289
+ stream_info = tool_streams.get(index)
290
+ if not stream_info:
291
+ continue
292
+ buffer = stream_info.get("buffer", "")
293
+ try:
294
+ args_obj = json.loads(buffer) if buffer else {}
295
+ except json.JSONDecodeError:
296
+ args_obj = {"__raw": buffer}
297
+ final_parts.append(
298
+ types.Part.from_function_call(
299
+ name=str(stream_info.get("name") or "tool"),
300
+ args=args_obj,
301
+ )
302
+ )
303
+
304
+ final_content = types.Content(role="model", parts=final_parts)
305
+
306
+ if last_chunk is not None:
307
+ final_response = last_chunk.model_copy(deep=True)
308
+ if getattr(final_response, "candidates", None):
309
+ final_candidate = final_response.candidates[0]
310
+ final_candidate.content = final_content
311
+ else:
312
+ final_response.candidates = [types.Candidate(content=final_content)]
313
+ else:
314
+ final_response = types.GenerateContentResponse(
315
+ candidates=[types.Candidate(content=final_content)]
316
+ )
317
+
318
+ if usage_metadata:
319
+ final_response.usage_metadata = usage_metadata
320
+
321
+ return final_response
322
+
112
323
  async def _google_completion(
113
324
  self,
114
325
  message: List[types.Content] | None,
@@ -163,13 +374,24 @@ class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
163
374
  )
164
375
 
165
376
  # 3. Call the google.genai API
377
+ client = self._initialize_google_client()
166
378
  try:
167
379
  # Use the async client
168
- api_response = await self._google_client.aio.models.generate_content(
169
- model=request_params.model,
170
- contents=conversation_history, # Full conversational context for this turn
171
- config=generate_content_config,
172
- )
380
+ api_response = None
381
+ streaming_supported = response_schema is None and response_mime_type is None
382
+ if streaming_supported:
383
+ api_response = await self._stream_generate_content(
384
+ model=request_params.model,
385
+ contents=conversation_history,
386
+ config=generate_content_config,
387
+ client=client,
388
+ )
389
+ if api_response is None:
390
+ api_response = await client.aio.models.generate_content(
391
+ model=request_params.model,
392
+ contents=conversation_history, # Full conversational context for this turn
393
+ config=generate_content_config,
394
+ )
173
395
  self.logger.debug("Google generate_content response:", data=api_response)
174
396
 
175
397
  # Track usage if response is valid and has usage data
@@ -195,6 +417,15 @@ class GoogleNativeLLM(FastAgentLLM[types.Content, types.Content]):
195
417
  self.logger.error(f"Error during Google generate_content call: {e}")
196
418
  # Decide how to handle other exceptions - potentially re-raise or return an error message
197
419
  raise e
420
+ finally:
421
+ try:
422
+ await client.aio.aclose()
423
+ except Exception:
424
+ pass
425
+ try:
426
+ client.close()
427
+ except Exception:
428
+ pass
198
429
 
199
430
  # 4. Process the API response
200
431
  if not api_response.candidates: