fast-agent-mcp 0.0.13__py3-none-any.whl → 0.0.15__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.
@@ -10,266 +10,57 @@ from typing import (
10
10
  Callable,
11
11
  TypeVar,
12
12
  Any,
13
- Union,
14
- TypeAlias,
15
13
  Literal,
16
14
  )
17
- from enum import Enum
18
15
  import yaml
19
16
  import argparse
20
17
  from contextlib import asynccontextmanager
21
18
 
22
- from mcp_agent.core.exceptions import (
23
- AgentConfigError,
24
- ModelConfigError,
25
- PromptExitError,
26
- ServerConfigError,
27
- ProviderKeyError,
28
- ServerInitializationError,
29
- )
30
-
31
19
  from mcp_agent.app import MCPApp
32
20
  from mcp_agent.agents.agent import Agent, AgentConfig
33
21
  from mcp_agent.context_dependent import ContextDependent
22
+ from mcp_agent.config import Settings
34
23
  from mcp_agent.event_progress import ProgressAction
35
- from mcp_agent.workflows.orchestrator.orchestrator import Orchestrator
36
- from mcp_agent.workflows.parallel.parallel_llm import ParallelLLM
37
24
  from mcp_agent.workflows.evaluator_optimizer.evaluator_optimizer import (
38
25
  EvaluatorOptimizerLLM,
39
26
  QualityRating,
40
27
  )
41
- from mcp_agent.workflows.router.router_llm import LLMRouter
42
- from mcp_agent.config import Settings
43
- from rich import print
44
- from mcp_agent.progress_display import progress_display
45
- from mcp_agent.workflows.llm.model_factory import ModelFactory
46
28
  from mcp_agent.workflows.llm.augmented_llm import AugmentedLLM, RequestParams
29
+ from mcp_agent.workflows.llm.model_factory import ModelFactory
30
+ from mcp_agent.workflows.orchestrator.orchestrator import Orchestrator
31
+ from mcp_agent.workflows.parallel.parallel_llm import ParallelLLM
32
+ from mcp_agent.workflows.router.router_llm import LLMRouter
47
33
 
48
- # TODO -- resintate once Windows&Python 3.13 platform issues are fixed
49
- # import readline # noqa: F401
50
-
51
- # Type aliases for better readability
52
- WorkflowType: TypeAlias = Union[
53
- Orchestrator, ParallelLLM, EvaluatorOptimizerLLM, LLMRouter
54
- ]
55
- AgentOrWorkflow: TypeAlias = Union[Agent, WorkflowType]
56
- ProxyDict: TypeAlias = Dict[str, "BaseAgentProxy"]
57
-
58
-
59
- class AgentType(Enum):
60
- """Enumeration of supported agent types."""
34
+ from mcp_agent.core.agent_app import AgentApp
35
+ from mcp_agent.core.agent_types import AgentType
36
+ from mcp_agent.core.agent_utils import unwrap_proxy, get_agent_instances, log_agent_load
37
+ from mcp_agent.core.error_handling import handle_error
38
+ from mcp_agent.core.proxies import (
39
+ BaseAgentProxy,
40
+ LLMAgentProxy,
41
+ WorkflowProxy,
42
+ RouterProxy,
43
+ ChainProxy,
44
+ )
45
+ from mcp_agent.core.types import AgentOrWorkflow, ProxyDict
46
+ from mcp_agent.core.exceptions import (
47
+ AgentConfigError,
48
+ CircularDependencyError,
49
+ ModelConfigError,
50
+ PromptExitError,
51
+ ServerConfigError,
52
+ ProviderKeyError,
53
+ ServerInitializationError,
54
+ )
61
55
 
62
- BASIC = "agent"
63
- ORCHESTRATOR = "orchestrator"
64
- PARALLEL = "parallel"
65
- EVALUATOR_OPTIMIZER = "evaluator_optimizer"
66
- ROUTER = "router"
56
+ # TODO -- reinstate once Windows&Python 3.13 platform issues are fixed
57
+ # import readline # noqa: F401
67
58
 
59
+ from rich import print
68
60
 
69
61
  T = TypeVar("T") # For the wrapper classes
70
62
 
71
63
 
