fast-agent-mcp 0.3.15__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.

Files changed (47) hide show
  1. fast_agent/__init__.py +2 -0
  2. fast_agent/agents/agent_types.py +5 -0
  3. fast_agent/agents/llm_agent.py +7 -0
  4. fast_agent/agents/llm_decorator.py +6 -0
  5. fast_agent/agents/mcp_agent.py +134 -10
  6. fast_agent/cli/__main__.py +35 -0
  7. fast_agent/cli/commands/check_config.py +85 -0
  8. fast_agent/cli/commands/go.py +100 -36
  9. fast_agent/cli/constants.py +15 -1
  10. fast_agent/cli/main.py +2 -1
  11. fast_agent/config.py +39 -10
  12. fast_agent/constants.py +8 -0
  13. fast_agent/context.py +24 -15
  14. fast_agent/core/direct_decorators.py +9 -0
  15. fast_agent/core/fastagent.py +101 -1
  16. fast_agent/core/logging/listeners.py +8 -0
  17. fast_agent/interfaces.py +12 -0
  18. fast_agent/llm/fastagent_llm.py +45 -0
  19. fast_agent/llm/memory.py +26 -1
  20. fast_agent/llm/model_database.py +4 -1
  21. fast_agent/llm/model_factory.py +4 -2
  22. fast_agent/llm/model_info.py +19 -43
  23. fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
  24. fast_agent/llm/provider/google/llm_google_native.py +238 -7
  25. fast_agent/llm/provider/openai/llm_openai.py +382 -19
  26. fast_agent/llm/provider/openai/responses.py +133 -0
  27. fast_agent/resources/setup/agent.py +2 -0
  28. fast_agent/resources/setup/fastagent.config.yaml +6 -0
  29. fast_agent/skills/__init__.py +9 -0
  30. fast_agent/skills/registry.py +208 -0
  31. fast_agent/tools/shell_runtime.py +404 -0
  32. fast_agent/ui/console_display.py +47 -996
  33. fast_agent/ui/elicitation_form.py +76 -24
  34. fast_agent/ui/elicitation_style.py +2 -2
  35. fast_agent/ui/enhanced_prompt.py +107 -37
  36. fast_agent/ui/history_display.py +20 -5
  37. fast_agent/ui/interactive_prompt.py +108 -3
  38. fast_agent/ui/markdown_helpers.py +104 -0
  39. fast_agent/ui/markdown_truncator.py +103 -45
  40. fast_agent/ui/message_primitives.py +50 -0
  41. fast_agent/ui/streaming.py +638 -0
  42. fast_agent/ui/tool_display.py +417 -0
  43. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/METADATA +8 -7
  44. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/RECORD +47 -39
  45. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/WHEEL +0 -0
  46. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/entry_points.txt +0 -0
  47. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/licenses/LICENSE +0 -0
@@ -6,6 +6,7 @@ directly creates Agent instances without proxies.
6
6
 
7
7
  import argparse
8
8
  import asyncio
9
+ import pathlib
9
10
  import sys
10
11
  from contextlib import asynccontextmanager
11
12
  from importlib.metadata import version as get_version
@@ -76,12 +77,14 @@ from fast_agent.core.validation import (
76
77
  validate_workflow_references,
77
78
  )
78
79
  from fast_agent.mcp.prompts.prompt_load import load_prompt
80
+ from fast_agent.skills import SkillManifest, SkillRegistry
79
81
  from fast_agent.ui.usage_display import display_usage_report
80
82
 
81
83
  if TYPE_CHECKING:
82
84
  from mcp.client.session import ElicitationFnT
83
85
  from pydantic import AnyUrl
84
86
 
87
+ from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION
85
88
  from fast_agent.interfaces import AgentProtocol
86
89
  from fast_agent.types import PromptMessageExtended
87
90
 
@@ -102,6 +105,7 @@ class FastAgent:
102
105
  ignore_unknown_args: bool = False,
103
106
  parse_cli_args: bool = True,
104
107
  quiet: bool = False, # Add quiet parameter
108
+ skills_directory: str | pathlib.Path | None = None,
105
109
  **kwargs,
106
110
  ) -> None:
107
111
  """
@@ -119,6 +123,10 @@ class FastAgent:
119
123
  """
120
124
  self.args = argparse.Namespace() # Initialize args always
