fast-agent-mcp 0.3.14__py3-none-any.whl → 0.3.16__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 (49) 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 +52 -4
  4. fast_agent/agents/llm_decorator.py +6 -0
  5. fast_agent/agents/mcp_agent.py +137 -13
  6. fast_agent/agents/tool_agent.py +33 -19
  7. fast_agent/agents/workflow/router_agent.py +2 -1
  8. fast_agent/cli/__main__.py +35 -0
  9. fast_agent/cli/commands/check_config.py +90 -2
  10. fast_agent/cli/commands/go.py +100 -36
  11. fast_agent/cli/constants.py +13 -1
  12. fast_agent/cli/main.py +1 -0
  13. fast_agent/config.py +41 -12
  14. fast_agent/constants.py +8 -0
  15. fast_agent/context.py +24 -15
  16. fast_agent/core/direct_decorators.py +9 -0
  17. fast_agent/core/fastagent.py +115 -2
  18. fast_agent/core/logging/listeners.py +8 -0
  19. fast_agent/core/validation.py +31 -33
  20. fast_agent/human_input/form_fields.py +4 -1
  21. fast_agent/interfaces.py +12 -1
  22. fast_agent/llm/fastagent_llm.py +76 -0
  23. fast_agent/llm/memory.py +26 -1
  24. fast_agent/llm/model_database.py +2 -2
  25. fast_agent/llm/model_factory.py +4 -1
  26. fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
  27. fast_agent/llm/provider/openai/llm_openai.py +184 -18
  28. fast_agent/llm/provider/openai/responses.py +133 -0
  29. fast_agent/mcp/prompt_message_extended.py +2 -2
  30. fast_agent/resources/setup/agent.py +2 -0
  31. fast_agent/resources/setup/fastagent.config.yaml +11 -4
  32. fast_agent/skills/__init__.py +9 -0
  33. fast_agent/skills/registry.py +200 -0
  34. fast_agent/tools/shell_runtime.py +404 -0
  35. fast_agent/ui/console_display.py +925 -73
  36. fast_agent/ui/elicitation_form.py +98 -24
  37. fast_agent/ui/elicitation_style.py +2 -2
  38. fast_agent/ui/enhanced_prompt.py +128 -26
  39. fast_agent/ui/history_display.py +20 -5
  40. fast_agent/ui/interactive_prompt.py +108 -3
  41. fast_agent/ui/markdown_truncator.py +942 -0
  42. fast_agent/ui/mcp_display.py +2 -2
  43. fast_agent/ui/plain_text_truncator.py +68 -0
  44. fast_agent/ui/streaming_buffer.py +449 -0
  45. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/METADATA +9 -7
  46. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/RECORD +49 -42
  47. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/WHEEL +0 -0
  48. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/entry_points.txt +0 -0
  49. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.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,
@@ -293,7 +315,9 @@ class FastAgent:
293
315
  default: bool = False,
294
316
  elicitation_handler: Optional[ElicitationFnT] = None,
295
317
  api_key: str | None = None,
296
- ) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]]: ...
318
+ ) -> Callable[
319
+ [Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]
320
+ ]: ...
297
321
 