72
- class BaseAgentProxy:
73
- """Base class for all proxy types"""
74
-
75
- def __init__(self, app: MCPApp, name: str):
76
- self._app = app
77
- self._name = name
78
-
79
- async def __call__(self, message: Optional[str] = None) -> str:
80
- """Allow: agent.researcher('message')"""
81
- return await self.send(message)
82
-
83
- async def send(self, message: Optional[str] = None) -> str:
84
- """Allow: agent.researcher.send('message')"""
85
- if message is None:
86
- return await self.prompt()
87
- return await self.generate_str(message)
88
-
89
- async def prompt(self, default_prompt: str = "") -> str:
90
- """Allow: agent.researcher.prompt()"""
91
- return await self._app.prompt(self._name, default_prompt)
92
-
93
- async def generate_str(self, message: str) -> str:
94
- """Generate response for a message - must be implemented by subclasses"""
95
- raise NotImplementedError("Subclasses must implement generate_str")
96
-
97
-
98
- class AgentProxy(BaseAgentProxy):
99
- """Legacy proxy for individual agent operations"""
100
-
101
- async def generate_str(self, message: str) -> str:
102
- return await self._app.send(self._name, message)
103
-
104
-
105
- class LLMAgentProxy(BaseAgentProxy):
106
- """Proxy for regular agents that use _llm.generate_str()"""
107
-
108
- def __init__(self, app: MCPApp, name: str, agent: Agent):
109
- super().__init__(app, name)
110
- self._agent = agent
111
-
112
- async def generate_str(self, message: str) -> str:
113
- return await self._agent._llm.generate_str(message)
114
-
115
-
116
- class WorkflowProxy(BaseAgentProxy):
117
- """Proxy for workflow types that implement generate_str() directly"""
118
-
119
- def __init__(self, app: MCPApp, name: str, workflow: WorkflowType):
120
- super().__init__(app, name)
121
- self._workflow = workflow
122
-
123
- async def generate_str(self, message: str) -> str:
124
- return await self._workflow.generate_str(message)
125
-
126
-
127
- class RouterProxy(BaseAgentProxy):
128
- """Proxy for LLM Routers"""
129
-
130
- def __init__(self, app: MCPApp, name: str, workflow: WorkflowType):
131
- super().__init__(app, name)
132
- self._workflow = workflow
133
-
134
- async def generate_str(self, message: str) -> str:
135
- results = await self._workflow.route(message)
136
- if not results:
137
- return "No appropriate route found for the request."
138
-
139
- # Get the top result
140
- top_result = results[0]
141
- if isinstance(top_result.result, Agent):
142
- # Agent route - delegate to the agent
143
- agent = top_result.result
144
-
145
- return await agent._llm.generate_str(message)
146
- elif isinstance(top_result.result, str):
147
- # Server route - use the router directly
148
- return "Tool call requested by router - not yet supported"
149
-
150
- return f"Routed to: {top_result.result} ({top_result.confidence}): {top_result.reasoning}"
151
-
152
-
153
- class AgentApp:
154
- """Main application wrapper"""
155
-
156
- def __init__(self, app: MCPApp, agents: ProxyDict):
157
- self._app = app
158
- self._agents = agents
159
- # Optional: set default agent for direct calls
160
- self._default = next(iter(agents)) if agents else None
161
-
162
- async def send(self, agent_name: str, message: Optional[str]) -> str:
163
- """Core message sending"""
164
- if agent_name not in self._agents:
165
- raise ValueError(f"No agent named '{agent_name}'")
166
-
167
- if not message or "" == message:
168
- return await self.prompt(agent_name)
169
-
170
- proxy = self._agents[agent_name]
171
- return await proxy.generate_str(message)
172
-
173
- async def prompt(self, agent_name: Optional[str] = None, default: str = "") -> str:
174
- """
175
- Interactive prompt for sending messages with advanced features.
176
-
177
- Args:
178
- agent_name: Optional target agent name (uses default if not specified)
179
- default: Default message to use when user presses enter
180
- """
181
- from .enhanced_prompt import get_enhanced_input, handle_special_commands
182
-
183
- agent = agent_name or self._default
184
-
185
- if agent not in self._agents:
186
- raise ValueError(f"No agent named '{agent}'")
187
-
188
- # Pass all available agent names for auto-completion
189
- available_agents = list(self._agents.keys())
190
-
191
- # Create agent_types dictionary mapping agent names to their types
192
- agent_types = {}
193
- for name, proxy in self._agents.items():
194
- # Determine agent type based on the proxy type
195
- if isinstance(proxy, LLMAgentProxy):
196
- # Convert AgentType.BASIC.value ("agent") to "Agent"
197
- agent_types[name] = "Agent"
198
- elif isinstance(proxy, RouterProxy):
199
- agent_types[name] = "Router"
200
- elif isinstance(proxy, WorkflowProxy):
201
- # For workflow proxies, check the workflow type
202
- workflow = proxy._workflow
203
- if isinstance(workflow, Orchestrator):
204
- agent_types[name] = "Orchestrator"
205
- elif isinstance(workflow, ParallelLLM):
206
- agent_types[name] = "Parallel"
207
- elif isinstance(workflow, EvaluatorOptimizerLLM):
208
- agent_types[name] = "Evaluator"
209
- else:
210
- agent_types[name] = "Workflow"
211
-
212
- result = ""
213
- while True:
214
- with progress_display.paused():
215
- # Use the enhanced input method with advanced features
216
- user_input = await get_enhanced_input(
217
- agent_name=agent,
218
- default=default,
219
- show_default=(default != ""),
220
- show_stop_hint=True,
221
- multiline=False, # Default to single-line mode
222
- available_agent_names=available_agents,
223
- syntax=None, # Can enable syntax highlighting for code input
224
- agent_types=agent_types, # Pass agent types for display
225
- )
226
-
227
- # Handle special commands
228
- command_result = await handle_special_commands(user_input, self)
229
-
230
- # Check if we should switch agents
231
- if (
232
- isinstance(command_result, dict)
233
- and "switch_agent" in command_result
234
- ):
235
- agent = command_result["switch_agent"]
236
- continue
237
-
238
- # Skip further processing if command was handled
239
- if command_result:
240
- continue
241
-
242
- if user_input.upper() == "STOP":
243
- return
244
- if user_input == "":
245
- continue
246
-
247
- result = await self.send(agent, user_input)
248
-
249
- return result
250
-
251
- def __getattr__(self, name: str) -> AgentProxy:
252
- """Support: agent.researcher"""
253
- if name not in self._agents:
254
- raise AttributeError(f"No agent named '{name}'")
255
- return AgentProxy(self, name)
256
-
257
- def __getitem__(self, name: str) -> AgentProxy:
258
- """Support: agent['researcher']"""
259
- if name not in self._agents:
260
- raise KeyError(f"No agent named '{name}'")
261
- return AgentProxy(self, name)
262
-
263
- async def __call__(
264
- self, message: Optional[str] = "", agent_name: Optional[str] = None
265
- ) -> str:
266
- """Support: agent('message')"""
267
- target = agent_name or self._default
268
- if not target:
269
- raise ValueError("No default agent available")
270
- return await self.send(target, message)
271
-
272
-
273
64
  class FastAgent(ContextDependent):
