fast-agent-mcp 0.0.8__py3-none-any.whl → 0.0.11__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 (28) hide show
  1. {fast_agent_mcp-0.0.8.dist-info → fast_agent_mcp-0.0.11.dist-info}/METADATA +15 -9
  2. {fast_agent_mcp-0.0.8.dist-info → fast_agent_mcp-0.0.11.dist-info}/RECORD +28 -26
  3. mcp_agent/app.py +4 -4
  4. mcp_agent/cli/commands/bootstrap.py +4 -0
  5. mcp_agent/cli/commands/setup.py +1 -1
  6. mcp_agent/core/fastagent.py +498 -369
  7. mcp_agent/event_progress.py +5 -2
  8. mcp_agent/human_input/handler.py +6 -2
  9. mcp_agent/logging/rich_progress.py +10 -5
  10. mcp_agent/mcp/mcp_aggregator.py +2 -1
  11. mcp_agent/mcp/mcp_connection_manager.py +67 -37
  12. mcp_agent/resources/examples/data-analysis/analysis.py +1 -1
  13. mcp_agent/resources/examples/data-analysis/fastagent.config.yaml +2 -0
  14. mcp_agent/resources/examples/internal/job.py +83 -0
  15. mcp_agent/resources/examples/workflows/agent_build.py +61 -0
  16. mcp_agent/resources/examples/workflows/chaining.py +0 -1
  17. mcp_agent/resources/examples/workflows/human_input.py +0 -1
  18. mcp_agent/resources/examples/workflows/orchestrator.py +1 -7
  19. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +63 -65
  20. mcp_agent/workflows/llm/augmented_llm.py +9 -1
  21. mcp_agent/workflows/llm/augmented_llm_anthropic.py +28 -23
  22. mcp_agent/workflows/llm/model_factory.py +25 -11
  23. mcp_agent/workflows/orchestrator/orchestrator.py +106 -100
  24. mcp_agent/workflows/orchestrator/orchestrator_prompts.py +11 -6
  25. mcp_agent/workflows/router/router_llm.py +13 -2
  26. {fast_agent_mcp-0.0.8.dist-info → fast_agent_mcp-0.0.11.dist-info}/WHEEL +0 -0
  27. {fast_agent_mcp-0.0.8.dist-info → fast_agent_mcp-0.0.11.dist-info}/entry_points.txt +0 -0
  28. {fast_agent_mcp-0.0.8.dist-info → fast_agent_mcp-0.0.11.dist-info}/licenses/LICENSE +0 -0
