fast-agent-mcp 0.4.7__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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation utilities for FastAgent configuration and dependencies.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fast_agent.agents.agent_types import AgentType
|
|
8
|
+
from fast_agent.core.exceptions import (
|
|
9
|
+
AgentConfigError,
|
|
10
|
+
CircularDependencyError,
|
|
11
|
+
ServerConfigError,
|
|
12
|
+
)
|
|
13
|
+
from fast_agent.interfaces import LlmAgentProtocol
|
|
14
|
+
from fast_agent.llm.fastagent_llm import FastAgentLLM
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def validate_server_references(context, agents: dict[str, dict[str, Any]]) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Validate that all server references in agent configurations exist in config.
|
|
20
|
+
Raises ServerConfigError if any referenced servers are not defined.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
context: Application context
|
|
24
|
+
agents: Dictionary of agent configurations
|
|
25
|
+
"""
|
|
26
|
+
if not context.config.mcp or not context.config.mcp.servers:
|
|
27
|
+
available_servers = set()
|
|
28
|
+
else:
|
|
29
|
+
available_servers = set(context.config.mcp.servers.keys())
|
|
30
|
+
|
|
31
|
+
# Check each agent's server references
|
|
32
|
+
for name, agent_data in agents.items():
|
|
33
|
+
config = agent_data["config"]
|
|
34
|
+
if config.servers:
|
|
35
|
+
missing = [s for s in config.servers if s not in available_servers]
|
|
36
|
+
if missing:
|
|
37
|
+
raise ServerConfigError(
|
|
38
|
+
f"Missing server configuration for agent '{name}'",
|
|
39
|
+
f"The following servers are referenced but not defined in config: {', '.join(missing)}",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def validate_workflow_references(agents: dict[str, dict[str, Any]]) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Validate that all workflow references point to valid agents/workflows.
|
|
46
|
+
Also validates that referenced agents have required configuration.
|
|
47
|
+
Raises AgentConfigError if any validation fails.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
agents: Dictionary of agent configurations
|
|
51
|
+
"""
|
|
52
|
+
available_components = set(agents.keys())
|
|
53
|
+
|
|
54
|
+
for name, agent_data in agents.items():
|
|
55
|
+
agent_type = agent_data["type"] # This is a string from config
|
|
56
|
+
|
|
57
|
+
# Note: Compare string values from config with the Enum's string value
|
|
58
|
+
if agent_type == AgentType.PARALLEL.value:
|
|
59
|
+
# Check fan_in exists
|
|
60
|
+
fan_in = agent_data["fan_in"]
|
|
61
|
+
if fan_in and fan_in not in available_components:
|
|
62
|
+
raise AgentConfigError(
|
|
63
|
+
f"Parallel workflow '{name}' references non-existent fan_in component: {fan_in}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Check fan_out agents exist
|
|
67
|
+
fan_out = agent_data["fan_out"]
|
|
68
|
+
missing = [a for a in fan_out if a not in available_components]
|
|
69
|
+
if missing:
|
|
70
|
+
raise AgentConfigError(
|
|
71
|
+
f"Parallel workflow '{name}' references non-existent fan_out components: {', '.join(missing)}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
elif agent_type == AgentType.ORCHESTRATOR.value:
|
|
75
|
+
# Check all child agents exist and are properly configured
|
|
76
|
+
child_agents = agent_data["child_agents"]
|
|
77
|
+
missing = [a for a in child_agents if a not in available_components]
|
|
78
|
+
if missing:
|
|
79
|
+
raise AgentConfigError(
|
|
80
|
+
f"Orchestrator '{name}' references non-existent agents: {', '.join(missing)}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Validate child agents have required LLM configuration
|
|
84
|
+
for agent_name in child_agents:
|
|
85
|
+
child_data = agents[agent_name]
|
|
86
|
+
if child_data["type"] == AgentType.BASIC.value:
|
|
87
|
+
# For basic agents, we'll validate LLM config during creation
|
|
88
|
+
continue
|
|
89
|
+
# Check if it's a workflow type or has LLM capability
|
|
90
|
+
# Workflows like EvaluatorOptimizer and Parallel are valid for orchestrator
|
|
91
|
+
func = child_data["func"]
|
|
92
|
+
workflow_types = [
|
|
93
|
+
AgentType.EVALUATOR_OPTIMIZER.value,
|
|
94
|
+
AgentType.PARALLEL.value,
|
|
95
|
+
AgentType.ROUTER.value,
|
|
96
|
+
AgentType.CHAIN.value,
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
if not (
|
|
100
|
+
isinstance(func, FastAgentLLM)
|
|
101
|
+
or child_data["type"] in workflow_types
|
|
102
|
+
or (isinstance(func, LlmAgentProtocol) and func.llm is not None)
|
|
103
|
+
):
|
|
104
|
+
raise AgentConfigError(
|
|
105
|
+
f"Agent '{agent_name}' used by orchestrator '{name}' lacks LLM capability",
|
|
106
|
+
"All agents used by orchestrators must be LLM-capable (either an AugmentedLLM or implement LlmAgentProtocol)",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
elif agent_type == AgentType.ROUTER.value:
|
|
110
|
+
# Check all referenced agents exist
|
|
111
|
+
router_agents = agent_data["router_agents"]
|
|
112
|
+
missing = [a for a in router_agents if a not in available_components]
|
|
113
|
+
if missing:
|
|
114
|
+
raise AgentConfigError(
|
|
115
|
+
f"Router '{name}' references non-existent agents: {', '.join(missing)}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
elif agent_type == AgentType.EVALUATOR_OPTIMIZER.value:
|
|
119
|
+
# Check both evaluator and optimizer exist
|
|
120
|
+
evaluator = agent_data["evaluator"]
|
|
121
|
+
generator = agent_data["generator"]
|
|
122
|
+
missing = []
|
|
123
|
+
if evaluator not in available_components:
|
|
124
|
+
missing.append(f"evaluator: {evaluator}")
|
|
125
|
+
if generator not in available_components:
|
|
126
|
+
missing.append(f"generator: {generator}")
|
|
127
|
+
if missing:
|
|
128
|
+
raise AgentConfigError(
|
|
129
|
+
f"Evaluator-Optimizer '{name}' references non-existent components: {', '.join(missing)}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
elif agent_type == AgentType.CHAIN.value:
|
|
133
|
+
# Check that all agents in the sequence exist
|
|
134
|
+
sequence = agent_data.get("sequence", agent_data.get("agents", []))
|
|
135
|
+
missing = [a for a in sequence if a not in available_components]
|
|
136
|
+
if missing:
|
|
137
|
+
raise AgentConfigError(
|
|
138
|
+
f"Chain '{name}' references non-existent agents: {', '.join(missing)}"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_dependencies(
|
|
143
|
+
name: str,
|
|
144
|
+
agents: dict[str, dict[str, Any]],
|
|
145
|
+
visited: set,
|
|
146
|
+
path: set,
|
|
147
|
+
agent_type: AgentType = None,
|
|
148
|
+
) -> list[str]:
|
|
149
|
+
"""
|
|
150
|
+
Get dependencies for an agent in topological order.
|
|
151
|
+
Works for both Parallel and Chain workflows.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Name of the agent
|
|
155
|
+
agents: Dictionary of agent configurations
|
|
156
|
+
visited: Set of already visited agents
|
|
157
|
+
path: Current path for cycle detection
|
|
158
|
+
agent_type: Optional type filter (e.g., only check Parallel or Chain)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of agent names in dependency order
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
CircularDependencyError: If circular dependency detected
|
|
165
|
+
"""
|
|
166
|
+
if name in path:
|
|
167
|
+
path_str = " -> ".join(path)
|
|
168
|
+
raise CircularDependencyError(f"Path: {path_str} -> {name}")
|
|
169
|
+
|
|
170
|
+
if name in visited:
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
if name not in agents:
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
config = agents[name]
|
|
177
|
+
|
|
178
|
+
# Skip if not the requested type (when filtering)
|
|
179
|
+
if agent_type and config["type"] != agent_type.value:
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
path.add(name)
|
|
183
|
+
deps = []
|
|
184
|
+
|
|
185
|
+
# Handle dependencies based on agent type
|
|
186
|
+
if config["type"] == AgentType.PARALLEL.value:
|
|
187
|
+
# Get dependencies from fan-out agents
|
|
188
|
+
for fan_out in config["fan_out"]:
|
|
189
|
+
deps.extend(get_dependencies(fan_out, agents, visited, path, agent_type))
|
|
190
|
+
elif config["type"] == AgentType.CHAIN.value:
|
|
191
|
+
# Get dependencies from sequence agents
|
|
192
|
+
sequence = config.get("sequence", config.get("router_agents", []))
|
|
193
|
+
for agent_name in sequence:
|
|
194
|
+
deps.extend(get_dependencies(agent_name, agents, visited, path, agent_type))
|
|
195
|
+
|
|
196
|
+
# Add this agent after its dependencies
|
|
197
|
+
deps.append(name)
|
|
198
|
+
visited.add(name)
|
|
199
|
+
path.remove(name)
|
|
200
|
+
|
|
201
|
+
return deps
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_agent_dependencies(agent_data: dict[str, Any]) -> set[str]:
|
|
205
|
+
deps: set[str] = set()
|
|
206
|
+
agent_dependency_attribute_names = {
|
|
207
|
+
AgentType.CHAIN: ("sequence",),
|
|
208
|
+
AgentType.EVALUATOR_OPTIMIZER: ("evaluator", "generator", "eval_optimizer_agents"),
|
|
209
|
+
AgentType.ITERATIVE_PLANNER: ("child_agents",),
|
|
210
|
+
AgentType.ORCHESTRATOR: ("child_agents",),
|
|
211
|
+
AgentType.BASIC: ("child_agents",),
|
|
212
|
+
AgentType.PARALLEL: ("fan_out", "fan_in", "parallel_agents"),
|
|
213
|
+
AgentType.ROUTER: ("router_agents",),
|
|
214
|
+
}
|
|
215
|
+
agent_type = agent_data["type"]
|
|
216
|
+
dependency_names = agent_dependency_attribute_names.get(agent_type, None)
|
|
217
|
+
if dependency_names is None:
|
|
218
|
+
return deps
|
|
219
|
+
|
|
220
|
+
for dependency_name in dependency_names:
|
|
221
|
+
dependency_value = agent_data.get(dependency_name)
|
|
222
|
+
if dependency_value is None:
|
|
223
|
+
continue
|
|
224
|
+
if isinstance(dependency_value, str):
|
|
225
|
+
deps.add(dependency_value)
|
|
226
|
+
else:
|
|
227
|
+
# here, we have an implicit assumption that if it is not a None or a string, then it is a list
|
|
228
|
+
deps.update(dependency_value)
|
|
229
|
+
|
|
230
|
+
return deps
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_dependencies_groups(
|
|
234
|
+
agents_dict: dict[str, dict[str, Any]], allow_cycles: bool = False
|
|
235
|
+
) -> list[list[str]]:
|
|
236
|
+
"""
|
|
237
|
+
Get dependencies between agents and group them into dependency layers.
|
|
238
|
+
Each layer can be initialized in parallel.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
agents_dict: Dictionary of agent configurations
|
|
242
|
+
allow_cycles: Whether to allow cyclic dependencies
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
List of lists, where each inner list is a group of agents that can be initialized together
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
CircularDependencyError: If circular dependency detected and allow_cycles is False
|
|
249
|
+
"""
|
|
250
|
+
# Get all agent names
|
|
251
|
+
agent_names = list(agents_dict.keys())
|
|
252
|
+
|
|
253
|
+
# Dictionary to store dependencies for each agent
|
|
254
|
+
dependencies = {
|
|
255
|
+
name: get_agent_dependencies(agent_data) for name, agent_data in agents_dict.items()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Check for cycles if not allowed
|
|
259
|
+
if not allow_cycles:
|
|
260
|
+
visited = set()
|
|
261
|
+
path = set()
|
|
262
|
+
|
|
263
|
+
def visit(node) -> None:
|
|
264
|
+
if node in path:
|
|
265
|
+
path_str = " -> ".join(path) + " -> " + node
|
|
266
|
+
raise CircularDependencyError(f"Circular dependency detected: {path_str}")
|
|
267
|
+
if node in visited:
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
path.add(node)
|
|
271
|
+
for dep in dependencies[node]:
|
|
272
|
+
if dep in agent_names: # Skip dependencies to non-existent agents
|
|
273
|
+
visit(dep)
|
|
274
|
+
path.remove(node)
|
|
275
|
+
visited.add(node)
|
|
276
|
+
|
|
277
|
+
# Check each node
|
|
278
|
+
for name in agent_names:
|
|
279
|
+
if name not in visited:
|
|
280
|
+
visit(name)
|
|
281
|
+
|
|
282
|
+
# Group agents by dependency level
|
|
283
|
+
result = []
|
|
284
|
+
remaining = set(agent_names)
|
|
285
|
+
|
|
286
|
+
while remaining:
|
|
287
|
+
# Find all agents that have no remaining dependencies
|
|
288
|
+
current_level = set()
|
|
289
|
+
for name in remaining:
|
|
290
|
+
if not dependencies[name] & remaining: # If no dependencies in remaining agents
|
|
291
|
+
current_level.add(name)
|
|
292
|
+
|
|
293
|
+
if not current_level:
|
|
294
|
+
if allow_cycles:
|
|
295
|
+
# If cycles are allowed, just add one remaining node to break the cycle
|
|
296
|
+
current_level.add(next(iter(remaining)))
|
|
297
|
+
else:
|
|
298
|
+
# This should not happen if we checked for cycles
|
|
299
|
+
raise CircularDependencyError("Unresolvable dependency cycle detected")
|
|
300
|
+
|
|
301
|
+
# Add the current level to the result
|
|
302
|
+
result.append(list(current_level))
|
|
303
|
+
|
|
304
|
+
# Remove current level from remaining
|
|
305
|
+
remaining -= current_level
|
|
306
|
+
|
|
307
|
+
return result
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def validate_provider_keys_post_creation(active_agents: dict[str, Any]) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Validate that API keys are available for all created agents with LLMs.
|
|
313
|
+
|
|
314
|
+
This runs after agent creation when we have actual agent instances.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
active_agents: Dictionary of created agent instances
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
ProviderKeyError: If any required API key is missing
|
|
321
|
+
"""
|
|
322
|
+
for agent_name, agent in active_agents.items():
|
|
323
|
+
llm = getattr(agent, "_llm", None)
|
|
324
|
+
if llm:
|
|
325
|
+
# This throws a ProviderKeyError if the key is not present
|
|
326
|
+
llm._api_key()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Module for converting log events to progress events."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProgressAction(str, Enum):
|
|
9
|
+
"""Progress actions available in the system."""
|
|
10
|
+
|
|
11
|
+
STARTING = "Starting"
|
|
12
|
+
LOADED = "Loaded"
|
|
13
|
+
INITIALIZED = "Initialized"
|
|
14
|
+
CHATTING = "Chatting"
|
|
15
|
+
STREAMING = "Streaming" # Special action for real-time streaming updates
|
|
16
|
+
THINKING = "Thinking" # Special action for real-time thinking updates
|
|
17
|
+
ROUTING = "Routing"
|
|
18
|
+
PLANNING = "Planning"
|
|
19
|
+
READY = "Ready"
|
|
20
|
+
CALLING_TOOL = "Calling Tool"
|
|
21
|
+
TOOL_PROGRESS = "Tool Progress"
|
|
22
|
+
UPDATED = "Updated"
|
|
23
|
+
FINISHED = "Finished"
|
|
24
|
+
SHUTDOWN = "Shutdown"
|
|
25
|
+
AGGREGATOR_INITIALIZED = "Running"
|
|
26
|
+
SERVER_OFFLINE = "Offline"
|
|
27
|
+
SERVER_RECONNECTING = "Reconnecting"
|
|
28
|
+
SERVER_ONLINE = "Online"
|
|
29
|
+
FATAL_ERROR = "Error"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProgressEvent(BaseModel):
|
|
33
|
+
"""Represents a progress event converted from a log event."""
|
|
34
|
+
|
|
35
|
+
action: ProgressAction
|
|
36
|
+
target: str
|
|
37
|
+
details: str | None = None
|
|
38
|
+
agent_name: str | None = None
|
|
39
|
+
correlation_id: str | None = None
|
|
40
|
+
instance_name: str | None = None
|
|
41
|
+
tool_name: str | None = None
|
|
42
|
+
streaming_tokens: str | None = None # Special field for streaming token count
|
|
43
|
+
progress: float | None = None # Current progress value
|
|
44
|
+
total: float | None = None # Total value for progress calculation
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
"""Format the progress event for display."""
|
|
48
|
+
# Special handling for streaming - show token count in action position
|
|
49
|
+
if self.action == ProgressAction.STREAMING and self.streaming_tokens:
|
|
50
|
+
# For streaming, show just the token count instead of "Streaming"
|
|
51
|
+
action_display = self.streaming_tokens.ljust(11)
|
|
52
|
+
base = f"{action_display}. {self.target}"
|
|
53
|
+
if self.details:
|
|
54
|
+
base += f" - {self.details}"
|
|
55
|
+
else:
|
|
56
|
+
base = f"{self.action.ljust(11)}. {self.target}"
|
|
57
|
+
if self.details:
|
|
58
|
+
base += f" - {self.details}"
|
|
59
|
+
|
|
60
|
+
if self.agent_name:
|
|
61
|
+
base = f"[{self.agent_name}] {base}"
|
|
62
|
+
return base
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
History export utilities for agents.
|
|
3
|
+
|
|
4
|
+
Provides a minimal, type-friendly way to save an agent's message history
|
|
5
|
+
without using control strings. Uses the existing serialization helpers
|
|
6
|
+
to choose JSON (for .json files) or Markdown-like delimited text otherwise.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from fast_agent.mcp.prompt_serialization import save_messages
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from fast_agent.interfaces import AgentProtocol
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HistoryExporter:
|
|
20
|
+
"""Utility for exporting agent history to a file."""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
async def save(agent: AgentProtocol, filename: str | None = None) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Save the given agent's message history to a file.
|
|
26
|
+
|
|
27
|
+
If filename ends with ".json", the history is saved in MCP JSON format.
|
|
28
|
+
Otherwise, it is saved in a human-readable Markdown-style format.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
agent: The agent whose history will be saved.
|
|
32
|
+
filename: Optional filename. If None, a default timestamped name is generated.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The path that was written to.
|
|
36
|
+
"""
|
|
37
|
+
# Determine a default filename when not provided
|
|
38
|
+
if not filename:
|
|
39
|
+
from datetime import datetime
|
|
40
|
+
timestamp = datetime.now().strftime("%y_%m_%d_%H_%M")
|
|
41
|
+
target = f"{timestamp}-conversation.json"
|
|
42
|
+
else:
|
|
43
|
+
target = filename
|
|
44
|
+
|
|
45
|
+
messages = agent.message_history
|
|
46
|
+
save_messages(messages, target)
|
|
47
|
+
|
|
48
|
+
# Return and optionally print a small confirmation
|
|
49
|
+
return target
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Human input modules for forms and elicitation.
|
|
2
|
+
|
|
3
|
+
Keep __init__ lightweight to avoid circular imports during submodule import.
|
|
4
|
+
Exports schema builders directly and defers simple form API imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fast_agent.human_input.form_fields import (
|
|
8
|
+
BooleanField,
|
|
9
|
+
EnumField,
|
|
10
|
+
FormSchema,
|
|
11
|
+
IntegerField,
|
|
12
|
+
NumberField,
|
|
13
|
+
StringField,
|
|
14
|
+
boolean,
|
|
15
|
+
choice,
|
|
16
|
+
date,
|
|
17
|
+
datetime,
|
|
18
|
+
email,
|
|
19
|
+
integer,
|
|
20
|
+
number,
|
|
21
|
+
string,
|
|
22
|
+
url,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Schema builder
|
|
27
|
+
"FormSchema",
|
|
28
|
+
# Field classes
|
|
29
|
+
"StringField",
|
|
30
|
+
"IntegerField",
|
|
31
|
+
"NumberField",
|
|
32
|
+
"BooleanField",
|
|
33
|
+
"EnumField",
|
|
34
|
+
# Field convenience functions
|
|
35
|
+
"string",
|
|
36
|
+
"email",
|
|
37
|
+
"url",
|
|
38
|
+
"date",
|
|
39
|
+
"datetime",
|
|
40
|
+
"integer",
|
|
41
|
+
"number",
|
|
42
|
+
"boolean",
|
|
43
|
+
"choice",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Note: form(), ask() helpers are available via fast_agent.human_input.simple_form;
|
|
47
|
+
# intentionally not imported here to avoid import-time cycles.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from fast_agent.human_input.elicitation_state import elicitation_state
|
|
4
|
+
from fast_agent.human_input.types import (
|
|
5
|
+
HumanInputRequest,
|
|
6
|
+
HumanInputResponse,
|
|
7
|
+
)
|
|
8
|
+
from fast_agent.tools.elicitation import set_elicitation_input_callback
|
|
9
|
+
from fast_agent.ui.elicitation_form import (
|
|
10
|
+
show_simple_elicitation_form,
|
|
11
|
+
)
|
|
12
|
+
from fast_agent.ui.elicitation_style import (
|
|
13
|
+
ELICITATION_STYLE,
|
|
14
|
+
)
|
|
15
|
+
from fast_agent.ui.progress_display import progress_display
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def elicitation_input_callback(
|
|
19
|
+
request: HumanInputRequest,
|
|
20
|
+
agent_name: str | None = None,
|
|
21
|
+
server_name: str | None = None,
|
|
22
|
+
server_info: dict[str, Any] | None = None,
|
|
23
|
+
) -> HumanInputResponse:
|
|
24
|
+
"""Request input from a human user for MCP server elicitation requests."""
|
|
25
|
+
|
|
26
|
+
# Extract effective names
|
|
27
|
+
effective_agent_name = agent_name or (
|
|
28
|
+
request.metadata.get("agent_name", "Unknown Agent") if request.metadata else "Unknown Agent"
|
|
29
|
+
)
|
|
30
|
+
effective_server_name = server_name or "Unknown Server"
|
|
31
|
+
|
|
32
|
+
# Start tracking elicitation operation
|
|
33
|
+
try:
|
|
34
|
+
from fast_agent.ui import notification_tracker
|
|
35
|
+
notification_tracker.start_elicitation(effective_server_name)
|
|
36
|
+
except Exception:
|
|
37
|
+
# Don't let notification tracking break elicitation
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# Check if elicitation is disabled for this server
|
|
42
|
+
if elicitation_state.is_disabled(effective_server_name):
|
|
43
|
+
return HumanInputResponse(
|
|
44
|
+
request_id=request.request_id,
|
|
45
|
+
response="__CANCELLED__",
|
|
46
|
+
metadata={"auto_cancelled": True, "reason": "Server elicitation disabled by user"},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Get the elicitation schema from metadata
|
|
50
|
+
schema: dict[str, Any] | None = None
|
|
51
|
+
if request.metadata and "requested_schema" in request.metadata:
|
|
52
|
+
schema = request.metadata["requested_schema"]
|
|
53
|
+
|
|
54
|
+
# Use the context manager to pause the progress display while getting input
|
|
55
|
+
with progress_display.paused():
|
|
56
|
+
try:
|
|
57
|
+
if schema:
|
|
58
|
+
form_action, form_data = await show_simple_elicitation_form(
|
|
59
|
+
schema=schema,
|
|
60
|
+
message=request.prompt,
|
|
61
|
+
agent_name=effective_agent_name,
|
|
62
|
+
server_name=effective_server_name,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if form_action == "accept" and form_data is not None:
|
|
66
|
+
# Convert form data to JSON string
|
|
67
|
+
import json
|
|
68
|
+
|
|
69
|
+
response = json.dumps(form_data)
|
|
70
|
+
elif form_action == "decline":
|
|
71
|
+
response = "__DECLINED__"
|
|
72
|
+
elif form_action == "disable":
|
|
73
|
+
response = "__DISABLE_SERVER__"
|
|
74
|
+
else: # cancel
|
|
75
|
+
response = "__CANCELLED__"
|
|
76
|
+
else:
|
|
77
|
+
# No schema, fall back to text input using prompt_toolkit only
|
|
78
|
+
from prompt_toolkit.shortcuts import input_dialog
|
|
79
|
+
|
|
80
|
+
response = await input_dialog(
|
|
81
|
+
title="Input Requested",
|
|
82
|
+
text=f"Agent: {effective_agent_name}\nServer: {effective_server_name}\n\n{request.prompt}",
|
|
83
|
+
style=ELICITATION_STYLE,
|
|
84
|
+
).run_async()
|
|
85
|
+
|
|
86
|
+
if response is None:
|
|
87
|
+
response = "__CANCELLED__"
|
|
88
|
+
|
|
89
|
+
except KeyboardInterrupt:
|
|
90
|
+
response = "__CANCELLED__"
|
|
91
|
+
except EOFError:
|
|
92
|
+
response = "__CANCELLED__"
|
|
93
|
+
|
|
94
|
+
return HumanInputResponse(
|
|
95
|
+
request_id=request.request_id,
|
|
96
|
+
response=response.strip() if isinstance(response, str) else response,
|
|
97
|
+
metadata={"has_schema": schema is not None},
|
|
98
|
+
)
|
|
99
|
+
finally:
|
|
100
|
+
# End tracking elicitation operation
|
|
101
|
+
try:
|
|
102
|
+
from fast_agent.ui import notification_tracker
|
|
103
|
+
notification_tracker.end_elicitation(effective_server_name)
|
|
104
|
+
except Exception:
|
|
105
|
+
# Don't let notification tracking break elicitation
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Register adapter with fast_agent tools so they can invoke this UI handler without importing types
|
|
110
|
+
async def _elicitation_adapter(
|
|
111
|
+
request_payload: dict,
|
|
112
|
+
agent_name: str | None = None,
|
|
113
|
+
server_name: str | None = None,
|
|
114
|
+
server_info: dict[str, Any] | None = None,
|
|
115
|
+
) -> str:
|
|
116
|
+
req = HumanInputRequest(**request_payload)
|
|
117
|
+
resp = await elicitation_input_callback(
|
|
118
|
+
request=req, agent_name=agent_name, server_name=server_name, server_info=server_info
|
|
119
|
+
)
|
|
120
|
+
return resp.response if isinstance(resp.response, str) else str(resp.response)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
set_elicitation_input_callback(_elicitation_adapter)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Simple state management for elicitation Cancel All functionality."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ElicitationState:
|
|
6
|
+
"""Manages global state for elicitation requests, including disabled servers."""
|
|
7
|
+
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.disabled_servers: set[str] = set()
|
|
10
|
+
|
|
11
|
+
def disable_server(self, server_name: str) -> None:
|
|
12
|
+
"""Disable elicitation requests for a specific server."""
|
|
13
|
+
self.disabled_servers.add(server_name)
|
|
14
|
+
|
|
15
|
+
def is_disabled(self, server_name: str) -> bool:
|
|
16
|
+
"""Check if elicitation is disabled for a server."""
|
|
17
|
+
return server_name in self.disabled_servers
|
|
18
|
+
|
|
19
|
+
def enable_server(self, server_name: str) -> None:
|
|
20
|
+
"""Re-enable elicitation requests for a specific server."""
|
|
21
|
+
self.disabled_servers.discard(server_name)
|
|
22
|
+
|
|
23
|
+
def clear_all(self) -> None:
|
|
24
|
+
"""Clear all disabled servers."""
|
|
25
|
+
self.disabled_servers.clear()
|
|
26
|
+
|
|
27
|
+
def get_disabled_servers(self) -> set[str]:
|
|
28
|
+
"""Get a copy of all disabled servers."""
|
|
29
|
+
return self.disabled_servers.copy()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Global instance for session-scoped Cancel All functionality
|
|
33
|
+
elicitation_state = ElicitationState()
|