274
65
  """
275
66
  A decorator-based interface for MCP Agent applications.
@@ -293,11 +84,27 @@ class FastAgent(ContextDependent):
293
84
  "--model",
294
85
  help="Override the default model for all agents. Precedence is default < config_file < command line < constructor",
295
86
  )
87
+ parser.add_argument(
88
+ "--agent",
89
+ help="Specify the agent to send a message to (used with --message)",
90
+ )
91
+ parser.add_argument(
92
+ "-m", "--message",
93
+ help="Message to send to the specified agent (requires --agent)",
94
+ )
95
+ parser.add_argument(
96
+ "--quiet", action="store_true",
97
+ help="Disable progress display, tool and message logging for cleaner output",
98
+ )
296
99
  self.args = parser.parse_args()
100
+
101
+ # Quiet mode will be handled in _load_config()
297
102
 
298
103
  self.name = name
299
104
  self.config_path = config_path
300
105
  self._load_config()
106
+
107
+ # Create the MCPApp with the config
301
108
  self.app = MCPApp(
302
109
  name=name,
303
110
  settings=Settings(**self.config) if hasattr(self, "config") else None,
@@ -323,8 +130,9 @@ class FastAgent(ContextDependent):
323
130
  if agent_type not in [
324
131
  AgentType.PARALLEL.value,
325
132
  AgentType.EVALUATOR_OPTIMIZER.value,
133
+ AgentType.CHAIN.value,
326
134
  ]:
327
- self._log_agent_load(name)
135
+ log_agent_load(self.app, name)
328
136
  if agent_type == AgentType.BASIC.value:
329
137
  if not isinstance(instance, Agent):
330
138
  raise TypeError(
@@ -355,6 +163,10 @@ class FastAgent(ContextDependent):
355
163
  f"Expected LLMRouter instance for {name}, got {type(instance)}"
356
164
  )
357
165
  return RouterProxy(self.app, name, instance)
166
+ elif agent_type == AgentType.CHAIN.value:
167
+ # Chain proxy is directly returned from _create_agents_by_type
168
+ # No need for type checking as it's already a ChainProxy
169
+ return instance
358
170
  else:
359
171
  raise ValueError(f"Unknown agent type: {agent_type}")
360
172
 
@@ -367,7 +179,7 @@ class FastAgent(ContextDependent):
367
179
  """Load configuration from YAML file, properly handling without dotenv processing"""
368
180
  if self.config_path:
369
181
  with open(self.config_path) as f:
370
- self.config = yaml.safe_load(f)
182
+ self.config = yaml.safe_load(f) or {}
371
183
 
372
184
  def _validate_server_references(self) -> None:
373
185
  """
@@ -461,6 +273,15 @@ class FastAgent(ContextDependent):
461
273
  f"Evaluator-Optimizer '{name}' references non-existent components: {', '.join(missing)}"
462
274
  )
463
275
 