298
322
  def custom(
299
323
  self,
@@ -428,6 +452,21 @@ class FastAgent:
428
452
  with tracer.start_as_current_span(self.name):
429
453
  try:
430
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
+
431
470
  # Apply quiet mode if requested
432
471
  if quiet_mode:
433
472
  cfg = self.app.context.config
@@ -474,6 +513,17 @@ class FastAgent:
474
513
  # Create a wrapper with all agents for simplified access
475
514
  wrapper = AgentApp(active_agents)
476
515
 
516
+ # Disable streaming if parallel agents are present
517
+ from fast_agent.agents.agent_types import AgentType
518
+
519
+ has_parallel = any(
520
+ agent.agent_type == AgentType.PARALLEL for agent in active_agents.values()
521
+ )
522
+ if has_parallel:
523
+ cfg = self.app.context.config
524
+ if cfg is not None and cfg.logger is not None:
525
+ cfg.logger.streaming = "none"
526
+
477
527
  # Handle command line options that should be processed after agent initialization
478
528
 
479
529
  # Handle --server option
@@ -608,6 +658,69 @@ class FastAgent:
608
658
  except Exception:
609
659
  pass
610
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
+
611
724
  def _handle_error(self, e: Exception, error_type: Optional[str] = None) -> None:
612
725
  """
613
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")
@@ -200,6 +200,34 @@ def get_dependencies(
200
200
  return deps
201
201
 
202
202
 
203
+ def get_agent_dependencies(agent_data: dict[str, Any]) -> set[str]:
204
+ deps: set[str] = set()
205
+ agent_dependency_attribute_names = {
206
+ AgentType.CHAIN: ("sequence",),
207
+ AgentType.EVALUATOR_OPTIMIZER: ("evaluator", "generator", "eval_optimizer_agents"),
208
+ AgentType.ITERATIVE_PLANNER: ("child_agents",),
209
+ AgentType.ORCHESTRATOR: ("child_agents",),
210
+ AgentType.PARALLEL: ("fan_out", "fan_in", "parallel_agents"),
211
+ AgentType.ROUTER: ("router_agents",),
212
+ }
213
+ agent_type = agent_data["type"]
214
+ dependency_names = agent_dependency_attribute_names.get(agent_type, None)
215
+ if dependency_names is None:
216
+ return deps
217
+
218
+ for dependency_name in dependency_names:
219
+ dependency_value = agent_data.get(dependency_name)
220
+ if dependency_value is None:
221
+ continue
222
+ if isinstance(dependency_value, str):
223
+ deps.add(dependency_value)
224
+ else:
225
+ # here, we have an implicit assumption that if it is not a None or a string, then it is a list
226
+ deps.update(dependency_value)
227
+
228
+ return deps
229
+
230
+
203
231
  def get_dependencies_groups(
204
232
  agents_dict: Dict[str, Dict[str, Any]], allow_cycles: bool = False
205
233
  ) -> List[List[str]]:
@@ -221,39 +249,9 @@ def get_dependencies_groups(
221
249
  agent_names = list(agents_dict.keys())
222
250
 
223
251
  # Dictionary to store dependencies for each agent
224
- dependencies = {name: set() for name in agent_names}
225
-
226
- # Build the dependency graph
227
- for name, agent_data in agents_dict.items():
228
- agent_type = agent_data["type"] # This is a string from config
229
-
230
- # Note: Compare string values from config with the Enum's string value
231
- if agent_type == AgentType.PARALLEL.value:
232
- # Parallel agents depend on their fan-out and fan-in agents
233
- dependencies[name].update(agent_data.get("parallel_agents", []))
234
- # Also add explicit fan_out dependencies if present
235
- if "fan_out" in agent_data:
236
- dependencies[name].update(agent_data["fan_out"])
237
- # Add explicit fan_in dependency if present
238
- if "fan_in" in agent_data and agent_data["fan_in"]:
239
- dependencies[name].add(agent_data["fan_in"])
240
- elif agent_type == AgentType.CHAIN.value:
241
- # Chain agents depend on the agents in their sequence
242
- dependencies[name].update(agent_data.get("sequence", []))
243
- elif agent_type == AgentType.ROUTER.value:
244
- # Router agents depend on the agents they route to
245
- dependencies[name].update(agent_data.get("router_agents", []))
246
- elif agent_type == AgentType.ORCHESTRATOR.value:
247
- # Orchestrator agents depend on their child agents
248
- dependencies[name].update(agent_data.get("child_agents", []))
249
- elif agent_type == AgentType.EVALUATOR_OPTIMIZER.value:
250
- # Evaluator-Optimizer agents depend on their evaluator and generator agents
251
- if "evaluator" in agent_data:
252
- dependencies[name].add(agent_data["evaluator"])
253
- if "generator" in agent_data:
254
- dependencies[name].add(agent_data["generator"])
255
- # For backward compatibility - also check eval_optimizer_agents if present
256
- dependencies[name].update(agent_data.get("eval_optimizer_agents", []))
252
+ dependencies = {
253
+ name: get_agent_dependencies(agent_data) for name, agent_data in agents_dict.items()
254
+ }
257
255
 
258
256
  # Check for cycles if not allowed
259
257
  if not allow_cycles:
@@ -29,6 +29,8 @@ class StringField:
29
29
  schema["minLength"] = self.min_length
30
30
  if self.max_length is not None:
31
31
  schema["maxLength"] = self.max_length
32
+ if self.pattern is not None:
33
+ schema["pattern"] = self.pattern
32
34
  if self.format:
33
35
  schema["format"] = self.format
34
36
 
@@ -178,10 +180,11 @@ def string(
178
180
  default: Optional[str] = None,
179
181
  min_length: Optional[int] = None,
180
182
  max_length: Optional[int] = None,
183
+ pattern: Optional[str] = None,
181
184
  format: Optional[str] = None,
182
185
  ) -> StringField:
183
186
  """Create a string field."""
184
- return StringField(title, description, default, min_length, max_length, format)
187
+ return StringField(title, description, default, min_length, max_length, pattern, format)
185
188
 
186
189
 
187
190
  def email(
fast_agent/interfaces.py CHANGED
@@ -8,6 +8,7 @@ without pulling in MCP-specific code, helping to avoid circular imports.
8
8
  from typing import (
9
9
  TYPE_CHECKING,
10
10
  Any,
11
+ Callable,
11
12
  Dict,
12
13
  List,
13
14
  Mapping,
@@ -83,10 +84,18 @@ class FastAgentLLMProtocol(Protocol):
83
84
  self,
84
85
  request_params: RequestParams | None = None,
85
86
  ) -> RequestParams: ...
86
-
87
+
88
+ def add_stream_listener(self, listener: Callable[[str], None]) -> Callable[[], None]: ...
89
+
90
+ def add_tool_stream_listener(
91
+ self, listener: Callable[[str, Dict[str, Any] | None], None]
92
+ ) -> Callable[[], None]: ...
93
+
87
94
  @property
88
95
  def message_history(self) -> List[PromptMessageExtended]: ...
89
96
 
97
+ def pop_last_message(self) -> PromptMessageExtended | None: ...
98
+
90
99
  @property
91
100
  def usage_accumulator(self) -> UsageAccumulator | None: ...
92
101
 
@@ -120,6 +129,8 @@ class LlmAgentProtocol(Protocol):
120
129
 
121
130
  def clear(self, *, clear_prompts: bool = False) -> None: ...
122
131
 
132
+ def pop_last_message(self) -> PromptMessageExtended | None: ...
133
+
123
134
 
124
135
  class AgentProtocol(LlmAgentProtocol, Protocol):
125
136
  """Standard agent interface with flexible input types."""
@@ -3,6 +3,7 @@ from contextvars import ContextVar
3
3
  from typing import (
4
4
  TYPE_CHECKING,
5
5
  Any,
6
+ Callable,
6
7
  Dict,
7
8
  Generic,
8
9
  List,
@@ -157,6 +158,8 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
157
158
 
158
159
  # Initialize usage tracking
159
160
  self._usage_accumulator = UsageAccumulator()
161
+ self._stream_listeners: set[Callable[[str], None]] = set()
162
+ self._tool_stream_listeners: set[Callable[[str, Dict[str, Any] | None], None]] = set()
160
163
 
161
164
  def _initialize_default_params(self, kwargs: dict) -> RequestParams:
162
165
  """Initialize default parameters for the LLM.
@@ -483,6 +486,8 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
483
486
  Returns:
484
487
  Updated estimated token count
485
488
  """
489
+ self._notify_stream_listeners(content)
490
+
486
491
  # Rough estimate: 1 token per 4 characters (OpenAI's typical ratio)
487
492
  text_length = len(content)
488
493
  additional_tokens = max(1, text_length // 4)
@@ -503,6 +508,64 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
503
508
 
504
509
  return new_total
505
510
 
511
+ def add_stream_listener(self, listener: Callable[[str], None]) -> Callable[[], None]:
512
+ """
513
+ Register a callback invoked with streaming text chunks.
514
+
515
+ Args:
516
+ listener: Callable receiving the text chunk emitted by the provider.
517
+
518
+ Returns:
519
+ A function that removes the listener when called.
520
+ """
521
+ self._stream_listeners.add(listener)
522
+
523
+ def remove() -> None:
524
+ self._stream_listeners.discard(listener)
525
+
526
+ return remove
527
+
528
+ def _notify_stream_listeners(self, chunk: str) -> None:
529
+ """Notify registered listeners with a streaming text chunk."""
530
+ if not chunk:
531
+ return
532
+ for listener in list(self._stream_listeners):
533
+ try:
534
+ listener(chunk)
535
+ except Exception:
536
+ self.logger.exception("Stream listener raised an exception")
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
+
506
569
  def _log_chat_finished(self, model: Optional[str] = None) -> None:
507
570
  """Log a chat finished event"""
508
571
  data = {
@@ -612,6 +675,19 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
612
675
  """
613
676
  return self._message_history
614
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
+
615
691
  def clear(self, *, clear_prompts: bool = False) -> None:
616
692
  """Reset stored message history while optionally retaining prompt templates."""
617
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.
@@ -130,11 +130,9 @@ class ModelDatabase:
130
130
  context_window=400000, max_output_tokens=128000, tokenizes=OPENAI_MULTIMODAL
131
131
  )