121
125
  self._programmatic_quiet = quiet # Store the programmatic quiet setting
126
+ self._skills_directory_override = (
127
+ Path(skills_directory).expanduser() if skills_directory else None
128
+ )
129
+ self._default_skill_manifests: List[SkillManifest] = []
122
130
 
123
131
  # --- Wrap argument parsing logic ---
124
132
  if parse_cli_args:
@@ -173,6 +181,10 @@ class FastAgent:
173
181
  default="0.0.0.0",
174
182
  help="Host address to bind to when running as a server with SSE transport",
175
183
  )
184
+ parser.add_argument(
185
+ "--skills",
186
+ help="Path to skills directory to use instead of default .claude/skills",
187
+ )
176
188
 
177
189
  if ignore_unknown_args:
178
190
  known_args, _ = parser.parse_known_args()
@@ -200,6 +212,14 @@ class FastAgent:
200
212
  if self._programmatic_quiet:
201
213
  self.args.quiet = True
202
214
 
215
+ # Apply CLI skills directory if not already set programmatically
216
+ if (
217
+ self._skills_directory_override is None
218
+ and hasattr(self.args, "skills")
219
+ and self.args.skills
220
+ ):
221
+ self._skills_directory_override = Path(self.args.skills).expanduser()
222
+
203
223
  self.name = name
204
224
  self.config_path = config_path
205
225
 
@@ -271,6 +291,7 @@ class FastAgent:
271
291
  from collections.abc import Coroutine
272
292
  from pathlib import Path
273
293
 
294
+ from fast_agent.skills import SkillManifest, SkillRegistry
274
295
  from fast_agent.types import RequestParams
275
296
 
276
297
  P = ParamSpec("P")
@@ -281,11 +302,12 @@ class FastAgent:
281
302
  name: str = "default",
282
303
  instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
283
304
  *,
284
- instruction: str | Path | AnyUrl = "You are a helpful agent.",
305
+ instruction: str | Path | AnyUrl = DEFAULT_AGENT_INSTRUCTION,
285
306
  servers: List[str] = [],
286
307
  tools: Optional[Dict[str, List[str]]] = None,
287
308
  resources: Optional[Dict[str, List[str]]] = None,
288
309
  prompts: Optional[Dict[str, List[str]]] = None,
310
+ skills: Optional[List[SkillManifest | SkillRegistry | Path | str | None]] = None,
289
311
  model: Optional[str] = None,
290
312
  use_history: bool = True,
291
313
  request_params: RequestParams | None = None,
@@ -430,6 +452,21 @@ class FastAgent:
430
452
  with tracer.start_as_current_span(self.name):
431
453
  try:
432
454
  async with self.app.run():
455
+ registry = getattr(self.context, "skill_registry", None)
456
+ if self._skills_directory_override is not None:
457
+ override_registry = SkillRegistry(
458
+ base_dir=Path.cwd(),
459
+ override_directory=self._skills_directory_override,
460
+ )
461
+ self.context.skill_registry = override_registry
462
+ registry = override_registry
463
+
464
+ default_skills: List[SkillManifest] = []
465
+ if registry:
466
+ default_skills = registry.load_manifests()
467
+
468
+ self._apply_skills_to_agent_configs(default_skills)
469
+
433
470
  # Apply quiet mode if requested
434
471
  if quiet_mode:
435
472
  cfg = self.app.context.config
@@ -621,6 +658,69 @@ class FastAgent:
621
658
  except Exception:
622
659
  pass
623
660
 
