fast-agent-mcp 0.0.12__py3-none-any.whl → 0.0.14__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.
- {fast_agent_mcp-0.0.12.dist-info → fast_agent_mcp-0.0.14.dist-info}/METADATA +194 -11
- {fast_agent_mcp-0.0.12.dist-info → fast_agent_mcp-0.0.14.dist-info}/RECORD +21 -23
- mcp_agent/agents/agent.py +48 -8
- mcp_agent/cli/commands/bootstrap.py +2 -1
- mcp_agent/cli/commands/setup.py +1 -1
- mcp_agent/cli/main.py +3 -3
- mcp_agent/core/enhanced_prompt.py +59 -16
- mcp_agent/core/exceptions.py +24 -0
- mcp_agent/core/fastagent.py +262 -34
- mcp_agent/human_input/handler.py +43 -18
- mcp_agent/mcp/mcp_connection_manager.py +14 -12
- mcp_agent/resources/examples/workflows/chaining.py +18 -4
- mcp_agent/resources/examples/workflows/evaluator.py +2 -2
- mcp_agent/resources/examples/workflows/fastagent.config.yaml +24 -0
- mcp_agent/resources/examples/workflows/orchestrator.py +3 -2
- mcp_agent/resources/examples/workflows/parallel.py +2 -1
- mcp_agent/workflows/llm/augmented_llm.py +3 -0
- mcp_agent/workflows/llm/model_factory.py +7 -4
- mcp_agent/resources/examples/mcp_researcher/researcher.py +0 -38
- mcp_agent/resources/examples/workflows/agent.py +0 -17
- mcp_agent/resources/examples/workflows/fastagent.py +0 -22
- {fast_agent_mcp-0.0.12.dist-info → fast_agent_mcp-0.0.14.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.0.12.dist-info → fast_agent_mcp-0.0.14.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.0.12.dist-info → fast_agent_mcp-0.0.14.dist-info}/licenses/LICENSE +0 -0
mcp_agent/core/fastagent.py
CHANGED
|
@@ -3,7 +3,17 @@ Decorator-based interface for MCP Agent applications.
|
|
|
3
3
|
Provides a simplified way to create and manage agents using decorators.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import (
|
|
7
|
+
List,
|
|
8
|
+
Optional,
|
|
9
|
+
Dict,
|
|
10
|
+
Callable,
|
|
11
|
+
TypeVar,
|
|
12
|
+
Any,
|
|
13
|
+
Union,
|
|
14
|
+
TypeAlias,
|
|
15
|
+
Literal,
|
|
16
|
+
)
|
|
7
17
|
from enum import Enum
|
|
8
18
|
import yaml
|
|
9
19
|
import argparse
|
|
@@ -11,6 +21,9 @@ from contextlib import asynccontextmanager
|
|
|
11
21
|
|
|
12
22
|
from mcp_agent.core.exceptions import (
|
|
13
23
|
AgentConfigError,
|
|
24
|
+
CircularDependencyError,
|
|
25
|
+
ModelConfigError,
|
|
26
|
+
PromptExitError,
|
|
14
27
|
ServerConfigError,
|
|
15
28
|
ProviderKeyError,
|
|
16
29
|
ServerInitializationError,
|
|
@@ -52,6 +65,7 @@ class AgentType(Enum):
|
|
|
52
65
|
PARALLEL = "parallel"
|
|
53
66
|
EVALUATOR_OPTIMIZER = "evaluator_optimizer"
|
|
54
67
|
ROUTER = "router"
|
|
68
|
+
CHAIN = "chain"
|
|
55
69
|
|
|
56
70
|
|
|
57
71
|
T = TypeVar("T") # For the wrapper classes
|
|
@@ -138,6 +152,27 @@ class RouterProxy(BaseAgentProxy):
|
|
|
138
152
|
return f"Routed to: {top_result.result} ({top_result.confidence}): {top_result.reasoning}"
|
|
139
153
|
|
|
140
154
|
|
|
155
|
+
class ChainProxy(BaseAgentProxy):
|
|
156
|
+
"""Proxy for chained agent operations"""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self, app: MCPApp, name: str, sequence: List[str], agent_proxies: ProxyDict
|
|
160
|
+
):
|
|
161
|
+
super().__init__(app, name)
|
|
162
|
+
self._sequence = sequence
|
|
163
|
+
self._agent_proxies = agent_proxies
|
|
164
|
+
|
|
165
|
+
async def generate_str(self, message: str) -> str:
|
|
166
|
+
"""Chain message through a sequence of agents"""
|
|
167
|
+
current_message = message
|
|
168
|
+
|
|
169
|
+
for agent_name in self._sequence:
|
|
170
|
+
proxy = self._agent_proxies[agent_name]
|
|
171
|
+
current_message = await proxy.generate_str(current_message)
|
|
172
|
+
|
|
173
|
+
return current_message
|
|
174
|
+
|
|
175
|
+
|
|
141
176
|
class AgentApp:
|
|
142
177
|
"""Main application wrapper"""
|
|
143
178
|
|
|
@@ -175,7 +210,7 @@ class AgentApp:
|
|
|
175
210
|
|
|
176
211
|
# Pass all available agent names for auto-completion
|
|
177
212
|
available_agents = list(self._agents.keys())
|
|
178
|
-
|
|
213
|
+
|
|
179
214
|
# Create agent_types dictionary mapping agent names to their types
|
|
180
215
|
agent_types = {}
|
|
181
216
|
for name, proxy in self._agents.items():
|
|
@@ -185,6 +220,8 @@ class AgentApp:
|
|
|
185
220
|
agent_types[name] = "Agent"
|
|
186
221
|
elif isinstance(proxy, RouterProxy):
|
|
187
222
|
agent_types[name] = "Router"
|
|
223
|
+
elif isinstance(proxy, ChainProxy):
|
|
224
|
+
agent_types[name] = "Chain"
|
|
188
225
|
elif isinstance(proxy, WorkflowProxy):
|
|
189
226
|
# For workflow proxies, check the workflow type
|
|
190
227
|
workflow = proxy._workflow
|
|
@@ -228,7 +265,7 @@ class AgentApp:
|
|
|
228
265
|
continue
|
|
229
266
|
|
|
230
267
|
if user_input.upper() == "STOP":
|
|
231
|
-
return
|
|
268
|
+
return result
|
|
232
269
|
if user_input == "":
|
|
233
270
|
continue
|
|
234
271
|
|
|
@@ -311,6 +348,7 @@ class FastAgent(ContextDependent):
|
|
|
311
348
|
if agent_type not in [
|
|
312
349
|
AgentType.PARALLEL.value,
|
|
313
350
|
AgentType.EVALUATOR_OPTIMIZER.value,
|
|
351
|
+
AgentType.CHAIN.value,
|
|
314
352
|
]:
|
|
315
353
|
self._log_agent_load(name)
|
|
316
354
|
if agent_type == AgentType.BASIC.value:
|
|
@@ -343,6 +381,10 @@ class FastAgent(ContextDependent):
|
|
|
343
381
|
f"Expected LLMRouter instance for {name}, got {type(instance)}"
|
|
344
382
|
)
|
|
345
383
|
return RouterProxy(self.app, name, instance)
|
|
384
|
+
elif agent_type == AgentType.CHAIN.value:
|
|
385
|
+
# Chain proxy is directly returned from _create_agents_by_type
|
|
386
|
+
# No need for type checking as it's already a ChainProxy
|
|
387
|
+
return instance
|
|
346
388
|
else:
|
|
347
389
|
raise ValueError(f"Unknown agent type: {agent_type}")
|
|
348
390
|
|
|
@@ -449,6 +491,15 @@ class FastAgent(ContextDependent):
|
|
|
449
491
|
f"Evaluator-Optimizer '{name}' references non-existent components: {', '.join(missing)}"
|
|
450
492
|
)
|
|
451
493
|
|
|
494
|
+
elif agent_type == AgentType.CHAIN.value:
|
|
495
|
+
# Check that all agents in the sequence exist
|
|
496
|
+
sequence = agent_data.get("sequence", agent_data.get("agents", []))
|
|
497
|
+
missing = [a for a in sequence if a not in available_components]
|
|
498
|
+
if missing:
|
|
499
|
+
raise AgentConfigError(
|
|
500
|
+
f"Chain '{name}' references non-existent agents: {', '.join(missing)}"
|
|
501
|
+
)
|
|
502
|
+
|
|
452
503
|
def _get_model_factory(
|
|
453
504
|
self,
|
|
454
505
|
model: Optional[str] = None,
|
|
@@ -633,6 +684,7 @@ class FastAgent(ContextDependent):
|
|
|
633
684
|
use_history: bool = False,
|
|
634
685
|
request_params: Optional[Dict] = None,
|
|
635
686
|
human_input: bool = False,
|
|
687
|
+
plan_type: Literal["full", "iterative"] = "full",
|
|
636
688
|
) -> Callable:
|
|
637
689
|
"""
|
|
638
690
|
Decorator to create and register an orchestrator.
|
|
@@ -645,6 +697,7 @@ class FastAgent(ContextDependent):
|
|
|
645
697
|
use_history: Whether to maintain conversation history (forced false)
|
|
646
698
|
request_params: Additional request parameters for the LLM
|
|
647
699
|
human_input: Whether to enable human input capabilities
|
|
700
|
+
plan_type: Planning approach - "full" generates entire plan first, "iterative" plans one step at a time
|
|
648
701
|
"""
|
|
649
702
|
default_instruction = """
|
|
650
703
|
You are an expert planner. Given an objective task and a list of MCP servers (which are collections of tools)
|
|
@@ -666,6 +719,7 @@ class FastAgent(ContextDependent):
|
|
|
666
719
|
use_history=use_history,
|
|
667
720
|
request_params=request_params,
|
|
668
721
|
human_input=human_input,
|
|
722
|
+
plan_type=plan_type,
|
|
669
723
|
)
|
|
670
724
|
return decorator
|
|
671
725
|
|
|
@@ -784,6 +838,55 @@ class FastAgent(ContextDependent):
|
|
|
784
838
|
)
|
|
785
839
|
return decorator
|
|
786
840
|
|
|
841
|
+
def chain(
|
|
842
|
+
self,
|
|
843
|
+
name: str = "Chain",
|
|
844
|
+
*,
|
|
845
|
+
sequence: List[str] = None,
|
|
846
|
+
agents: List[str] = None, # Alias for sequence
|
|
847
|
+
instruction: str = None,
|
|
848
|
+
model: str | None = None,
|
|
849
|
+
use_history: bool = True,
|
|
850
|
+
request_params: Optional[Dict] = None,
|
|
851
|
+
) -> Callable:
|
|
852
|
+
"""
|
|
853
|
+
Decorator to create and register a chain of agents.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
name: Name of the chain
|
|
857
|
+
sequence: List of agent names in order of execution (preferred name)
|
|
858
|
+
agents: Alias for sequence (backwards compatibility)
|
|
859
|
+
instruction: Optional custom instruction for the chain (if none provided, will autogenerate based on sequence)
|
|
860
|
+
model: Model specification string (not used directly in chain)
|
|
861
|
+
use_history: Whether to maintain conversation history
|
|
862
|
+
request_params: Additional request parameters
|
|
863
|
+
"""
|
|
864
|
+
# Support both parameter names
|
|
865
|
+
agent_sequence = sequence or agents
|
|
866
|
+
if agent_sequence is None:
|
|
867
|
+
raise ValueError("Either 'sequence' or 'agents' parameter must be provided")
|
|
868
|
+
|
|
869
|
+
# Auto-generate instruction if not provided
|
|
870
|
+
if instruction is None:
|
|
871
|
+
# We'll generate it later when we have access to the agent configs and can see servers
|
|
872
|
+
instruction = f"Chain of agents: {', '.join(agent_sequence)}"
|
|
873
|
+
|
|
874
|
+
decorator = self._create_decorator(
|
|
875
|
+
AgentType.CHAIN,
|
|
876
|
+
default_name="Chain",
|
|
877
|
+
default_instruction=instruction,
|
|
878
|
+
default_use_history=True,
|
|
879
|
+
wrapper_needed=True,
|
|
880
|
+
)(
|
|
881
|
+
name=name,
|
|
882
|
+
sequence=agent_sequence,
|
|
883
|
+
instruction=instruction,
|
|
884
|
+
model=model,
|
|
885
|
+
use_history=use_history,
|
|
886
|
+
request_params=request_params,
|
|
887
|
+
)
|
|
888
|
+
return decorator
|
|
889
|
+
|
|
787
890
|
async def _create_agents_by_type(
|
|
788
891
|
self,
|
|
789
892
|
agent_app: MCPApp,
|
|
@@ -864,7 +967,7 @@ class FastAgent(ContextDependent):
|
|
|
864
967
|
which can be performed by LLMs with access to the servers or agents.
|
|
865
968
|
""",
|
|
866
969
|
servers=[], # Planner doesn't need server access
|
|
867
|
-
model=config.model,
|
|
970
|
+
model=config.model,
|
|
868
971
|
default_request_params=base_params,
|
|
869
972
|
)
|
|
870
973
|
planner_agent = Agent(
|
|
@@ -885,8 +988,10 @@ class FastAgent(ContextDependent):
|
|
|
885
988
|
available_agents=child_agents,
|
|
886
989
|
context=agent_app.context,
|
|
887
990
|
request_params=planner.default_request_params, # Base params already include model settings
|
|
888
|
-
plan_type=
|
|
889
|
-
|
|
991
|
+
plan_type=agent_data.get(
|
|
992
|
+
"plan_type", "full"
|
|
993
|
+
), # Get plan_type from agent_data
|
|
994
|
+
verb=ProgressAction.PLANNING,
|
|
890
995
|
)
|
|
891
996
|
|
|
892
997
|
elif agent_type == AgentType.EVALUATOR_OPTIMIZER:
|
|
@@ -941,6 +1046,43 @@ class FastAgent(ContextDependent):
|
|
|
941
1046
|
verb=ProgressAction.ROUTING, # Set verb for progress display
|
|
942
1047
|
)
|
|
943
1048
|
|
|
1049
|
+
elif agent_type == AgentType.CHAIN:
|
|
1050
|
+
# Get the sequence from either parameter
|
|
1051
|
+
sequence = agent_data.get("sequence", agent_data.get("agents", []))
|
|
1052
|
+
|
|
1053
|
+
# Auto-generate instruction if not provided or if it's just the default
|
|
1054
|
+
default_instruction = f"Chain of agents: {', '.join(sequence)}"
|
|
1055
|
+
|
|
1056
|
+
# If user provided a custom instruction, use that
|
|
1057
|
+
# Otherwise, generate a description based on the sequence and their servers
|
|
1058
|
+
if config.instruction == default_instruction:
|
|
1059
|
+
# Get all agent names in the sequence
|
|
1060
|
+
agent_names = []
|
|
1061
|
+
all_servers = set()
|
|
1062
|
+
|
|
1063
|
+
# Collect information about the agents and their servers
|
|
1064
|
+
for agent_name in sequence:
|
|
1065
|
+
if agent_name in active_agents:
|
|
1066
|
+
agent_proxy = active_agents[agent_name]
|
|
1067
|
+
if hasattr(agent_proxy, "_agent"):
|
|
1068
|
+
# For LLMAgentProxy
|
|
1069
|
+
agent_instance = agent_proxy._agent
|
|
1070
|
+
agent_names.append(agent_name)
|
|
1071
|
+
if hasattr(agent_instance, "server_names"):
|
|
1072
|
+
all_servers.update(agent_instance.server_names)
|
|
1073
|
+
elif hasattr(agent_proxy, "_workflow"):
|
|
1074
|
+
# For WorkflowProxy
|
|
1075
|
+
agent_names.append(agent_name)
|
|
1076
|
+
|
|
1077
|
+
# Generate a better description
|
|
1078
|
+
if agent_names:
|
|
1079
|
+
server_part = f" with access to servers: {', '.join(sorted(all_servers))}" if all_servers else ""
|
|
1080
|
+
config.instruction = f"Sequence of agents: {', '.join(agent_names)}{server_part}."
|
|
1081
|
+
|
|
1082
|
+
# Create a ChainProxy without needing a new instance
|
|
1083
|
+
# Just pass the agent proxies and sequence
|
|
1084
|
+
instance = ChainProxy(self.app, name, sequence, active_agents)
|
|
1085
|
+
|
|
944
1086
|
elif agent_type == AgentType.PARALLEL:
|
|
945
1087
|
# Get fan-out agents (could be basic agents or other parallels)
|
|
946
1088
|
fan_out_agents = self._get_agent_instances(
|
|
@@ -1020,16 +1162,18 @@ class FastAgent(ContextDependent):
|
|
|
1020
1162
|
agent_app, AgentType.EVALUATOR_OPTIMIZER, active_agents
|
|
1021
1163
|
)
|
|
1022
1164
|
|
|
1023
|
-
def
|
|
1024
|
-
self, name: str, visited: set, path: set
|
|
1165
|
+
def _get_dependencies(
|
|
1166
|
+
self, name: str, visited: set, path: set, agent_type: AgentType = None
|
|
1025
1167
|
) -> List[str]:
|
|
1026
1168
|
"""
|
|
1027
|
-
Get dependencies for
|
|
1169
|
+
Get dependencies for an agent in topological order.
|
|
1170
|
+
Works for both Parallel and Chain workflows.
|
|
1028
1171
|
|
|
1029
1172
|
Args:
|
|
1030
|
-
name: Name of the
|
|
1173
|
+
name: Name of the agent
|
|
1031
1174
|
visited: Set of already visited agents
|
|
1032
1175
|
path: Current path for cycle detection
|
|
1176
|
+
agent_type: Optional type filter (e.g., only check Parallel or Chain)
|
|
1033
1177
|
|
|
1034
1178
|
Returns:
|
|
1035
1179
|
List of agent names in dependency order
|
|
@@ -1039,7 +1183,7 @@ class FastAgent(ContextDependent):
|
|
|
1039
1183
|
"""
|
|
1040
1184
|
if name in path:
|
|
1041
1185
|
path_str = " -> ".join(path)
|
|
1042
|
-
raise
|
|
1186
|
+
raise CircularDependencyError(f"Path: {path_str} -> {name}")
|
|
1043
1187
|
|
|
1044
1188
|
if name in visited:
|
|
1045
1189
|
return []
|
|
@@ -1048,15 +1192,26 @@ class FastAgent(ContextDependent):
|
|
|
1048
1192
|
return []
|
|
1049
1193
|
|
|
1050
1194
|
config = self.agents[name]
|
|
1051
|
-
|
|
1195
|
+
|
|
1196
|
+
# Skip if not the requested type (when filtering)
|
|
1197
|
+
if agent_type and config["type"] != agent_type.value:
|
|
1052
1198
|
return []
|
|
1053
1199
|
|
|
1054
1200
|
path.add(name)
|
|
1055
1201
|
deps = []
|
|
1056
1202
|
|
|
1057
|
-
#
|
|
1058
|
-
|
|
1059
|
-
|
|
1203
|
+
# Handle dependencies based on agent type
|
|
1204
|
+
if config["type"] == AgentType.PARALLEL.value:
|
|
1205
|
+
# Get dependencies from fan-out agents
|
|
1206
|
+
for fan_out in config["fan_out"]:
|
|
1207
|
+
deps.extend(self._get_dependencies(fan_out, visited, path, agent_type))
|
|
1208
|
+
elif config["type"] == AgentType.CHAIN.value:
|
|
1209
|
+
# Get dependencies from sequence agents
|
|
1210
|
+
sequence = config.get("sequence", config.get("agents", []))
|
|
1211
|
+
for agent_name in sequence:
|
|
1212
|
+
deps.extend(
|
|
1213
|
+
self._get_dependencies(agent_name, visited, path, agent_type)
|
|
1214
|
+
)
|
|
1060
1215
|
|
|
1061
1216
|
# Add this agent after its dependencies
|
|
1062
1217
|
deps.append(name)
|
|
@@ -1065,54 +1220,95 @@ class FastAgent(ContextDependent):
|
|
|
1065
1220
|
|
|
1066
1221
|
return deps
|
|
1067
1222
|
|
|
1068
|
-
|
|
1069
|
-
self,
|
|
1223
|
+
def _get_parallel_dependencies(
|
|
1224
|
+
self, name: str, visited: set, path: set
|
|
1225
|
+
) -> List[str]:
|
|
1226
|
+
"""
|
|
1227
|
+
Get dependencies for a parallel agent in topological order.
|
|
1228
|
+
Legacy function that calls the more general _get_dependencies.
|
|
1229
|
+
|
|
1230
|
+
Args:
|
|
1231
|
+
name: Name of the parallel agent
|
|
1232
|
+
visited: Set of already visited agents
|
|
1233
|
+
path: Current path for cycle detection
|
|
1234
|
+
|
|
1235
|
+
Returns:
|
|
1236
|
+
List of agent names in dependency order
|
|
1237
|
+
|
|
1238
|
+
Raises:
|
|
1239
|
+
ValueError: If circular dependency detected
|
|
1240
|
+
"""
|
|
1241
|
+
return self._get_dependencies(name, visited, path, AgentType.PARALLEL)
|
|
1242
|
+
|
|
1243
|
+
async def _create_agents_in_dependency_order(
|
|
1244
|
+
self, agent_app: MCPApp, active_agents: ProxyDict, agent_type: AgentType
|
|
1070
1245
|
) -> ProxyDict:
|
|
1071
1246
|
"""
|
|
1072
|
-
Create
|
|
1247
|
+
Create agents in dependency order to avoid circular references.
|
|
1248
|
+
Works for both Parallel and Chain workflows.
|
|
1073
1249
|
|
|
1074
1250
|
Args:
|
|
1075
1251
|
agent_app: The main application instance
|
|
1076
1252
|
active_agents: Dictionary of already created agents/proxies
|
|
1253
|
+
agent_type: Type of agents to create (AgentType.PARALLEL or AgentType.CHAIN)
|
|
1077
1254
|
|
|
1078
1255
|
Returns:
|
|
1079
|
-
Dictionary of initialized
|
|
1256
|
+
Dictionary of initialized agents
|
|
1080
1257
|
"""
|
|
1081
|
-
|
|
1258
|
+
result_agents = {}
|
|
1082
1259
|
visited = set()
|
|
1083
1260
|
|
|
1084
|
-
# Get all
|
|
1085
|
-
|
|
1261
|
+
# Get all agents of the specified type
|
|
1262
|
+
agent_names = [
|
|
1086
1263
|
name
|
|
1087
1264
|
for name, agent_data in self.agents.items()
|
|
1088
|
-
if agent_data["type"] ==
|
|
1265
|
+
if agent_data["type"] == agent_type.value
|
|
1089
1266
|
]
|
|
1090
1267
|
|
|
1091
1268
|
# Create agents in dependency order
|
|
1092
|
-
for name in
|
|
1269
|
+
for name in agent_names:
|
|
1093
1270
|
# Get ordered dependencies if not already processed
|
|
1094
1271
|
if name not in visited:
|
|
1095
1272
|
try:
|
|
1096
|
-
ordered_agents = self.
|
|
1097
|
-
name, visited, set()
|
|
1273
|
+
ordered_agents = self._get_dependencies(
|
|
1274
|
+
name, visited, set(), agent_type
|
|
1098
1275
|
)
|
|
1099
1276
|
except ValueError as e:
|
|
1100
|
-
raise ValueError(
|
|
1277
|
+
raise ValueError(
|
|
1278
|
+
f"Error creating {agent_type.name.lower()} agent {name}: {str(e)}"
|
|
1279
|
+
)
|
|
1101
1280
|
|
|
1102
1281
|
# Create each agent in order
|
|
1103
1282
|
for agent_name in ordered_agents:
|
|
1104
|
-
if agent_name not in
|
|
1105
|
-
# Create one
|
|
1283
|
+
if agent_name not in result_agents:
|
|
1284
|
+
# Create one agent at a time using the generic method
|
|
1106
1285
|
agent_result = await self._create_agents_by_type(
|
|
1107
1286
|
agent_app,
|
|
1108
|
-
|
|
1287
|
+
agent_type,
|
|
1109
1288
|
active_agents,
|
|
1110
1289
|
agent_name=agent_name,
|
|
1111
1290
|
)
|
|
1112
1291
|
if agent_name in agent_result:
|
|
1113
|
-
|
|
1292
|
+
result_agents[agent_name] = agent_result[agent_name]
|
|
1293
|
+
|
|
1294
|
+
return result_agents
|
|
1295
|
+
|
|
1296
|
+
async def _create_parallel_agents(
|
|
1297
|
+
self, agent_app: MCPApp, active_agents: ProxyDict
|
|
1298
|
+
) -> ProxyDict:
|
|
1299
|
+
"""
|
|
1300
|
+
Create parallel execution agents in dependency order.
|
|
1301
|
+
|
|
1302
|
+
Args:
|
|
1303
|
+
agent_app: The main application instance
|
|
1304
|
+
active_agents: Dictionary of already created agents/proxies
|
|
1114
1305
|
|
|
1115
|
-
|
|
1306
|
+
Returns:
|
|
1307
|
+
Dictionary of initialized parallel agents
|
|
1308
|
+
"""
|
|
1309
|
+
return await self._create_agents_in_dependency_order(
|
|
1310
|
+
agent_app, active_agents, AgentType.PARALLEL
|
|
1311
|
+
)
|
|
1116
1312
|
|
|
1117
1313
|
async def _create_routers(
|
|
1118
1314
|
self, agent_app: MCPApp, active_agents: ProxyDict
|
|
@@ -1186,12 +1382,16 @@ class FastAgent(ContextDependent):
|
|
|
1186
1382
|
agent_app, active_agents
|
|
1187
1383
|
)
|
|
1188
1384
|
routers = await self._create_routers(agent_app, active_agents)
|
|
1385
|
+
chains = await self._create_agents_in_dependency_order(
|
|
1386
|
+
agent_app, active_agents, AgentType.CHAIN
|
|
1387
|
+
)
|
|
1189
1388
|
|
|
1190
1389
|
# Merge all agents into active_agents
|
|
1191
1390
|
active_agents.update(orchestrators)
|
|
1192
1391
|
active_agents.update(parallel_agents)
|
|
1193
1392
|
active_agents.update(evaluator_optimizers)
|
|
1194
1393
|
active_agents.update(routers)
|
|
1394
|
+
active_agents.update(chains)
|
|
1195
1395
|
|
|
1196
1396
|
# Create wrapper with all agents
|
|
1197
1397
|
wrapper = AgentApp(agent_app, active_agents)
|
|
@@ -1228,10 +1428,37 @@ class FastAgent(ContextDependent):
|
|
|
1228
1428
|
had_error = True
|
|
1229
1429
|
self._handle_error(
|
|
1230
1430
|
e,
|
|
1231
|
-
"Server Startup Error",
|
|
1431
|
+
"MCP Server Startup Error",
|
|
1232
1432
|
"There was an error starting up the MCP Server.",
|
|
1233
1433
|
)
|
|
1234
1434
|
raise SystemExit(1)
|
|
1435
|
+
|
|
1436
|
+
except ModelConfigError as e:
|
|
1437
|
+
had_error = True
|
|
1438
|
+
self._handle_error(
|
|
1439
|
+
e,
|
|
1440
|
+
"Model Configuration Error",
|
|
1441
|
+
"Common models: gpt-4o, o3-mini, sonnet, haiku. for o3, set reasoning effort with o3-mini.high",
|
|
1442
|
+
)
|
|
1443
|
+
raise SystemExit(1)
|
|
1444
|
+
|
|
1445
|
+
except CircularDependencyError as e:
|
|
1446
|
+
had_error = True
|
|
1447
|
+
self._handle_error(
|
|
1448
|
+
e,
|
|
1449
|
+
"Circular Dependency Detected",
|
|
1450
|
+
"Check your agent configuration for circular dependencies.",
|
|
1451
|
+
)
|
|
1452
|
+
raise SystemExit(1)
|
|
1453
|
+
|
|
1454
|
+
except PromptExitError as e:
|
|
1455
|
+
had_error = True
|
|
1456
|
+
self._handle_error(
|
|
1457
|
+
e,
|
|
1458
|
+
"User requested exit",
|
|
1459
|
+
)
|
|
1460
|
+
raise SystemExit(1)
|
|
1461
|
+
|
|
1235
1462
|
finally:
|
|
1236
1463
|
# Clean up any active agents without re-raising errors
|
|
1237
1464
|
if active_agents and not had_error:
|
|
@@ -1239,7 +1466,8 @@ class FastAgent(ContextDependent):
|
|
|
1239
1466
|
if isinstance(proxy, LLMAgentProxy):
|
|
1240
1467
|
try:
|
|
1241
1468
|
await proxy._agent.__aexit__(None, None, None)
|
|
1242
|
-
except Exception:
|
|
1469
|
+
except Exception as e:
|
|
1470
|
+
print(f"DEBUG {e.message}")
|
|
1243
1471
|
pass # Ignore cleanup errors
|
|
1244
1472
|
|
|
1245
1473
|
def _handle_error(
|
mcp_agent/human_input/handler.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from rich.panel import Panel
|
|
3
|
-
from rich.prompt import Prompt
|
|
4
3
|
|
|
5
4
|
from mcp_agent.console import console
|
|
6
5
|
from mcp_agent.human_input.types import (
|
|
@@ -8,10 +7,11 @@ from mcp_agent.human_input.types import (
|
|
|
8
7
|
HumanInputResponse,
|
|
9
8
|
)
|
|
10
9
|
from mcp_agent.progress_display import progress_display
|
|
10
|
+
from mcp_agent.core.enhanced_prompt import get_enhanced_input, handle_special_commands
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
async def console_input_callback(request: HumanInputRequest) -> HumanInputResponse:
|
|
14
|
-
"""Request input from a human user via console using
|
|
14
|
+
"""Request input from a human user via console using prompt_toolkit."""
|
|
15
15
|
|
|
16
16
|
# Prepare the prompt text
|
|
17
17
|
prompt_text = request.prompt
|
|
@@ -28,26 +28,51 @@ async def console_input_callback(request: HumanInputRequest) -> HumanInputRespon
|
|
|
28
28
|
padding=(1, 2),
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
# Extract agent name from metadata dictionary
|
|
32
|
+
agent_name = (
|
|
33
|
+
request.metadata.get("agent_name", "Unknown Agent")
|
|
34
|
+
if request.metadata
|
|
35
|
+
else "Unknown Agent"
|
|
36
|
+
)
|
|
37
|
+
|
|
31
38
|
# Use the context manager to pause the progress display while getting input
|
|
32
39
|
with progress_display.paused():
|
|
33
40
|
console.print(panel)
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
try:
|
|
43
|
+
if request.timeout_seconds:
|
|
44
|
+
try:
|
|
45
|
+
# Use get_enhanced_input with empty agent list to disable agent switching
|
|
46
|
+
response = await asyncio.wait_for(
|
|
47
|
+
get_enhanced_input(
|
|
48
|
+
agent_name=agent_name,
|
|
49
|
+
available_agent_names=[], # No agents for selection
|
|
50
|
+
show_stop_hint=False,
|
|
51
|
+
is_human_input=True,
|
|
52
|
+
toolbar_color="ansimagenta",
|
|
53
|
+
),
|
|
54
|
+
request.timeout_seconds,
|
|
55
|
+
)
|
|
56
|
+
except asyncio.TimeoutError:
|
|
57
|
+
console.print("\n[red]Timeout waiting for input[/red]")
|
|
58
|
+
raise TimeoutError("No response received within timeout period")
|
|
59
|
+
else:
|
|
60
|
+
response = await get_enhanced_input(
|
|
61
|
+
agent_name=agent_name,
|
|
62
|
+
available_agent_names=[], # No agents for selection
|
|
63
|
+
show_stop_hint=False,
|
|
64
|
+
is_human_input=True,
|
|
65
|
+
toolbar_color="ansimagenta",
|
|
43
66
|
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
|
|
68
|
+
# if response and (response.startswith("/") or response.startswith("@")):
|
|
69
|
+
await handle_special_commands(response)
|
|
70
|
+
|
|
71
|
+
except KeyboardInterrupt:
|
|
72
|
+
console.print("\n[yellow]Input interrupted[/yellow]")
|
|
73
|
+
response = ""
|
|
74
|
+
except EOFError:
|
|
75
|
+
console.print("\n[yellow]Input terminated[/yellow]")
|
|
76
|
+
response = ""
|
|
52
77
|
|
|
53
78
|
return HumanInputResponse(request_id=request.request_id, response=response.strip())
|
|
@@ -75,15 +75,15 @@ class ServerConnection:
|
|
|
75
75
|
|
|
76
76
|
# Signal we want to shut down
|
|
77
77
|
self._shutdown_event = Event()
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
# Track error state
|
|
80
80
|
self._error_occurred = False
|
|
81
81
|
self._error_message = None
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
def is_healthy(self) -> bool:
|
|
84
84
|
"""Check if the server connection is healthy and ready to use."""
|
|
85
85
|
return self.session is not None and not self._error_occurred
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
def reset_error_state(self) -> None:
|
|
88
88
|
"""Reset the error state, allowing reconnection attempts."""
|
|
89
89
|
self._error_occurred = False
|
|
@@ -216,10 +216,10 @@ class MCPConnectionManager(ContextDependent):
|
|
|
216
216
|
try:
|
|
217
217
|
# First request all servers to shutdown
|
|
218
218
|
await self.disconnect_all()
|
|
219
|
-
|
|
219
|
+
|
|
220
220
|
# Add a small delay to allow for clean shutdown
|
|
221
221
|
await asyncio.sleep(0.5)
|
|
222
|
-
|
|
222
|
+
|
|
223
223
|
# Then close the task group if it's active
|
|
224
224
|
if self._task_group_active:
|
|
225
225
|
await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
|
|
@@ -305,10 +305,12 @@ class MCPConnectionManager(ContextDependent):
|
|
|
305
305
|
server_conn = self.running_servers.get(server_name)
|
|
306
306
|
if server_conn and server_conn.is_healthy():
|
|
307
307
|
return server_conn
|
|
308
|
-
|
|
308
|
+
|
|
309
309
|
# If server exists but isn't healthy, remove it so we can create a new one
|
|
310
310
|
if server_conn:
|
|
311
|
-
logger.info(
|
|
311
|
+
logger.info(
|
|
312
|
+
f"{server_name}: Server exists but is unhealthy, recreating..."
|
|
313
|
+
)
|
|
312
314
|
self.running_servers.pop(server_name)
|
|
313
315
|
server_conn.request_shutdown()
|
|
314
316
|
|
|
@@ -326,9 +328,9 @@ class MCPConnectionManager(ContextDependent):
|
|
|
326
328
|
if not server_conn.is_healthy():
|
|
327
329
|
error_msg = server_conn._error_message or "Unknown error"
|
|
328
330
|
raise ServerInitializationError(
|
|
329
|
-
f"{server_name}: Failed to initialize
|
|
331
|
+
f"MCP Server: '{server_name}': Failed to initialize with error: '{error_msg}'. Check fastagent.config.yaml"
|
|
330
332
|
)
|
|
331
|
-
|
|
333
|
+
|
|
332
334
|
return server_conn
|
|
333
335
|
|
|
334
336
|
async def disconnect_server(self, server_name: str) -> None:
|
|
@@ -353,16 +355,16 @@ class MCPConnectionManager(ContextDependent):
|
|
|
353
355
|
"""Disconnect all servers that are running under this connection manager."""
|
|
354
356
|
# Get a copy of servers to shutdown
|
|
355
357
|
servers_to_shutdown = []
|
|
356
|
-
|
|
358
|
+
|
|
357
359
|
async with self._lock:
|
|
358
360
|
if not self.running_servers:
|
|
359
361
|
return
|
|
360
|
-
|
|
362
|
+
|
|
361
363
|
# Make a copy of the servers to shut down
|
|
362
364
|
servers_to_shutdown = list(self.running_servers.items())
|
|
363
365
|
# Clear the dict immediately to prevent any new access
|
|
364
366
|
self.running_servers.clear()
|
|
365
|
-
|
|
367
|
+
|
|
366
368
|
# Release the lock before waiting for servers to shut down
|
|
367
369
|
for name, conn in servers_to_shutdown:
|
|
368
370
|
logger.info(f"{name}: Requesting shutdown...")
|