132
132
 
133
- # TODO update to 32000
134
133
  ANTHROPIC_OPUS_4_VERSIONED = ModelParameters(
135
134
  context_window=200000, max_output_tokens=32000, tokenizes=ANTHROPIC_MULTIMODAL
136
135
  )
137
- # TODO update to 64000
138
136
  ANTHROPIC_SONNET_4_VERSIONED = ModelParameters(
139
137
  context_window=200000, max_output_tokens=64000, tokenizes=ANTHROPIC_MULTIMODAL
140
138
  )
@@ -237,6 +235,8 @@ class ModelDatabase:
237
235
  "claude-opus-4-0": ANTHROPIC_OPUS_4_VERSIONED,
238
236
  "claude-opus-4-1": ANTHROPIC_OPUS_4_VERSIONED,
239
237
  "claude-opus-4-20250514": ANTHROPIC_OPUS_4_VERSIONED,
238
+ "claude-haiku-4-5-20251001": ANTHROPIC_SONNET_4_VERSIONED,
239
+ "claude-haiku-4-5": ANTHROPIC_SONNET_4_VERSIONED,
240
240
  # DeepSeek Models
241
241
  "deepseek-chat": DEEPSEEK_CHAT_STANDARD,
242
242
  # Google Gemini Models (vanilla aliases and versioned)