661
+ def _apply_skills_to_agent_configs(self, default_skills: List[SkillManifest]) -> None:
662
+ self._default_skill_manifests = list(default_skills)
663
+
664
+ for agent_data in self.agents.values():
665
+ config_obj = agent_data.get("config")
666
+ if not config_obj:
667
+ continue
668
+
669
+ resolved = self._resolve_skills(config_obj.skills)
670
+ if not resolved:
671
+ resolved = list(default_skills)
672
+ else:
673
+ resolved = self._deduplicate_skills(resolved)
674
+
675
+ config_obj.skill_manifests = resolved
676
+
677
+ def _resolve_skills(
678
+ self,
679
+ entry: SkillManifest
680
+ | SkillRegistry
681
+ | Path
682
+ | str
683
+ | List[SkillManifest | SkillRegistry | Path | str | None]
684
+ | None,
685
+ ) -> List[SkillManifest]:
686
+ if entry is None:
687
+ return []
688
+ if isinstance(entry, list):
689
+ manifests: List[SkillManifest] = []
690
+ for item in entry:
691
+ manifests.extend(self._resolve_skills(item))
692
+ return manifests
693
+ if isinstance(entry, SkillManifest):
694
+ return [entry]
695
+ if isinstance(entry, SkillRegistry):
696
+ try:
697
+ return entry.load_manifests()
698
+ except Exception:
699
+ logger.debug(
700
+ "Failed to load skills from registry",
701
+ data={"registry": type(entry).__name__},
702
+ )
703
+ return []
704
+ if isinstance(entry, Path):
705
+ return SkillRegistry.load_directory(entry.expanduser().resolve())
706
+ if isinstance(entry, str):
707
+ return SkillRegistry.load_directory(Path(entry).expanduser().resolve())
708
+
709
+ logger.debug(
710
+ "Unsupported skill entry type",
711
+ data={"type": type(entry).__name__},
712
+ )
713
+ return []
714
+
715
+ @staticmethod
716
+ def _deduplicate_skills(manifests: List[SkillManifest]) -> List[SkillManifest]:
717
+ unique: Dict[str, SkillManifest] = {}
718
+ for manifest in manifests:
719
+ key = manifest.name.lower()
720
+ if key not in unique:
721
+ unique[key] = manifest
722
+ return list(unique.values())
723
+
624
724
  def _handle_error(self, e: Exception, error_type: Optional[str] = None) -> None:
625
725
  """
626
726
  Handle errors with consistent formatting and messaging.
@@ -64,6 +64,14 @@ def convert_log_event(event: Event) -> "ProgressEvent | None":
64
64
  chat_turn = event_data.get("chat_turn")
65
65
  if chat_turn is not None:
66
66
  details = f"{model} turn {chat_turn}"
67
+
68
+ tool_name = event_data.get("tool_name")
69
+ tool_event = event_data.get("tool_event")
70
+ if tool_name:
71
+ tool_suffix = tool_name
72
+ if tool_event:
73
+ tool_suffix = f"{tool_suffix} ({tool_event})"
74
+ details = f"{details} • {tool_suffix}".strip()
67
75
  else:
68
76
  if not target:
69
77
  target = event_data.get("target", "unknown")
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
 
@@ -87,9 +89,15 @@ class FastAgentLLMProtocol(Protocol):
87
89
 
88
90
  def add_stream_listener(self, listener: Callable[[str], None]) -> Callable[[], None]: ...
89
91
 
92
+ def add_tool_stream_listener(
93
+ self, listener: Callable[[str, Dict[str, Any] | None], None]
94
+ ) -> Callable[[], None]: ...
95
+
90
96
  @property
91
97
  def message_history(self) -> List[PromptMessageExtended]: ...
92
98
 
99
+ def pop_last_message(self) -> PromptMessageExtended | None: ...
100
+
93
101
  @property
94
102
  def usage_accumulator(self) -> UsageAccumulator | None: ...
95
103
 
@@ -105,6 +113,7 @@ class FastAgentLLMProtocol(Protocol):
105
113
  def clear(self, *, clear_prompts: bool = False) -> None: ...
106
114
 
107
115
 
116
+ @runtime_checkable
108
117
  class LlmAgentProtocol(Protocol):
109
118
  """Protocol defining the minimal interface for LLM agents."""
110
119
 
@@ -123,7 +132,10 @@ class LlmAgentProtocol(Protocol):
123
132
 
124
133
  def clear(self, *, clear_prompts: bool = False) -> None: ...
125
134
 
135
+ def pop_last_message(self) -> PromptMessageExtended | None: ...
136
+
126
137
 
138
+ @runtime_checkable
127
139
  class AgentProtocol(LlmAgentProtocol, Protocol):
128
140
  """Standard agent interface with flexible input types."""
129
141
 
@@ -159,6 +159,7 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
159
159
  # Initialize usage tracking
160
160
  self._usage_accumulator = UsageAccumulator()
161
161
  self._stream_listeners: set[Callable[[str], None]] = set()
162
+ self._tool_stream_listeners: set[Callable[[str, Dict[str, Any] | None], None]] = set()
162
163
 
163
164
  def _initialize_default_params(self, kwargs: dict) -> RequestParams:
164
165
  """Initialize default parameters for the LLM.