276
+ elif agent_type == AgentType.CHAIN.value:
277
+ # Check that all agents in the sequence exist
278
+ sequence = agent_data.get("sequence", agent_data.get("agents", []))
279
+ missing = [a for a in sequence if a not in available_components]
280
+ if missing:
281
+ raise AgentConfigError(
282
+ f"Chain '{name}' references non-existent agents: {', '.join(missing)}"
283
+ )
284
+
464
285
  def _get_model_factory(
465
286
  self,
466
287
  model: Optional[str] = None,
@@ -599,6 +420,7 @@ class FastAgent(ContextDependent):
599
420
  def agent(
600
421
  self,
601
422
  name: str = "Agent",
423
+ instruction_or_kwarg: str = None,
602
424
  *,
603
425
  instruction: str = "You are a helpful agent.",
604
426
  servers: List[str] = [],
@@ -612,13 +434,26 @@ class FastAgent(ContextDependent):
612
434
 
613
435
  Args:
614
436
  name: Name of the agent
615
- instruction: Base instruction for the agent
437
+ instruction_or_kwarg: Optional positional parameter for instruction
438
+ instruction: Base instruction for the agent (keyword arg)
616
439
  servers: List of server names the agent should connect to
617
440
  model: Model specification string (highest precedence)
618
441
  use_history: Whether to maintain conversation history
619
442
  request_params: Additional request parameters for the LLM
620
443
  human_input: Whether to enable human input capabilities
444
+
445
+ The instruction can be provided either as a second positional argument
446
+ or as a keyword argument. Positional argument takes precedence when both are provided.
447
+
448
+ Usage:
449
+ @fast.agent("agent_name", "Your instruction here") # Using positional arg
450
+ @fast.agent("agent_name", instruction="Your instruction here") # Using keyword arg
621
451
  """
452
+ # Use positional argument if provided, otherwise use keyword argument
453
+ final_instruction = (
454
+ instruction_or_kwarg if instruction_or_kwarg is not None else instruction
455
+ )
456
+
622
457
  decorator = self._create_decorator(
623
458
  AgentType.BASIC,
624
459
  default_name="Agent",
@@ -626,7 +461,7 @@ class FastAgent(ContextDependent):
626
461
  default_use_history=True,
627
462
  )(
628
463
  name=name,
629
- instruction=instruction,
464
+ instruction=final_instruction,
630
465
  servers=servers,
631
466
  model=model,
632
467
  use_history=use_history,
@@ -687,25 +522,47 @@ class FastAgent(ContextDependent):
687
522
  def parallel(
688
523
  self,
689
524
  name: str,
690
- fan_in: str,
691
525
  fan_out: List[str],
526
+ fan_in: Optional[str] = None,
692
527
  instruction: str = "",
693
528
  model: str | None = None,
694
529
  use_history: bool = True,
695
530
  request_params: Optional[Dict] = None,
531
+ include_request: bool = True,
696
532
  ) -> Callable:
697
533
  """
698
534
  Decorator to create and register a parallel executing agent.
699
535
 
700
536
  Args:
701
537
  name: Name of the parallel executing agent
702
- fan_in: Name of collecting agent
703
538
  fan_out: List of parallel execution agents
539
+ fan_in: Optional name of collecting agent. If not provided, a passthrough agent
540
+ will be created automatically with the name "{name}_fan_in"
704
541
  instruction: Optional instruction for the parallel agent
705
542
  model: Model specification string
706
543
  use_history: Whether to maintain conversation history
707
544
  request_params: Additional request parameters for the LLM
545
+ include_request: Whether to include the original request in the fan-in message
708
546
  """
547
+ # If fan_in is not provided, create a passthrough agent with a derived name
548
+ if fan_in is None:
549
+ passthrough_name = f"{name}_fan_in"
550
+
551
+ # Register the passthrough agent directly in self.agents
552
+ self.agents[passthrough_name] = {
553
+ "config": AgentConfig(
554
+ name=passthrough_name,
555
+ instruction=f"Passthrough fan-in for {name}",
556
+ servers=[],
557
+ use_history=use_history,
558
+ ),
559
+ "type": AgentType.BASIC.value, # Using BASIC type since we're just attaching a PassthroughLLM
560
+ "func": lambda x: x, # Simple passthrough function (never actually called)
561
+ }
562
+
563
+ # Use this passthrough as the fan-in
564
+ fan_in = passthrough_name
565
+
709
566
  decorator = self._create_decorator(
710
567
  AgentType.PARALLEL,
711
568
  default_instruction="",
@@ -719,6 +576,7 @@ class FastAgent(ContextDependent):
719
576
  model=model,
720
577
  use_history=use_history,
721
578
  request_params=request_params,
579
+ include_request=include_request,
722
580
  )
723
581
  return decorator
724
582
 
@@ -799,6 +657,88 @@ class FastAgent(ContextDependent):
799
657
  )
800
658
  return decorator
801
659
 
660
+ def chain(
661
+ self,
662
+ name: str = "Chain",
663
+ *,
664
+ sequence: List[str] = None,
665
+ agents: List[str] = None, # Alias for sequence
666
+ instruction: str = None,
667
+ model: str | None = None,
668
+ use_history: bool = True,
669
+ request_params: Optional[Dict] = None,
670
+ continue_with_final: bool = True,
671
+ ) -> Callable:
672
+ """
673
+ Decorator to create and register a chain of agents.
674
+
675
+ Args:
676
+ name: Name of the chain
677
+ sequence: List of agent names in order of execution (preferred name)
678
+ agents: Alias for sequence (backwards compatibility)
679
+ instruction: Optional custom instruction for the chain (if none provided, will autogenerate based on sequence)
680
+ model: Model specification string (not used directly in chain)
681
+ use_history: Whether to maintain conversation history
682
+ request_params: Additional request parameters
683
+ continue_with_final: When using prompt(), whether to continue with the final agent after processing chain (default: True)
684
+ """
685
+ # Support both parameter names
686
+ agent_sequence = sequence or agents
687
+ if agent_sequence is None:
688
+ raise ValueError("Either 'sequence' or 'agents' parameter must be provided")
689
+
690
+ # Auto-generate instruction if not provided
691
+ if instruction is None:
692
+ # We'll generate it later when we have access to the agent configs and can see servers
693
+ instruction = f"Chain of agents: {', '.join(agent_sequence)}"
694
+
695
+ decorator = self._create_decorator(
696
+ AgentType.CHAIN,
697
+ default_name="Chain",
698
+ default_instruction=instruction,
699
+ default_use_history=True,
700
+ wrapper_needed=True,
701
+ )(
702
+ name=name,
703
+ sequence=agent_sequence,
704
+ instruction=instruction,
705
+ model=model,
706
+ use_history=use_history,
707
+ request_params=request_params,
708
+ continue_with_final=continue_with_final,
709
+ )
710
+ return decorator
711
+
712
+ def passthrough(
713
+ self,
714
+ name: str = "Passthrough",
715
+ use_history: bool = True,
716
+ **kwargs
717
+ ) -> Callable:
718
+ """
719
+ Decorator to create and register a passthrough agent.
720
+ A passthrough agent simply returns any input message without modification.
721
+
722
+ This is useful for parallel workflows where no fan-in aggregation is needed
723
+ (the fan-in agent can be a passthrough that simply returns the combined outputs).
724
+
725
+ Args:
726
+ name: Name of the passthrough agent
727
+ use_history: Whether to maintain conversation history
728
+ **kwargs: Additional parameters (ignored, for compatibility)
729
+ """
730
+ decorator = self._create_decorator(
731
+ AgentType.BASIC, # Using BASIC agent type since we'll use a regular agent with PassthroughLLM
732
+ default_name="Passthrough",
733
+ default_instruction="Passthrough agent that returns input without modification",
734
+ default_use_history=use_history,
735
+ wrapper_needed=True,
736
+ )(
737
+ name=name,
738
+ use_history=use_history,
739
+ )
740
+ return decorator
741
+
802
742
  async def _create_agents_by_type(
803
743
  self,
804
744
  agent_app: MCPApp,
@@ -832,19 +772,42 @@ class FastAgent(ContextDependent):
832
772
 
833
773
  # Type-specific initialization
834
774
  if agent_type == AgentType.BASIC:
835
- # Create basic agent with configuration
836
- agent = Agent(config=config, context=agent_app.context)
837
-
838
- # Set up LLM with proper configuration
839
- async with agent:
840
- llm_factory = self._get_model_factory(
841
- model=config.model,
842
- request_params=config.default_request_params,
843
- )
844
- agent._llm = await agent.attach_llm(llm_factory)
845
-
846
- # Store the agent
847
- instance = agent
775
+ # Get the agent name for special handling
776
+ agent_name = agent_data["config"].name
777
+
778
+ # Check if this is an agent that should use the PassthroughLLM
779
+ if agent_name.endswith("_fan_in") or agent_name.startswith("passthrough"):
780
+ # Import here to avoid circular imports
781
+ from mcp_agent.workflows.llm.augmented_llm import PassthroughLLM
782
+
783
+ # Create basic agent with configuration
784
+ agent = Agent(config=config, context=agent_app.context)
785
+
786
+ # Set up a PassthroughLLM directly
787
+ async with agent:
788
+ agent._llm = PassthroughLLM(
789
+ name=f"{config.name}_llm",
790
+ context=agent_app.context,
791
+ agent=agent,
792
+ default_request_params=config.default_request_params,
793
+ )
794
+
795
+ # Store the agent
796
+ instance = agent
797
+ else:
798
+ # Standard basic agent with LLM
799
+ agent = Agent(config=config, context=agent_app.context)
800
+
801
+ # Set up LLM with proper configuration
802
+ async with agent:
803
+ llm_factory = self._get_model_factory(
804
+ model=config.model,
805
+ request_params=config.default_request_params,
806
+ )
807
+ agent._llm = await agent.attach_llm(llm_factory)
808
+
809
+ # Store the agent
810
+ instance = agent
848
811
 
849
812
  elif agent_type == AgentType.ORCHESTRATOR:
850
813
  # Get base params configured with model settings
@@ -958,6 +921,54 @@ class FastAgent(ContextDependent):
958
921
  verb=ProgressAction.ROUTING, # Set verb for progress display
959
922
  )
960
923
 
924
+ elif agent_type == AgentType.CHAIN:
925
+ # Get the sequence from either parameter
926
+ sequence = agent_data.get("sequence", agent_data.get("agents", []))
927
+
928
+ # Auto-generate instruction if not provided or if it's just the default
929
+ default_instruction = f"Chain of agents: {', '.join(sequence)}"
930
+
931
+ # If user provided a custom instruction, use that
932
+ # Otherwise, generate a description based on the sequence and their servers
933
+ if config.instruction == default_instruction:
934
+ # Get all agent names in the sequence
935
+ agent_names = []
936
+ all_servers = set()
937
+
938
+ # Collect information about the agents and their servers
939
+ for agent_name in sequence:
940
+ if agent_name in active_agents:
941
+ agent_proxy = active_agents[agent_name]
942
+ if hasattr(agent_proxy, "_agent"):
943
+ # For LLMAgentProxy
944
+ agent_instance = agent_proxy._agent
945
+ agent_names.append(agent_name)
946
+ if hasattr(agent_instance, "server_names"):
947
+ all_servers.update(agent_instance.server_names)
948
+ elif hasattr(agent_proxy, "_workflow"):
949
+ # For WorkflowProxy
950
+ agent_names.append(agent_name)
951
+
952
+ # Generate a better description
953
+ if agent_names:
954
+ server_part = (
955
+ f" with access to servers: {', '.join(sorted(all_servers))}"
956
+ if all_servers
957
+ else ""
958
+ )
959
+ config.instruction = f"Sequence of agents: {', '.join(agent_names)}{server_part}."
960
+
961
+ # Create a ChainProxy without needing a new instance
962
+ # Just pass the agent proxies and sequence
963
+ instance = ChainProxy(self.app, name, sequence, active_agents)
964
+ # Set continue_with_final behavior from configuration
965
+ instance._continue_with_final = agent_data.get(
966
+ "continue_with_final", True
967
+ )
968
+
969
+ # We removed the AgentType.PASSTHROUGH case
970
+ # Passthrough agents are now created as BASIC agents with a special LLM
971
+
961
972
  elif agent_type == AgentType.PARALLEL:
962
973
  # Get fan-out agents (could be basic agents or other parallels)
963
974
  fan_out_agents = self._get_agent_instances(
@@ -979,6 +990,7 @@ class FastAgent(ContextDependent):
979
990
  context=agent_app.context,
980
991
  llm_factory=llm_factory,
981
992
  default_request_params=config.default_request_params,
993
+ include_request=agent_data.get("include_request", True),
982
994
  )
983
995
 
984
996
  else:
@@ -1037,16 +1049,18 @@ class FastAgent(ContextDependent):
1037
1049
  agent_app, AgentType.EVALUATOR_OPTIMIZER, active_agents
1038
1050
  )
1039
1051
 
1040
- def _get_parallel_dependencies(
1041
- self, name: str, visited: set, path: set
1052
+ def _get_dependencies(
1053
+ self, name: str, visited: set, path: set, agent_type: AgentType = None
1042
1054
  ) -> List[str]:
1043
1055
  """
1044
- Get dependencies for a parallel agent in topological order.
1056
+ Get dependencies for an agent in topological order.
1057
+ Works for both Parallel and Chain workflows.
1045
1058
 
1046
1059
  Args:
1047
- name: Name of the parallel agent
1060
+ name: Name of the agent
1048
1061
  visited: Set of already visited agents
1049
1062
  path: Current path for cycle detection
1063
+ agent_type: Optional type filter (e.g., only check Parallel or Chain)
1050
1064
 
1051
1065
  Returns:
1052
1066
  List of agent names in dependency order
@@ -1056,7 +1070,7 @@ class FastAgent(ContextDependent):
1056
1070
  """
1057
1071
  if name in path:
1058
1072
  path_str = " -> ".join(path)
1059
- raise ValueError(f"Circular dependency detected: {path_str} -> {name}")
1073
+ raise CircularDependencyError(f"Path: {path_str} -> {name}")
1060
1074
 
1061
1075
  if name in visited:
1062
1076
  return []
@@ -1065,15 +1079,26 @@ class FastAgent(ContextDependent):
1065
1079
  return []
1066
1080
 
1067
1081
  config = self.agents[name]
1068
- if config["type"] != AgentType.PARALLEL.value:
1082
+
1083
+ # Skip if not the requested type (when filtering)
1084
+ if agent_type and config["type"] != agent_type.value:
1069
1085
  return []
1070
1086
 
1071
1087
  path.add(name)
1072
1088
  deps = []
1073
1089
 
1074
- # Get dependencies from fan-out agents
1075
- for fan_out in config["fan_out"]:
1076
- deps.extend(self._get_parallel_dependencies(fan_out, visited, path))
1090
+ # Handle dependencies based on agent type
1091
+ if config["type"] == AgentType.PARALLEL.value:
1092
+ # Get dependencies from fan-out agents
1093
+ for fan_out in config["fan_out"]:
1094
+ deps.extend(self._get_dependencies(fan_out, visited, path, agent_type))
1095
+ elif config["type"] == AgentType.CHAIN.value:
1096
+ # Get dependencies from sequence agents
1097
+ sequence = config.get("sequence", config.get("agents", []))
1098
+ for agent_name in sequence:
1099
+ deps.extend(
1100
+ self._get_dependencies(agent_name, visited, path, agent_type)
1101
+ )
1077
1102
 
1078
1103
  # Add this agent after its dependencies
1079
1104
  deps.append(name)
@@ -1082,54 +1107,95 @@ class FastAgent(ContextDependent):
1082
1107
 
1083
1108
  return deps
1084
1109
 
1085
- async def _create_parallel_agents(
1086
- self, agent_app: MCPApp, active_agents: ProxyDict
1110
+ def _get_parallel_dependencies(
1111
+ self, name: str, visited: set, path: set
1112
+ ) -> List[str]:
1113
+ """
1114
+ Get dependencies for a parallel agent in topological order.
1115
+ Legacy function that calls the more general _get_dependencies.
1116
+
1117
+ Args:
1118
+ name: Name of the parallel agent
1119
+ visited: Set of already visited agents
1120
+ path: Current path for cycle detection
1121
+
1122
+ Returns:
1123
+ List of agent names in dependency order
1124
+
1125
+ Raises:
1126
+ ValueError: If circular dependency detected
1127
+ """
1128
+ return self._get_dependencies(name, visited, path, AgentType.PARALLEL)
1129
+
1130
+ async def _create_agents_in_dependency_order(
1131
+ self, agent_app: MCPApp, active_agents: ProxyDict, agent_type: AgentType
1087
1132
  ) -> ProxyDict:
1088
1133
  """
1089
- Create parallel execution agents in dependency order.
1134
+ Create agents in dependency order to avoid circular references.
1135
+ Works for both Parallel and Chain workflows.
1090
1136
 
1091
1137
  Args:
1092
1138
  agent_app: The main application instance
1093
1139
  active_agents: Dictionary of already created agents/proxies
1140
+ agent_type: Type of agents to create (AgentType.PARALLEL or AgentType.CHAIN)
1094
1141
 
1095
1142
  Returns:
1096
- Dictionary of initialized parallel agents
1143
+ Dictionary of initialized agents
1097
1144
  """
1098
- parallel_agents = {}
1145
+ result_agents = {}
1099
1146
  visited = set()
1100
1147
 
1101
- # Get all parallel agents
1102
- parallel_names = [
1148
+ # Get all agents of the specified type
1149
+ agent_names = [
1103
1150
  name
1104
1151
  for name, agent_data in self.agents.items()
1105
- if agent_data["type"] == AgentType.PARALLEL.value
1152
+ if agent_data["type"] == agent_type.value
1106
1153
  ]
1107
1154
 
1108
1155
  # Create agents in dependency order
1109
- for name in parallel_names:
1156
+ for name in agent_names:
1110
1157
  # Get ordered dependencies if not already processed
1111
1158
  if name not in visited:
1112
1159
  try:
1113
- ordered_agents = self._get_parallel_dependencies(
1114
- name, visited, set()
1160
+ ordered_agents = self._get_dependencies(
1161
+ name, visited, set(), agent_type
1115
1162
  )
1116
1163
  except ValueError as e:
1117
- raise ValueError(f"Error creating parallel agent {name}: {str(e)}")
1164
+ raise ValueError(
1165
+ f"Error creating {agent_type.name.lower()} agent {name}: {str(e)}"
1166
+ )
1118
1167
 
1119
1168
  # Create each agent in order
1120
1169
  for agent_name in ordered_agents:
1121
- if agent_name not in parallel_agents:
1122
- # Create one parallel agent at a time using the generic method
1170
+ if agent_name not in result_agents:
1171
+ # Create one agent at a time using the generic method
1123
1172
  agent_result = await self._create_agents_by_type(
1124
1173
  agent_app,
1125
- AgentType.PARALLEL,
1174
+ agent_type,
1126
1175
  active_agents,
1127
1176
  agent_name=agent_name,
1128
1177
  )
1129
1178
  if agent_name in agent_result:
1130
- parallel_agents[agent_name] = agent_result[agent_name]
1179
+ result_agents[agent_name] = agent_result[agent_name]
1131
1180
 
1132
- return parallel_agents
1181
+ return result_agents
1182
+
1183
+ async def _create_parallel_agents(
1184
+ self, agent_app: MCPApp, active_agents: ProxyDict
1185
+ ) -> ProxyDict:
1186
+ """
1187
+ Create parallel execution agents in dependency order.
1188
+
1189
+ Args:
1190
+ agent_app: The main application instance
1191
+ active_agents: Dictionary of already created agents/proxies
1192
+
1193
+ Returns:
1194
+ Dictionary of initialized parallel agents
1195
+ """
1196
+ return await self._create_agents_in_dependency_order(
1197
+ agent_app, active_agents, AgentType.PARALLEL
1198
+ )
1133
1199
 
1134
1200
  async def _create_routers(
1135
1201
  self, agent_app: MCPApp, active_agents: ProxyDict
@@ -1158,9 +1224,7 @@ class FastAgent(ContextDependent):
1158
1224
  Returns:
1159
1225
  The underlying Agent or workflow instance
1160
1226
  """
1161
- if isinstance(proxy, LLMAgentProxy):
1162
- return proxy._agent
1163
- return proxy._workflow
1227
+ return unwrap_proxy(proxy)
1164
1228
 
1165
1229
  def _get_agent_instances(
1166
1230
  self, agent_names: List[str], active_agents: ProxyDict
@@ -1175,7 +1239,7 @@ class FastAgent(ContextDependent):
1175
1239
  Returns:
1176
1240
  List of unwrapped agent/workflow instances
1177
1241
  """
1178
- return [self._unwrap_proxy(active_agents[name]) for name in agent_names]
1242
+ return get_agent_instances(agent_names, active_agents)
1179
1243
 
1180
1244
  @asynccontextmanager
1181
1245
  async def run(self):
@@ -1185,14 +1249,30 @@ class FastAgent(ContextDependent):
1185
1249
  """
1186
1250
  active_agents = {}
1187
1251
  had_error = False
1252
+
1253
+ # Handle quiet mode by disabling logger settings after initialization
1254
+ quiet_mode = hasattr(self, "args") and self.args.quiet
1255
+
1188
1256
  try:
1189
1257
  async with self.app.run() as agent_app:
1258
+ # Apply quiet mode directly to the context's config if needed
1259
+ if quiet_mode and hasattr(agent_app.context, "config") and hasattr(agent_app.context.config, "logger"):
1260
+ # Apply after initialization but before agents are created
1261
+ agent_app.context.config.logger.progress_display = False
1262
+ agent_app.context.config.logger.show_chat = False
1263
+ agent_app.context.config.logger.show_tools = False
1264
+
1265
+ # Directly disable the progress display singleton
1266
+ from mcp_agent.progress_display import progress_display
1267
+ progress_display.stop() # This will stop and hide the display
1268
+
1190
1269
  # Pre-flight validation
1191
1270
  self._validate_server_references()
1192
1271
  self._validate_workflow_references()
1193
1272
 
1194
1273
  # Create all types of agents in dependency order
1195
1274
  active_agents = await self._create_basic_agents(agent_app)
1275
+
1196
1276
  orchestrators = await self._create_orchestrators(
1197
1277
  agent_app, active_agents
1198
1278
  )
@@ -1203,15 +1283,43 @@ class FastAgent(ContextDependent):
1203
1283
  agent_app, active_agents
1204
1284
  )
1205
1285
  routers = await self._create_routers(agent_app, active_agents)
1286
+ chains = await self._create_agents_in_dependency_order(
1287
+ agent_app, active_agents, AgentType.CHAIN
1288
+ )
1206
1289
 
1207
1290
  # Merge all agents into active_agents
1208
1291
  active_agents.update(orchestrators)
1209
1292
  active_agents.update(parallel_agents)
1210
1293
  active_agents.update(evaluator_optimizers)
1211
1294
  active_agents.update(routers)
1295
+ active_agents.update(chains)
1212
1296
 
1213
1297
  # Create wrapper with all agents
1214
1298
  wrapper = AgentApp(agent_app, active_agents)
1299
+
1300
+ # Handle direct message sending if --agent and --message are provided
1301
+ if self.args.agent and self.args.message:
1302
+ agent_name = self.args.agent
1303
+ message = self.args.message
1304
+
1305
+ if agent_name not in active_agents:
1306
+ available_agents = ", ".join(active_agents.keys())
1307
+ print(f"\n\nError: Agent '{agent_name}' not found. Available agents: {available_agents}")
1308
+ raise SystemExit(1)
1309
+
1310
+ try:
1311
+ # Get response
1312
+ response = await wrapper[agent_name].send(message)
1313
+
1314
+ # Only print the response in quiet mode
1315
+ if self.args.quiet:
1316
+ print(f"{response}")
1317
+
1318
+ raise SystemExit(0)
1319
+ except Exception as e:
1320
+ print(f"\n\nError sending message to agent '{agent_name}': {str(e)}")
1321
+ raise SystemExit(1)
1322
+
1215
1323
  yield wrapper
1216
1324
 
1217
1325
  except ServerConfigError as e:
@@ -1259,6 +1367,15 @@ class FastAgent(ContextDependent):
1259
1367
  )
1260
1368
  raise SystemExit(1)
1261
1369
 
1370
+ except CircularDependencyError as e:
1371
+ had_error = True
1372
+ self._handle_error(
1373
+ e,
1374
+ "Circular Dependency Detected",
1375
+ "Check your agent configuration for circular dependencies.",
1376
+ )
1377
+ raise SystemExit(1)
1378
+
1262
1379
  except PromptExitError as e:
1263
1380
  had_error = True
1264
1381
  self._handle_error(
@@ -1289,19 +1406,8 @@ class FastAgent(ContextDependent):
1289
1406
  error_type: Type of error to display
1290
1407
  suggestion: Optional suggestion message to display
1291
1408
  """
1292
- print(f"\n[bold red]{error_type}:")
1293
- print(getattr(e, "message", str(e)))
1294
- if hasattr(e, "details") and e.details:
1295
- print("\nDetails:")
1296
- print(e.details)
1297
- if suggestion:
1298
- print(f"\n{suggestion}")
1409
+ handle_error(e, error_type, suggestion)
1299
1410
 
1300
1411
  def _log_agent_load(self, agent_name: str) -> None:
1301
- self.app._logger.info(
1302
- f"Loaded {agent_name}",
1303
- data={
1304
- "progress_action": ProgressAction.LOADED,
1305
- "agent_name": agent_name,
1306
- },
1307
- )
1412
+ # Using the imported function
1413
+ log_agent_load(self.app, agent_name)