@@ -19,6 +19,7 @@ from mcp_agent.core.exceptions import (
19
19
  from mcp_agent.app import MCPApp
20
20
  from mcp_agent.agents.agent import Agent, AgentConfig
21
21
  from mcp_agent.context_dependent import ContextDependent
22
+ from mcp_agent.event_progress import ProgressAction
22
23
  from mcp_agent.workflows.orchestrator.orchestrator import Orchestrator
23
24
  from mcp_agent.workflows.parallel.parallel_llm import ParallelLLM
24
25
  from mcp_agent.workflows.evaluator_optimizer.evaluator_optimizer import (
@@ -31,9 +32,10 @@ from rich.prompt import Prompt
31
32
  from rich import print
32
33
  from mcp_agent.progress_display import progress_display
33
34
  from mcp_agent.workflows.llm.model_factory import ModelFactory
34
- from mcp_agent.workflows.llm.augmented_llm import RequestParams
35
+ from mcp_agent.workflows.llm.augmented_llm import AugmentedLLM, RequestParams
35
36
 
36
- import readline # noqa: F401
37
+ # TODO -- resintate once Windows&Python 3.13 platform issues are fixed
38
+ # import readline # noqa: F401
37
39
 
38
40
  # Type aliases for better readability
39
41
  WorkflowType: TypeAlias = Union[
@@ -127,7 +129,9 @@ class RouterProxy(BaseAgentProxy):
127
129
  top_result = results[0]
128
130
  if isinstance(top_result.result, Agent):
129
131
  # Agent route - delegate to the agent
130
- return await top_result.result._llm.generate_str(message)
132
+ agent = top_result.result
133
+
134
+ return await agent._llm.generate_str(message)
131
135
  elif isinstance(top_result.result, str):
132
136
  # Server route - use the router directly
133
137
  return "Tool call requested by router - not yet supported"
@@ -222,6 +226,34 @@ class FastAgent(ContextDependent):
222
226
  Provides a simplified way to create and manage agents using decorators.
223
227
  """
224
228
 
229
+ def __init__(self, name: str, config_path: Optional[str] = None):
230
+ """
231
+ Initialize the decorator interface.
232
+
233
+ Args:
234
+ name: Name of the application
235
+ config_path: Optional path to config file
236
+ """
237
+ # Initialize ContextDependent
238
+ super().__init__()
239
+
240
+ # Setup command line argument parsing
241
+ parser = argparse.ArgumentParser(description="MCP Agent Application")
242
+ parser.add_argument(
243
+ "--model",
244
+ help="Override the default model for all agents. Precedence is default < config_file < command line < constructor",
245
+ )
246
+ self.args = parser.parse_args()
247
+
248
+ self.name = name
249
+ self.config_path = config_path
250
+ self._load_config()
251
+ self.app = MCPApp(
252
+ name=name,
253
+ settings=Settings(**self.config) if hasattr(self, "config") else None,
254
+ )
255
+ self.agents: Dict[str, Dict[str, Any]] = {}
256
+
225
257
  def _create_proxy(
226
258
  self, name: str, instance: AgentOrWorkflow, agent_type: str
227
259
  ) -> BaseAgentProxy:
@@ -238,6 +270,11 @@ class FastAgent(ContextDependent):
238
270
  Raises:
239
271
  TypeError: If instance type doesn't match expected type for agent_type
240
272
  """
273
+ if agent_type not in [
274
+ AgentType.PARALLEL.value,
275
+ AgentType.EVALUATOR_OPTIMIZER.value,
276
+ ]:
277
+ self._log_agent_load(name)
241
278
  if agent_type == AgentType.BASIC.value:
242
279
  if not isinstance(instance, Agent):
243
280
  raise TypeError(
@@ -271,34 +308,6 @@ class FastAgent(ContextDependent):
271
308
  else:
272
309
  raise ValueError(f"Unknown agent type: {agent_type}")
273
310
 
274
- def __init__(self, name: str, config_path: Optional[str] = None):
275
- """
276
- Initialize the decorator interface.
277
-
278
- Args:
279
- name: Name of the application
280
- config_path: Optional path to config file
281
- """
282
- # Initialize ContextDependent
283
- super().__init__()
284
-
285
- # Setup command line argument parsing
286
- parser = argparse.ArgumentParser(description="MCP Agent Application")
287
- parser.add_argument(
288
- "--model",
289
- help="Override the default model for all agents. Precedence is default < config_file < command line < constructor",
290
- )
291
- self.args = parser.parse_args()
292
-
293
- self.name = name
294
- self.config_path = config_path
295
- self._load_config()
296
- self.app = MCPApp(
297
- name=name,
298
- settings=Settings(**self.config) if hasattr(self, "config") else None,
299
- )
300
- self.agents: Dict[str, Dict[str, Any]] = {}
301
-
302
311
  @property
303
312
  def context(self):
304
313
  """Access the application context"""
@@ -334,7 +343,8 @@ class FastAgent(ContextDependent):
334
343
  def _validate_workflow_references(self) -> None:
335
344
  """
336
345
  Validate that all workflow references point to valid agents/workflows.
337
- Raises ValueError if any referenced components are not defined.
346
+ Also validates that referenced agents have required configuration.
347
+ Raises AgentConfigError if any validation fails.
338
348
  """
339
349
  available_components = set(self.agents.keys())
340
350
 
@@ -358,7 +368,7 @@ class FastAgent(ContextDependent):
358
368
  )
359
369
 
360
370
  elif agent_type == AgentType.ORCHESTRATOR.value:
361
- # Check all child agents exist
371
+ # Check all child agents exist and are properly configured
362
372
  child_agents = agent_data["child_agents"]
363
373
  missing = [a for a in child_agents if a not in available_components]
364
374
  if missing:
@@ -366,6 +376,18 @@ class FastAgent(ContextDependent):
366
376
  f"Orchestrator '{name}' references non-existent agents: {', '.join(missing)}"
367
377
  )
368
378
 
379
+ # Validate child agents have required LLM configuration
380
+ for agent_name in child_agents:
381
+ child_data = self.agents[agent_name]
382
+ if child_data["type"] == AgentType.BASIC.value:
383
+ # For basic agents, we'll validate LLM config during creation
384
+ continue
385
+ elif not isinstance(child_data["func"], AugmentedLLM):
386
+ raise AgentConfigError(
387
+ f"Agent '{agent_name}' used by orchestrator '{name}' lacks LLM capability",
388
+ "All agents used by orchestrators must be LLM-capable",
389
+ )
390
+
369
391
  elif agent_type == AgentType.ROUTER.value:
370
392
  # Check all referenced agents exist
371
393
  router_agents = agent_data["agents"]
@@ -426,13 +448,111 @@ class FastAgent(ContextDependent):
426
448
  # Let model factory handle the model string parsing and setup
427
449
  return ModelFactory.create_factory(model_spec, request_params=request_params)
428
450
 
451
+ def _create_decorator(
452
+ self,
453
+ agent_type: AgentType,
454
+ default_name: str = None,
455
+ default_instruction: str = None,
456
+ default_servers: List[str] = None,
457
+ default_use_history: bool = True,
458
+ wrapper_needed: bool = False,
459
+ **extra_defaults,
460
+ ) -> Callable:
461
+ """
462
+ Factory method for creating agent decorators with common behavior.
463
+
464
+ Args:
465
+ agent_type: Type of agent/workflow to create
466
+ default_name: Default name to use if not provided
467
+ default_instruction: Default instruction to use if not provided
468
+ default_servers: Default servers list to use if not provided
469
+ default_use_history: Default history setting
470
+ wrapper_needed: Whether to wrap the decorated function
471
+ **extra_defaults: Additional agent/workflow-specific parameters
472
+ """
473
+
474
+ def decorator_wrapper(**kwargs):
475
+ # Apply defaults for common parameters
476
+ name = kwargs.get("name", default_name or f"{agent_type.name.title()}")
477
+ instruction = kwargs.get("instruction", default_instruction or "")
478
+ servers = kwargs.get("servers", default_servers or [])
479
+ model = kwargs.get("model", None)
480
+ use_history = kwargs.get("use_history", default_use_history)
481
+ request_params = kwargs.get("request_params", None)
482
+ human_input = kwargs.get("human_input", False)
483
+
484
+ # Create base request params
485
+ def decorator(func: Callable) -> Callable:
486
+ # Create base request params
487
+ if (
488
+ request_params is not None
489
+ or model is not None
490
+ or use_history != default_use_history
491
+ ):
492
+ max_tokens = 4096 if agent_type == AgentType.BASIC else None
493
+ params_dict = {"use_history": use_history, "model": model}
494
+ if max_tokens:
495
+ params_dict["maxTokens"] = max_tokens
496
+ if request_params:
497
+ params_dict.update(request_params)
498
+ base_params = RequestParams(**params_dict)
499
+ else:
500
+ base_params = RequestParams(use_history=use_history)
501
+
502
+ # Create agent configuration
503
+ config = AgentConfig(
504
+ name=name,
505
+ instruction=instruction,
506
+ servers=servers,
507
+ model=model,
508
+ use_history=use_history,
509
+ default_request_params=base_params,
510
+ human_input=human_input,
511
+ )
512
+
513
+ # Build agent/workflow specific data
514
+ agent_data = {
515
+ "config": config,
516
+ "type": agent_type.value,
517
+ "func": func,
518
+ }
519
+
520
+ # Add extra parameters specific to this agent type
521
+ for key, value in kwargs.items():
522
+ if key not in [
523
+ "name",
524
+ "instruction",
525
+ "servers",
526
+ "model",
527
+ "use_history",
528
+ "request_params",
529
+ "human_input",
530
+ ]:
531
+ agent_data[key] = value
532
+
533
+ # Store the configuration under the agent name
534
+ self.agents[name] = agent_data
535
+
536
+ # Either wrap or return the original function
537
+ if wrapper_needed:
538
+
539
+ async def wrapper(*args, **kwargs):
540
+ return await func(*args, **kwargs)
541
+
542
+ return wrapper
543
+ return func
544
+
545
+ return decorator
546
+
547
+ return decorator_wrapper
548
+
429
549
  def agent(
430
550
  self,
431
551
  name: str = "Agent",
432
552
  *,
433
553
  instruction: str = "You are a helpful agent.",
434
554
  servers: List[str] = [],
435
- model: Optional[str] = None,
555
+ model: str | None = None,
436
556
  use_history: bool = True,
437
557
  request_params: Optional[Dict] = None,
438
558
  human_input: bool = False,
@@ -447,46 +567,32 @@ class FastAgent(ContextDependent):
447
567
  model: Model specification string (highest precedence)
448
568
  use_history: Whether to maintain conversation history
449
569
  request_params: Additional request parameters for the LLM
570
+ human_input: Whether to enable human input capabilities
450
571
  """
451
-
452
- def decorator(func: Callable) -> Callable:
453
- # Create base request params
454
- base_params = RequestParams(
455
- use_history=use_history,
456
- model=model, # Include model in initial params
457
- maxTokens=4096, # Default to larger context for agents TODO configurations
458
- **(request_params or {}),
459
- )
460
-
461
- # Create agent configuration
462
- config = AgentConfig(
463
- name=name,
464
- instruction=instruction,
465
- servers=servers,
466
- model=model, # Highest precedence
467
- use_history=use_history,
468
- default_request_params=base_params,
469
- human_input=human_input,
470
- )
471
-
472
- # Store the agent configuration
473
- self.agents[name] = {
474
- "config": config,
475
- "type": AgentType.BASIC.value,
476
- "func": func,
477
- }
478
-
479
- return func # Don't wrap the function, just return it
480
-
572
+ decorator = self._create_decorator(
573
+ AgentType.BASIC,
574
+ default_name="Agent",
575
+ default_instruction="You are a helpful agent.",
576
+ default_use_history=True,
577
+ )(
578
+ name=name,
579
+ instruction=instruction,
580
+ servers=servers,
581
+ model=model,
582
+ use_history=use_history,
583
+ request_params=request_params,
584
+ human_input=human_input,
585
+ )
481
586
  return decorator
482
587
 
483
588
  def orchestrator(
484
589
  self,
485
- name: str,
486
- instruction: str,
590
+ name: str = "Orchestrator",
591
+ *,
592
+ instruction: str | None = None,
487
593
  agents: List[str],
488
594
  model: str | None = None,
489
- use_history: bool = True,
595
+ use_history: bool = False,
490
596
  request_params: Optional[Dict] = None,
491
597
  human_input: bool = False,
492
598
  ) -> Callable:
@@ -498,37 +604,31 @@ class FastAgent(ContextDependent):
498
604
  instruction: Base instruction for the orchestrator
499
605
  agents: List of agent names this orchestrator can use
500
606
  model: Model specification string (highest precedence)
501
- use_history: Whether to maintain conversation history
607
+ use_history: Whether to maintain conversation history (forced false)
502
608
  request_params: Additional request parameters for the LLM
609
+ human_input: Whether to enable human input capabilities
503
610
  """
504
-
505
- def decorator(func: Callable) -> Callable:
506
- # Create base request params
507
- base_params = RequestParams(
508
- use_history=use_history, **(request_params or {})
509
- )
510
-
511
- # Create agent configuration
512
- config = AgentConfig(
513
- name=name,
514
- instruction=instruction,
515
- servers=[], # Orchestrators don't need servers
516
- model=model, # Highest precedence
517
- use_history=use_history,
518
- default_request_params=base_params,
519
- human_input=human_input,
520
- )
521
-
522
- # Store the orchestrator configuration
523
- self.agents[name] = {
524
- "config": config,
525
- "child_agents": agents,
526
- "type": AgentType.ORCHESTRATOR.value,
527
- "func": func,
528
- }
529
-
530
- return func
531
-
611
+ default_instruction = """
612
+ You are an expert planner. Given an objective task and a list of MCP servers (which are collections of tools)
613
+ or Agents (which are collections of servers), your job is to break down the objective into a series of steps,
614
+ which can be performed by LLMs with access to the servers or agents.
615
+ """
616
+
617
+ decorator = self._create_decorator(
618
+ AgentType.ORCHESTRATOR,
619
+ default_name="Orchestrator",
620
+ default_instruction=default_instruction,
621
+ default_servers=[],
622
+ default_use_history=False,
623
+ )(
624
+ name=name,
625
+ instruction=instruction,
626
+ child_agents=agents,
627
+ model=model,
628
+ use_history=use_history,
629
+ request_params=request_params,
630
+ human_input=human_input,
631
+ )
532
632
  return decorator
533
633
 
534
634
  def parallel(
@@ -553,33 +653,20 @@ class FastAgent(ContextDependent):
553
653
  use_history: Whether to maintain conversation history
554
654
  request_params: Additional request parameters for the LLM
555
655
  """
556
-
557
- def decorator(func: Callable) -> Callable:
558
- # Create request params with history setting
559
- params = RequestParams(**(request_params or {}))
560
- params.use_history = use_history
561
-
562
- # Create agent configuration
563
- config = AgentConfig(
564
- name=name,
565
- instruction=instruction,
566
- servers=[], # Parallel agents don't need servers
567
- model=model,
568
- use_history=use_history,
569
- default_request_params=params,
570
- )
571
-
572
- # Store the parallel configuration
573
- self.agents[name] = {
574
- "config": config,
575
- "fan_out": fan_out,
576
- "fan_in": fan_in,
577
- "type": AgentType.PARALLEL.value,
578
- "func": func,
579
- }
580
-
581
- return func
582
-
656
+ decorator = self._create_decorator(
657
+ AgentType.PARALLEL,
658
+ default_instruction="",
659
+ default_servers=[],
660
+ default_use_history=True,
661
+ )(
662
+ name=name,
663
+ fan_in=fan_in,
664
+ fan_out=fan_out,
665
+ instruction=instruction,
666
+ model=model,
667
+ use_history=use_history,
668
+ request_params=request_params,
669
+ )
583
670
  return decorator
584
671
 
585
672
  def evaluator_optimizer(
@@ -604,33 +691,21 @@ class FastAgent(ContextDependent):
604
691
  use_history: Whether to maintain conversation history
605
692
  request_params: Additional request parameters for the LLM
606
693
  """
607
-
608
- def decorator(func: Callable) -> Callable:
609
- # Create workflow configuration
610
- config = AgentConfig(
611
- name=name,
612
- instruction="", # Uses optimizer's instruction
613
- servers=[], # Uses agents' server access
614
- use_history=use_history,
615
- default_request_params=request_params,
616
- )
617
-
618
- # Store the workflow configuration
619
- self.agents[name] = {
620
- "config": config,
621
- "optimizer": optimizer,
622
- "evaluator": evaluator,
623
- "min_rating": min_rating,
624
- "max_refinements": max_refinements,
625
- "type": AgentType.EVALUATOR_OPTIMIZER.value,
626
- "func": func,
627
- }
628
-
629
- async def wrapper(*args, **kwargs):
630
- return await func(*args, **kwargs)
631
-
632
- return wrapper
633
-
694
+ decorator = self._create_decorator(
695
+ AgentType.EVALUATOR_OPTIMIZER,
696
+ default_instruction="",
697
+ default_servers=[],
698
+ default_use_history=True,
699
+ wrapper_needed=True,
700
+ )(
701
+ name=name,
702
+ optimizer=optimizer,
703
+ evaluator=evaluator,
704
+ min_rating=min_rating,
705
+ max_refinements=max_refinements,
706
+ use_history=use_history,
707
+ request_params=request_params,
708
+ )
634
709
  return decorator
635
710
 
636
711
  def router(
@@ -649,79 +724,231 @@ class FastAgent(ContextDependent):
649
724
  Args:
650
725
  name: Name of the router
651
726
  agents: List of agent names this router can delegate to
652
- servers: List of server names the router can use directly
727
+ servers: List of server names the router can use directly (currently not supported)
653
728
  model: Model specification string
654
729
  use_history: Whether to maintain conversation history
655
730
  request_params: Additional request parameters for the LLM
731
+ human_input: Whether to enable human input capabilities
656
732
  """
657
-
658
- def decorator(func: Callable) -> Callable:
659
- # Create base request params
660
- base_params = RequestParams(
661
- use_history=use_history, **(request_params or {})
662
- )
663
-
664
- # Create agent configuration
665
- config = AgentConfig(
666
- name=name,
667
- instruction="", # Router uses its own routing instruction
668
- servers=[], # , servers are not supported now
669
- model=model,
670
- use_history=use_history,
671
- default_request_params=base_params,
672
- human_input=human_input,
673
- )
674
-
675
- # Store the router configuration
676
- self.agents[name] = {
677
- "config": config,
678
- "agents": agents,
679
- "type": AgentType.ROUTER.value,
680
- "func": func,
681
- }
682
-
683
- async def wrapper(*args, **kwargs):
684
- return await func(*args, **kwargs)
685
-
686
- return wrapper
687
-
733
+ decorator = self._create_decorator(
734
+ AgentType.ROUTER,
735
+ default_instruction="",
736
+ default_servers=[],
737
+ default_use_history=True,
738
+ wrapper_needed=True,
739
+ )(
740
+ name=name,
741
+ agents=agents,
742
+ model=model,
743
+ use_history=use_history,
744
+ request_params=request_params,
745
+ human_input=human_input,
746
+ )
688
747
  return decorator
689
748
 
690
- async def _create_basic_agents(self, agent_app: MCPApp) -> ProxyDict:
749
+ async def _create_agents_by_type(
750
+ self,
751
+ agent_app: MCPApp,
752
+ agent_type: AgentType,
753
+ active_agents: ProxyDict = None,
754
+ **kwargs,
755
+ ) -> ProxyDict:
691
756
  """
692
- Create and initialize basic agents with their configurations.
757
+ Generic method to create agents of a specific type.
693
758
 
694
759
  Args:
695
760
  agent_app: The main application instance
761
+ agent_type: Type of agents to create
762
+ active_agents: Dictionary of already created agents/proxies (for dependencies)
763
+ **kwargs: Additional type-specific parameters
696
764
 
697
765
  Returns:
698
- Dictionary of initialized basic agents wrapped in appropriate proxies
766
+ Dictionary of initialized agents wrapped in appropriate proxies
699
767
  """
700
- active_agents = {}
768
+ if active_agents is None:
769
+ active_agents = {}
770
+
771
+ # Create a dictionary to store the initialized agents
772
+ result_agents = {}
701
773
 
774
+ # Get all agents of the specified type
702
775
  for name, agent_data in self.agents.items():
703
- if agent_data["type"] == AgentType.BASIC.value:
776
+ if agent_data["type"] == agent_type.value:
777
+ # Get common configuration
704
778
  config = agent_data["config"]
705
779
 
706
- # Create agent with configuration
707
- agent = Agent(config=config, context=agent_app.context)
780
+ # Type-specific initialization
781
+ if agent_type == AgentType.BASIC:
782
+ # Create basic agent with configuration
783
+ agent = Agent(config=config, context=agent_app.context)
784
+
785
+ # Set up LLM with proper configuration
786
+ async with agent:
787
+ llm_factory = self._get_model_factory(
788
+ model=config.model,
789
+ request_params=config.default_request_params,
790
+ )
791
+ agent._llm = await agent.attach_llm(llm_factory)
708
792
 
709
- # Set up LLM with proper configuration
710
- async with agent:
793
+ # Store the agent
794
+ instance = agent
795
+
796
+ elif agent_type == AgentType.ORCHESTRATOR:
797
+ # Get base params configured with model settings
798
+ base_params = (
799
+ config.default_request_params.model_copy()
800
+ if config.default_request_params
801
+ else RequestParams()
802
+ )
803
+ base_params.use_history = False # Force no history for orchestrator
804
+
805
+ # Get the child agents - need to unwrap proxies and validate LLM config
806
+ child_agents = []
807
+ for agent_name in agent_data["child_agents"]:
808
+ proxy = active_agents[agent_name]
809
+ instance = self._unwrap_proxy(proxy)
810
+ # Validate basic agents have LLM
811
+ if isinstance(instance, Agent):
812
+ if not hasattr(instance, "_llm") or not instance._llm:
813
+ raise AgentConfigError(
814
+ f"Agent '{agent_name}' used by orchestrator '{name}' missing LLM configuration",
815
+ "All agents must be fully configured with LLMs before being used in an orchestrator",
816
+ )
817
+ child_agents.append(instance)
818
+
819
+ # Create a properly configured planner agent
820
+ planner_config = AgentConfig(
821
+ name=f"{name}", # Use orchestrator name as prefix
822
+ instruction=config.instruction
823
+ or """
824
+ You are an expert planner. Given an objective task and a list of MCP servers (which are collections of tools)
825
+ or Agents (which are collections of servers), your job is to break down the objective into a series of steps,
826
+ which can be performed by LLMs with access to the servers or agents.
827
+ """,
828
+ servers=[], # Planner doesn't need server access
829
+ model=config.model, # Use same model as orchestrator
830
+ default_request_params=base_params,
831
+ )
832
+ planner_agent = Agent(
833
+ config=planner_config, context=agent_app.context
834
+ )
835
+ planner_factory = self._get_model_factory(
836
+ model=config.model,
837
+ request_params=config.default_request_params,
838
+ )
839
+
840
+ async with planner_agent:
841
+ planner = await planner_agent.attach_llm(planner_factory)
842
+
843
+ # Create the orchestrator with pre-configured planner
844
+ instance = Orchestrator(
845
+ name=config.name,
846
+ planner=planner, # Pass pre-configured planner
847
+ available_agents=child_agents,
848
+ context=agent_app.context,
849
+ request_params=planner.default_request_params, # Base params already include model settings
850
+ plan_type="full", # TODO -- support iterative plan type properly
851
+ verb=ProgressAction.PLANNING, # Using PLANNING instead of ORCHESTRATING
852
+ )
853
+
854
+ elif agent_type == AgentType.EVALUATOR_OPTIMIZER:
855
+ # Get the referenced agents - unwrap from proxies
856
+ optimizer = self._unwrap_proxy(
857
+ active_agents[agent_data["optimizer"]]
858
+ )
859
+ evaluator = self._unwrap_proxy(
860
+ active_agents[agent_data["evaluator"]]
861
+ )
862
+
863
+ if not optimizer or not evaluator:
864
+ raise ValueError(
865
+ f"Missing agents for workflow {name}: "
866
+ f"optimizer={agent_data['optimizer']}, "
867
+ f"evaluator={agent_data['evaluator']}"
868
+ )
869
+
870
+ # TODO: Remove legacy - factory usage is only needed for str evaluators
871
+ # Later this should only be passed when evaluator is a string
872
+ optimizer_model = (
873
+ optimizer.config.model if isinstance(optimizer, Agent) else None
874
+ )
875
+ instance = EvaluatorOptimizerLLM(
876
+ optimizer=optimizer,
877
+ evaluator=evaluator,
878
+ min_rating=QualityRating[agent_data["min_rating"]],
879
+ max_refinements=agent_data["max_refinements"],
880
+ llm_factory=self._get_model_factory(model=optimizer_model),
881
+ context=agent_app.context,
882
+ )
883
+
884
+ elif agent_type == AgentType.ROUTER:
885
+ # Get the router's agents - unwrap proxies
886
+ router_agents = self._get_agent_instances(
887
+ agent_data["agents"], active_agents
888
+ )
889
+
890
+ # Create the router with proper configuration
711
891
  llm_factory = self._get_model_factory(
712
892
  model=config.model,
713
893
  request_params=config.default_request_params,
714
894
  )
715
- agent._llm = await agent.attach_llm(llm_factory)
716
895
 
717
- # Create proxy for the agent
718
- active_agents[name] = self._create_proxy(
719
- name, agent, AgentType.BASIC.value
896
+ instance = LLMRouter(
897
+ name=config.name,
898
+ llm_factory=llm_factory,
899
+ agents=router_agents,
900
+ server_names=config.servers,
901
+ context=agent_app.context,
902
+ default_request_params=config.default_request_params,
903
+ verb=ProgressAction.ROUTING, # Set verb for progress display
904
+ )
905
+
906
+ elif agent_type == AgentType.PARALLEL:
907
+ # Get fan-out agents (could be basic agents or other parallels)
908
+ fan_out_agents = self._get_agent_instances(
909
+ agent_data["fan_out"], active_agents
910
+ )
911
+
912
+ # Get fan-in agent - unwrap proxy
913
+ fan_in_agent = self._unwrap_proxy(
914
+ active_agents[agent_data["fan_in"]]
915
+ )
916
+
917
+ # Create the parallel workflow
918
+ llm_factory = self._get_model_factory(config.model)
919
+ instance = ParallelLLM(
920
+ name=config.name,
921
+ instruction=config.instruction,
922
+ fan_out_agents=fan_out_agents,
923
+ fan_in_agent=fan_in_agent,
924
+ context=agent_app.context,
925
+ llm_factory=llm_factory,
926
+ default_request_params=config.default_request_params,
927
+ )
928
+
929
+ else:
930
+ raise ValueError(f"Unsupported agent type: {agent_type}")
931
+
932
+ # Create the appropriate proxy and store in results
933
+ result_agents[name] = self._create_proxy(
934
+ name, instance, agent_type.value
720
935
  )
721
936
 
722
- return active_agents
937
+ return result_agents
723
938
 
724
- def _create_orchestrators(
939
+ async def _create_basic_agents(self, agent_app: MCPApp) -> ProxyDict:
940
+ """
941
+ Create and initialize basic agents with their configurations.
942
+
943
+ Args:
944
+ agent_app: The main application instance
945
+
946
+ Returns:
947
+ Dictionary of initialized basic agents wrapped in appropriate proxies
948
+ """
949
+ return await self._create_agents_by_type(agent_app, AgentType.BASIC)
950
+
951
+ async def _create_orchestrators(
725
952
  self, agent_app: MCPApp, active_agents: ProxyDict
726
953
  ) -> ProxyDict:
727
954
  """
@@ -734,57 +961,9 @@ class FastAgent(ContextDependent):
734
961
  Returns:
735
962
  Dictionary of initialized orchestrator agents wrapped in appropriate proxies
736
963
  """
737
- orchestrators = {}
738
- for name, agent_data in self.agents.items():
739
- if agent_data["type"] == AgentType.ORCHESTRATOR.value:
740
- config = agent_data["config"]
741
-
742
- # TODO: Remove legacy - This model/params setup should be in Agent class
743
- # Resolve model alias if present
744
- model_config = ModelFactory.parse_model_string(config.model)
745
- resolved_model = model_config.model_name
746
-
747
- # Start with existing params if available
748
- if config.default_request_params:
749
- base_params = config.default_request_params.model_copy()
750
- # Update with orchestrator-specific settings
751
- base_params.use_history = config.use_history
752
- base_params.model = resolved_model
753
- else:
754
- base_params = RequestParams(
755
- use_history=config.use_history, model=resolved_model
756
- )
757
-
758
- llm_factory = self._get_model_factory(
759
- model=config.model, # Use original model string for factory creation
760
- request_params=base_params,
761
- )
762
-
763
- # Get the child agents - need to unwrap proxies
764
- child_agents = []
765
- for agent_name in agent_data["child_agents"]:
766
- proxy = active_agents[agent_name]
767
- if isinstance(proxy, LLMAgentProxy):
768
- child_agents.append(proxy._agent) # Get the actual Agent
769
- else:
770
- # Handle case where it might be another workflow
771
- child_agents.append(proxy._workflow)
772
-
773
- orchestrator = Orchestrator(
774
- name=config.name,
775
- instruction=config.instruction,
776
- available_agents=child_agents,
777
- context=agent_app.context,
778
- llm_factory=llm_factory,
779
- request_params=base_params, # Use our base params that include model
780
- plan_type="full",
781
- )
782
-
783
- # Use factory to create appropriate proxy
784
- orchestrators[name] = self._create_proxy(
785
- name, orchestrator, AgentType.ORCHESTRATOR.value
786
- )
787
- return orchestrators
964
+ return await self._create_agents_by_type(
965
+ agent_app, AgentType.ORCHESTRATOR, active_agents
966
+ )
788
967
 
789
968
  async def _create_evaluator_optimizers(
790
969
  self, agent_app: MCPApp, active_agents: ProxyDict
@@ -799,39 +978,9 @@ class FastAgent(ContextDependent):
799
978
  Returns:
800
979
  Dictionary of initialized evaluator-optimizer workflows
801
980
  """
802
- workflows = {}
803
- for name, agent_data in self.agents.items():
804
- if agent_data["type"] == AgentType.EVALUATOR_OPTIMIZER.value:
805
- # Get the referenced agents - unwrap from proxies
806
- optimizer = self._unwrap_proxy(active_agents[agent_data["optimizer"]])
807
- evaluator = self._unwrap_proxy(active_agents[agent_data["evaluator"]])
808
-
809
- if not optimizer or not evaluator:
810
- raise ValueError(
811
- f"Missing agents for workflow {name}: "
812
- f"optimizer={agent_data['optimizer']}, "
813
- f"evaluator={agent_data['evaluator']}"
814
- )
815
-
816
- # TODO: Remove legacy - factory usage is only needed for str evaluators
817
- # Later this should only be passed when evaluator is a string
818
- optimizer_model = (
819
- optimizer.config.model if isinstance(optimizer, Agent) else None
820
- )
821
- workflow = EvaluatorOptimizerLLM(
822
- optimizer=optimizer,
823
- evaluator=evaluator,
824
- min_rating=QualityRating[agent_data["min_rating"]],
825
- max_refinements=agent_data["max_refinements"],
826
- llm_factory=self._get_model_factory(model=optimizer_model),
827
- context=agent_app.context,
828
- )
829
-
830
- workflows[name] = self._create_proxy(
831
- name, workflow, AgentType.EVALUATOR_OPTIMIZER.value
832
- )
833
-
834
- return workflows
981
+ return await self._create_agents_by_type(
982
+ agent_app, AgentType.EVALUATOR_OPTIMIZER, active_agents
983
+ )
835
984
 
836
985
  def _get_parallel_dependencies(
837
986
  self, name: str, visited: set, path: set
@@ -878,7 +1027,7 @@ class FastAgent(ContextDependent):
878
1027
 
879
1028
  return deps
880
1029
 
881
- def _create_parallel_agents(
1030
+ async def _create_parallel_agents(
882
1031
  self, agent_app: MCPApp, active_agents: ProxyDict
883
1032
  ) -> ProxyDict:
884
1033
  """
@@ -915,38 +1064,21 @@ class FastAgent(ContextDependent):
915
1064
  # Create each agent in order
916
1065
  for agent_name in ordered_agents:
917
1066
  if agent_name not in parallel_agents:
918
- agent_data = self.agents[agent_name]
919
- config = agent_data["config"]
920
-
921
- # Get fan-out agents (could be basic agents or other parallels)
922
- fan_out_agents = self._get_agent_instances(
923
- agent_data["fan_out"], active_agents
924
- )
925
-
926
- # Get fan-in agent - unwrap proxy
927
- fan_in_agent = self._unwrap_proxy(
928
- active_agents[agent_data["fan_in"]]
929
- )
930
-
931
- # Create the parallel workflow
932
- llm_factory = self._get_model_factory(config.model)
933
- parallel = ParallelLLM(
934
- name=config.name,
935
- instruction=config.instruction,
936
- fan_out_agents=fan_out_agents,
937
- fan_in_agent=fan_in_agent,
938
- context=agent_app.context,
939
- llm_factory=llm_factory,
940
- default_request_params=config.default_request_params,
941
- )
942
-
943
- parallel_agents[agent_name] = self._create_proxy(
944
- name, parallel, AgentType.PARALLEL.value
1067
+ # Create one parallel agent at a time using the generic method
1068
+ agent_result = await self._create_agents_by_type(
1069
+ agent_app,
1070
+ AgentType.PARALLEL,
1071
+ active_agents,
1072
+ agent_name=agent_name,
945
1073
  )
1074
+ if agent_name in agent_result:
1075
+ parallel_agents[agent_name] = agent_result[agent_name]
946
1076
 
947
1077
  return parallel_agents
948
1078
 
949
- def _create_routers(self, agent_app: MCPApp, active_agents: ProxyDict) -> ProxyDict:
1079
+ async def _create_routers(
1080
+ self, agent_app: MCPApp, active_agents: ProxyDict
1081
+ ) -> ProxyDict:
950
1082
  """
951
1083
  Create router agents.
952
1084
 
@@ -957,34 +1089,9 @@ class FastAgent(ContextDependent):
957
1089
  Returns:
958
1090
  Dictionary of initialized router agents
959
1091
  """
960
- routers = {}
961
- for name, agent_data in self.agents.items():
962
- if agent_data["type"] == AgentType.ROUTER.value:
963
- config = agent_data["config"]
964
-
965
- # Get the router's agents - unwrap proxies
966
- router_agents = self._get_agent_instances(
967
- agent_data["agents"], active_agents
968
- )
969
-
970
- # Create the router with proper configuration
971
- llm_factory = self._get_model_factory(
972
- model=config.model,
973
- request_params=config.default_request_params,
974
- )
975
-
976
- router = LLMRouter(
977
- name=config.name, # Add the name parameter
978
- llm_factory=llm_factory,
979
- agents=router_agents,
980
- server_names=config.servers,
981
- context=agent_app.context,
982
- default_request_params=config.default_request_params,
983
- )
984
-
985
- routers[name] = self._create_proxy(name, router, AgentType.ROUTER.value)
986
-
987
- return routers
1092
+ return await self._create_agents_by_type(
1093
+ agent_app, AgentType.ROUTER, active_agents
1094
+ )
988
1095
 
989
1096
  def _unwrap_proxy(self, proxy: BaseAgentProxy) -> AgentOrWorkflow:
990
1097
  """
@@ -1029,14 +1136,18 @@ class FastAgent(ContextDependent):
1029
1136
  self._validate_server_references()
1030
1137
  self._validate_workflow_references()
1031
1138
 
1032
- # Create all types of agents
1139
+ # Create all types of agents in dependency order
1033
1140
  active_agents = await self._create_basic_agents(agent_app)
1034
- orchestrators = self._create_orchestrators(agent_app, active_agents)
1035
- parallel_agents = self._create_parallel_agents(agent_app, active_agents)
1141
+ orchestrators = await self._create_orchestrators(
1142
+ agent_app, active_agents
1143
+ )
1144
+ parallel_agents = await self._create_parallel_agents(
1145
+ agent_app, active_agents
1146
+ )
1036
1147
  evaluator_optimizers = await self._create_evaluator_optimizers(
1037
1148
  agent_app, active_agents
1038
1149
  )
1039
- routers = self._create_routers(agent_app, active_agents)
1150
+ routers = await self._create_routers(agent_app, active_agents)
1040
1151
 
1041
1152
  # Merge all agents into active_agents
1042
1153
  active_agents.update(orchestrators)
@@ -1050,48 +1161,38 @@ class FastAgent(ContextDependent):
1050
1161
 
1051
1162
  except ServerConfigError as e:
1052
1163
  had_error = True
1053
- print("\n[bold red]Server Configuration Error:")
1054
- print(e.message)
1055
- if e.details:
1056
- print("\nDetails:")
1057
- print(e.details)
1058
- print(
1059
- "\nPlease check your 'fastagent.config.yaml' configuration file and add the missing server definitions."
1164
+ self._handle_error(
1165
+ e,
1166
+ "Server Configuration Error",
1167
+ "Please check your 'fastagent.config.yaml' configuration file and add the missing server definitions.",
1060
1168
  )
1061
1169
  raise SystemExit(1)
1062
1170
 
1063
1171
  except ProviderKeyError as e:
1064
1172
  had_error = True
1065
- print("\n[bold red]Provider Configuration Error:")
1066
- print(e.message)
1067
- if e.details:
1068
- print("\nDetails:")
1069
- print(e.details)
1070
- print(
1071
- "\nPlease check your 'fastagent.secrets.yaml' configuration file and ensure all required API keys are set."
1173
+ self._handle_error(
1174
+ e,
1175
+ "Provider Configuration Error",
1176
+ "Please check your 'fastagent.secrets.yaml' configuration file and ensure all required API keys are set.",
1072
1177
  )
1073
1178
  raise SystemExit(1)
1074
1179
 
1075
1180
  except AgentConfigError as e:
1076
1181
  had_error = True
1077
- print("\n[bold red]Workflow or Agent Configuration Error:")
1078
- print(e.message)
1079
- if e.details:
1080
- print("\nDetails:")
1081
- print(e.details)
1082
- print(
1083
- "\nPlease check your agent definition and ensure names and references are correct."
1182
+ self._handle_error(
1183
+ e,
1184
+ "Workflow or Agent Configuration Error",
1185
+ "Please check your agent definition and ensure names and references are correct.",
1084
1186
  )
1085
1187
  raise SystemExit(1)
1086
1188
 
1087
1189
  except ServerInitializationError as e:
1088
1190
  had_error = True
1089
- print("\n[bold red]Server Startup Error:")
1090
- print(e.message)
1091
- if e.details:
1092
- print("\nDetails:")
1093
- print(e.details)
1094
- print("\nThere was an error starting up the MCP Server.")
1191
+ self._handle_error(
1192
+ e,
1193
+ "Server Startup Error",
1194
+ "There was an error starting up the MCP Server.",
1195
+ )
1095
1196
  raise SystemExit(1)
1096
1197
  finally:
1097
1198
  # Clean up any active agents without re-raising errors
@@ -1102,3 +1203,31 @@ class FastAgent(ContextDependent):
1102
1203
  await proxy._agent.__aexit__(None, None, None)
1103
1204
  except Exception:
1104
1205
  pass # Ignore cleanup errors
1206
+
1207
+ def _handle_error(
1208
+ self, e: Exception, error_type: str, suggestion: str = None
1209
+ ) -> None:
1210
+ """
1211
+ Handle errors with consistent formatting and messaging.
1212
+
1213
+ Args:
1214
+ e: The exception that was raised
1215
+ error_type: Type of error to display
1216
+ suggestion: Optional suggestion message to display
1217
+ """
1218
+ print(f"\n[bold red]{error_type}:")
1219
+ print(getattr(e, "message", str(e)))
1220
+ if hasattr(e, "details") and e.details:
1221
+ print("\nDetails:")
1222
+ print(e.details)
1223
+ if suggestion:
1224
+ print(f"\n{suggestion}")
1225
+
1226
+ def _log_agent_load(self, agent_name: str) -> None:
1227
+ self.app._logger.info(
1228
+ f"Loaded {agent_name}",
1229
+ data={
1230
+ "progress_action": ProgressAction.LOADED,
1231
+ "agent_name": agent_name,
1232
+ },
1233
+ )