@@ -534,6 +535,37 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
534
535
  except Exception:
535
536
  self.logger.exception("Stream listener raised an exception")
536
537
 
538
+ def add_tool_stream_listener(
539
+ self, listener: Callable[[str, Dict[str, Any] | None], None]
540
+ ) -> Callable[[], None]:
541
+ """Register a callback invoked with tool streaming events.
542
+
543
+ Args:
544
+ listener: Callable receiving event_type (str) and optional info dict.
545
+
546
+ Returns:
547
+ A function that removes the listener when called.
548
+ """
549
+
550
+ self._tool_stream_listeners.add(listener)
551
+
552
+ def remove() -> None:
553
+ self._tool_stream_listeners.discard(listener)
554
+
555
+ return remove
556
+
557
+ def _notify_tool_stream_listeners(
558
+ self, event_type: str, payload: Dict[str, Any] | None = None
559
+ ) -> None:
560
+ """Notify listeners about tool streaming lifecycle events."""
561
+
562
+ data = payload or {}
563
+ for listener in list(self._tool_stream_listeners):
564
+ try:
565
+ listener(event_type, data)
566
+ except Exception:
567
+ self.logger.exception("Tool stream listener raised an exception")
568
+
537
569
  def _log_chat_finished(self, model: Optional[str] = None) -> None:
538
570
  """Log a chat finished event"""
539
571
  data = {
@@ -643,6 +675,19 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
643
675
  """
644
676
  return self._message_history
645
677
 
678
+ def pop_last_message(self) -> PromptMessageExtended | None:
679
+ """Remove and return the most recent message from the conversation history."""
680
+ if not self._message_history:
681
+ return None
682
+
683
+ removed = self._message_history.pop()
684
+ try:
685
+ self.history.pop()
686
+ except Exception:
687
+ # If provider-specific memory isn't available, ignore to avoid crashing UX
688
+ pass
689
+ return removed
690
+
646
691
  def clear(self, *, clear_prompts: bool = False) -> None:
647
692
  """Reset stored message history while optionally retaining prompt templates."""
648
693
 
fast_agent/llm/memory.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Generic, List, Protocol, TypeVar
1
+ from typing import Generic, List, Optional, Protocol, TypeVar
2
2
 
3
3
  # Define our own type variable for implementation use
4
4
  MessageParamT = TypeVar("MessageParamT")
@@ -23,6 +23,8 @@ class Memory(Protocol, Generic[MessageParamT]):
23
23
 
24
24
  def clear(self, clear_prompts: bool = False) -> None: ...
25
25
 
26
+ def pop(self, *, from_prompts: bool = False) -> Optional[MessageParamT]: ...
27
+
26
28
 
27
29
  class SimpleMemory(Memory, Generic[MessageParamT]):
28
30
  """
@@ -108,6 +110,29 @@ class SimpleMemory(Memory, Generic[MessageParamT]):
108
110
  if clear_prompts:
109
111
  self.prompt_messages = []
110
112
 
113
+ def pop(self, *, from_prompts: bool = False) -> Optional[MessageParamT]:
114
+ """
115
+ Remove and return the most recent message from history or prompt messages.
116
+
117
+ Args:
118
+ from_prompts: If True, pop from prompt_messages instead of history
119
+
120
+ Returns:
121
+ The removed message if available, otherwise None
122
+ """
123
+ if from_prompts:
124
+ if not self.prompt_messages:
125
+ return None
126
+ return self.prompt_messages.pop()
127
+
128
+ if not self.history:
129
+ return None
130
+
131
+ removed = self.history.pop()
132
+ # Recalculate cache positions now that the history shrank
133
+ self.conversation_cache_positions = self._calculate_cache_positions(len(self.history))
134
+ return removed
135
+
111
136
  def should_apply_conversation_cache(self) -> bool:
112
137
  """
113
138
  Determine if conversation caching should be applied based on walking algorithm.
@@ -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
@@ -244,10 +244,114 @@ class AnthropicLLM(FastAgentLLM[MessageParam, Message]):
244
244
  """Process the streaming response and display real-time token usage."""
245
245
  # Track estimated output tokens by counting text chunks
246
246
  estimated_tokens = 0
247
+ tool_streams: dict[int, dict[str, Any]] = {}
247
248
 
248
249
  try:
249
250
  # Process the raw event stream to get token counts
250
251
  async for event in stream:
252
+ if (
253
+ event.type == "content_block_start"
254
+ and hasattr(event, "content_block")
255
+ and getattr(event.content_block, "type", None) == "tool_use"
256
+ ):
257
+ content_block = event.content_block
258
+ tool_streams[event.index] = {
259
+ "name": content_block.name,
260
+ "id": content_block.id,
261
+ "buffer": [],
262
+ }
263
+ self._notify_tool_stream_listeners(
264
+ "start",
265
+ {
266
+ "tool_name": content_block.name,
267
+ "tool_use_id": content_block.id,
268
+ "index": event.index,
269
+ "streams_arguments": False, # Anthropic doesn't stream arguments
270
+ },
271
+ )
272
+ self.logger.info(
273
+ "Model started streaming tool input",
274
+ data={
275
+ "progress_action": ProgressAction.CALLING_TOOL,
276
+ "agent_name": self.name,
277
+ "model": model,
278
+ "tool_name": content_block.name,
279
+ "tool_use_id": content_block.id,
280
+ "tool_event": "start",
281
+ },
282
+ )
283
+ continue
284
+
285
+ if (
286
+ event.type == "content_block_delta"
287
+ and hasattr(event, "delta")
288
+ and event.delta.type == "input_json_delta"
289
+ ):
290
+ info = tool_streams.get(event.index)
291
+ if info is not None:
292
+ chunk = event.delta.partial_json or ""
293
+ info["buffer"].append(chunk)
294
+ preview = chunk if len(chunk) <= 80 else chunk[:77] + "..."
295
+ self._notify_tool_stream_listeners(
296
+ "delta",
297
+ {
298
+ "tool_name": info.get("name"),
299
+ "tool_use_id": info.get("id"),
300
+ "index": event.index,
301
+ "chunk": chunk,
302
+ "streams_arguments": False,
303
+ },
304
+ )
305
+ self.logger.debug(
306
+ "Streaming tool input delta",
307
+ data={
308
+ "tool_name": info.get("name"),
309
+ "tool_use_id": info.get("id"),
310
+ "chunk": preview,
311
+ },
312
+ )
313
+ continue
314
+
315
+ if (
316
+ event.type == "content_block_stop"
317
+ and event.index in tool_streams
318
+ ):
319
+ info = tool_streams.pop(event.index)
320
+ preview_raw = "".join(info.get("buffer", []))
321
+ if preview_raw:
322
+ preview = (
323
+ preview_raw if len(preview_raw) <= 120 else preview_raw[:117] + "..."
324
+ )
325
+ self.logger.debug(
326
+ "Completed tool input stream",
327
+ data={
328
+ "tool_name": info.get("name"),
329
+ "tool_use_id": info.get("id"),
330
+ "input_preview": preview,
331
+ },
332
+ )
333
+ self._notify_tool_stream_listeners(
334
+ "stop",
335
+ {
336
+ "tool_name": info.get("name"),
337
+ "tool_use_id": info.get("id"),
338
+ "index": event.index,
339
+ "streams_arguments": False,
340
+ },
341
+ )
342
+ self.logger.info(
343
+ "Model finished streaming tool input",
344
+ data={
345
+ "progress_action": ProgressAction.CALLING_TOOL,
346
+ "agent_name": self.name,
347
+ "model": model,
348
+ "tool_name": info.get("name"),
349
+ "tool_use_id": info.get("id"),
350
+ "tool_event": "stop",
351
+ },
352
+ )
353
+ continue
354
+
251
355
  # Count tokens in real-time from content_block_delta events
252
356
  if (
253
357
  event.type == "content_block_delta"
@@ -258,6 +362,14 @@ class AnthropicLLM(FastAgentLLM[MessageParam, Message]):
258
362
  estimated_tokens = self._update_streaming_progress(
259
363
  event.delta.text, model, estimated_tokens
260
364
  )
365
+ self._notify_tool_stream_listeners(
366
+ "text",
367
+ {
368
+ "chunk": event.delta.text,
369
+ "index": event.index,
370
+ "streams_arguments": False,
371
+ },
372
+ )
261
373
 
262
374
  # Also check for final message_delta events with actual usage info
263
375
  elif (