@@ -86,6 +86,7 @@ class ModelFactory:
86
86
  "claude-sonnet-4-0": Provider.ANTHROPIC,
87
87
  "claude-sonnet-4-5-20250929": Provider.ANTHROPIC,
88
88
  "claude-sonnet-4-5": Provider.ANTHROPIC,
89
+ "claude-haiku-4-5": Provider.ANTHROPIC,
89
90
  "deepseek-chat": Provider.DEEPSEEK,
90
91
  "gemini-2.0-flash": Provider.GOOGLE,
91
92
  "gemini-2.5-flash-preview-05-20": Provider.GOOGLE,
@@ -109,9 +110,10 @@ class ModelFactory:
109
110
  "sonnet35": "claude-3-5-sonnet-latest",
110
111
  "sonnet37": "claude-3-7-sonnet-latest",
111
112
  "claude": "claude-sonnet-4-0",
112
- "haiku": "claude-3-5-haiku-latest",
113
+ "haiku": "claude-haiku-4-5",
113
114
  "haiku3": "claude-3-haiku-20240307",
114
115
  "haiku35": "claude-3-5-haiku-latest",
116
+ "hauku45": "claude-haiku-4-5",
115
117
  "opus": "claude-opus-4-1",
116
118
  "opus4": "claude-opus-4-1",
117
119
  "opus3": "claude-3-opus-latest",
@@ -319,6 +321,7 @@ class ModelFactory:
319
321
  return GroqLLM
320
322
  if provider == Provider.RESPONSES:
321
323
  from fast_agent.llm.provider.openai.responses import ResponsesLLM
324
+
322
325
  return ResponsesLLM
323
326
 
324
327
  except Exception as e: