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,652 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Iterative Planner Agent - works towards an objective using sub-agents
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
7
|
+
|
|
8
|
+
from mcp import Tool
|
|
9
|
+
from mcp.types import TextContent
|
|
10
|
+
|
|
11
|
+
from fast_agent.agents.agent_types import AgentConfig, AgentType
|
|
12
|
+
from fast_agent.agents.llm_agent import LlmAgent
|
|
13
|
+
from fast_agent.agents.workflow.orchestrator_models import (
|
|
14
|
+
Plan,
|
|
15
|
+
PlanningStep,
|
|
16
|
+
PlanResult,
|
|
17
|
+
Step,
|
|
18
|
+
TaskWithResult,
|
|
19
|
+
format_plan_result,
|
|
20
|
+
format_step_result_text,
|
|
21
|
+
)
|
|
22
|
+
from fast_agent.core.exceptions import AgentConfigError
|
|
23
|
+
from fast_agent.core.logging.logger import get_logger
|
|
24
|
+
from fast_agent.core.prompt import Prompt
|
|
25
|
+
from fast_agent.interfaces import AgentProtocol, ModelT
|
|
26
|
+
from fast_agent.types import PromptMessageExtended, RequestParams
|
|
27
|
+
from fast_agent.workflow_telemetry import (
|
|
28
|
+
NoOpPlanTelemetryProvider,
|
|
29
|
+
PlanEntry,
|
|
30
|
+
PlanEntryStatus,
|
|
31
|
+
PlanTelemetryProvider,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
ITERATIVE_PLAN_SYSTEM_PROMPT_TEMPLATE = """
|
|
38
|
+
You are an expert planner, able to Orchestrate complex tasks by breaking them down in to
|
|
39
|
+
manageable steps, and delegating tasks to Agents.
|
|
40
|
+
|
|
41
|
+
You work iteratively - given an Objective, you consider the current state of the plan,
|
|
42
|
+
decide the next step towards the goal. You document those steps and create clear instructions
|
|
43
|
+
for execution by the Agents, being specific about what you need to know to assess task completion.
|
|
44
|
+
|
|
45
|
+
NOTE: A 'Planning Step' has a description, and a list of tasks that can be delegated
|
|
46
|
+
and executed in parallel.
|
|
47
|
+
|
|
48
|
+
Agents have a 'description' describing their primary function, and a set of 'skills' that
|
|
49
|
+
represent Tools they can use in completing their function.
|
|
50
|
+
|
|
51
|
+
The following Agents are available to you:
|
|
52
|
+
|
|
53
|
+
{{agents}}
|
|
54
|
+
|
|
55
|
+
You must specify the Agent name precisely when generating a Planning Step.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
ITERATIVE_PLAN_PROMPT_TEMPLATE2 = """
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
<fastagent:data>
|
|
64
|
+
<fastagent:progress>
|
|
65
|
+
{plan_result}
|
|
66
|
+
</fastagent:progress>
|
|
67
|
+
|
|
68
|
+
<fastagent:status>
|
|
69
|
+
{plan_status}
|
|
70
|
+
{iterations_info}
|
|
71
|
+
</fastagent:status>
|
|
72
|
+
</fastagent:data>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The overall objective is:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
<fastagent:objective>
|
|
79
|
+
{objective}
|
|
80
|
+
</fastagent:objective>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Produce the next step in the plan to complete the Objective.
|
|
84
|
+
|
|
85
|
+
Consider the previous steps and results, and decide what needs to be done next.
|
|
86
|
+
|
|
87
|
+
Set "is_complete" to true when ANY of these conditions are met:
|
|
88
|
+
1. The objective has been achieved in full or substantively
|
|
89
|
+
2. The remaining work is minor or trivial compared to what's been accomplished
|
|
90
|
+
3. The plan has gathered sufficient information to answer the original request
|
|
91
|
+
4. The plan has no feasible way of completing the objective.
|
|
92
|
+
|
|
93
|
+
Only set is_complete to `true` if there are no outstanding tasks.
|
|
94
|
+
|
|
95
|
+
Be decisive - avoid excessive planning steps that add little value. It's better to complete a plan early than to continue with marginal improvements.
|
|
96
|
+
|
|
97
|
+
Focus on the meeting the core intent of the objective.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
DELEGATED_TASK_TEMPLATE = """
|
|
103
|
+
You are a component in an orchestrated workflow to achieve an objective.
|
|
104
|
+
|
|
105
|
+
The overall objective is:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
<fastagent:objective>
|
|
109
|
+
{objective}
|
|
110
|
+
</fastagent:objective>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Previous context in achieving the objective is below:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
<fastagent:context>
|
|
117
|
+
{context}
|
|
118
|
+
</fastagent:context>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Your job is to accomplish the "task" specified in `<fastagent:task>`. The overall objective and
|
|
122
|
+
previous context is supplied to inform your approach.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
<fastagent:task>
|
|
126
|
+
{task}
|
|
127
|
+
</fastagent:task>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Provide a direct, concise response on completion that makes it simple to assess the status of
|
|
131
|
+
the overall plan.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
PLAN_RESULT_TEMPLATE = """
|
|
136
|
+
The below shows the results of running a plan to meet the specified objective.
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
<fastagent:plan-results>
|
|
140
|
+
{plan_result}
|
|
141
|
+
</fastagent:plan-results>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The plan was stopped because {termination_reason}.
|
|
145
|
+
|
|
146
|
+
Provide a summary of the tasks completed and their outcomes to complete the Objective.
|
|
147
|
+
Use markdown formatting.
|
|
148
|
+
|
|
149
|
+
If the plan was marked as incomplete but the maximum number of iterations was reached,
|
|
150
|
+
make sure to state clearly what was accomplished and what remains to be done.
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
Complete the plan by providing an appropriate answer for the original objective. Provide a Mermaid diagram
|
|
154
|
+
(in code fences) showing the plan steps and their relationships, if applicable. Do not use parentheses in labels.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class IterativePlanner(LlmAgent):
|
|
159
|
+
"""
|
|
160
|
+
An agent that implements the orchestrator workflow pattern.
|
|
161
|
+
|
|
162
|
+
Dynamically creates execution plans and delegates tasks
|
|
163
|
+
to specialized worker agents, synthesizing their results into a cohesive output.
|
|
164
|
+
Supports both full planning and iterative planning modes.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def agent_type(self) -> AgentType:
|
|
169
|
+
"""Return the type of this agent."""
|
|
170
|
+
return AgentType.ITERATIVE_PLANNER
|
|
171
|
+
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
config: AgentConfig,
|
|
175
|
+
agents: List[AgentProtocol],
|
|
176
|
+
plan_iterations: int = -1,
|
|
177
|
+
context: Optional[Any] = None,
|
|
178
|
+
**kwargs,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Initialize an OrchestratorAgent.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
config: Agent configuration or name
|
|
185
|
+
agents: List of specialized worker agents available for task execution
|
|
186
|
+
plan_type: Planning mode ("full" or "iterative")
|
|
187
|
+
context: Optional context object
|
|
188
|
+
**kwargs: Additional keyword arguments to pass to BaseAgent
|
|
189
|
+
"""
|
|
190
|
+
if not agents:
|
|
191
|
+
raise AgentConfigError("At least one worker agent must be provided")
|
|
192
|
+
|
|
193
|
+
# Store agents by name for easier lookup
|
|
194
|
+
self.agents: Dict[str, AgentProtocol] = {}
|
|
195
|
+
for agent in agents:
|
|
196
|
+
agent_name = agent.name
|
|
197
|
+
self.agents[agent_name] = agent
|
|
198
|
+
|
|
199
|
+
# Extract plan_type from kwargs before passing to parent
|
|
200
|
+
kwargs.pop("plan_type", "full")
|
|
201
|
+
super().__init__(config, context=context, **kwargs)
|
|
202
|
+
|
|
203
|
+
self.plan_iterations = plan_iterations
|
|
204
|
+
self._plan_telemetry_provider: PlanTelemetryProvider = NoOpPlanTelemetryProvider()
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def plan_telemetry(self) -> PlanTelemetryProvider:
|
|
208
|
+
"""Telemetry provider for emitting plan updates."""
|
|
209
|
+
return self._plan_telemetry_provider
|
|
210
|
+
|
|
211
|
+
@plan_telemetry.setter
|
|
212
|
+
def plan_telemetry(self, provider: PlanTelemetryProvider | None) -> None:
|
|
213
|
+
if provider is None:
|
|
214
|
+
provider = NoOpPlanTelemetryProvider()
|
|
215
|
+
self._plan_telemetry_provider = provider
|
|
216
|
+
|
|
217
|
+
async def initialize(self) -> None:
|
|
218
|
+
"""Initialize the orchestrator agent and worker agents."""
|
|
219
|
+
# Initialize all worker agents first if not already initialized
|
|
220
|
+
for agent_name, agent in self.agents.items():
|
|
221
|
+
if not getattr(agent, "initialized", False):
|
|
222
|
+
logger.debug(f"Initializing agent: {agent_name}")
|
|
223
|
+
await agent.initialize()
|
|
224
|
+
|
|
225
|
+
# Format agent information using agent cards with XML formatting
|
|
226
|
+
agent_descriptions = []
|
|
227
|
+
for agent_name, agent in self.agents.items():
|
|
228
|
+
agent_card = await agent.agent_card()
|
|
229
|
+
# Format as XML for better readability in prompts
|
|
230
|
+
xml_formatted = self._format_agent_card_as_xml(agent_card)
|
|
231
|
+
agent_descriptions.append(xml_formatted)
|
|
232
|
+
|
|
233
|
+
agents_str = "\n".join(agent_descriptions)
|
|
234
|
+
|
|
235
|
+
# Replace {{agents}} placeholder in the system prompt template
|
|
236
|
+
system_prompt = self.config.instruction.replace("{{agents}}", agents_str)
|
|
237
|
+
|
|
238
|
+
# Update the config instruction with the formatted system prompt
|
|
239
|
+
self.instruction = system_prompt
|
|
240
|
+
# Initialize the base agent with the updated system prompt
|
|
241
|
+
await super().initialize()
|
|
242
|
+
|
|
243
|
+
self.initialized = True
|
|
244
|
+
|
|
245
|
+
async def shutdown(self) -> None:
|
|
246
|
+
"""Shutdown the orchestrator agent and worker agents."""
|
|
247
|
+
await super().shutdown()
|
|
248
|
+
|
|
249
|
+
# Shutdown all worker agents
|
|
250
|
+
for agent_name, agent in self.agents.items():
|
|
251
|
+
try:
|
|
252
|
+
await agent.shutdown()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.warning(f"Error shutting down agent {agent_name}: {str(e)}")
|
|
255
|
+
|
|
256
|
+
async def generate_impl(
|
|
257
|
+
self,
|
|
258
|
+
messages: List[PromptMessageExtended],
|
|
259
|
+
request_params: RequestParams | None = None,
|
|
260
|
+
tools: List[Tool] | None = None,
|
|
261
|
+
) -> PromptMessageExtended:
|
|
262
|
+
"""
|
|
263
|
+
Execute an orchestrated plan to process the input.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
normalized_messages: Already normalized list of PromptMessageExtended
|
|
267
|
+
request_params: Optional request parameters
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
The final synthesized response from the orchestration
|
|
271
|
+
"""
|
|
272
|
+
# Extract user request
|
|
273
|
+
objective = messages[-1].all_text() if messages else ""
|
|
274
|
+
plan_result = await self._execute_plan(objective, request_params)
|
|
275
|
+
# Return the result
|
|
276
|
+
return PromptMessageExtended(
|
|
277
|
+
role="assistant",
|
|
278
|
+
content=[TextContent(type="text", text=plan_result.result or "No result available")],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
async def structured_impl(
|
|
282
|
+
self,
|
|
283
|
+
messages: List[PromptMessageExtended],
|
|
284
|
+
model: Type[ModelT],
|
|
285
|
+
request_params: Optional[RequestParams] = None,
|
|
286
|
+
) -> Tuple[ModelT | None, PromptMessageExtended]:
|
|
287
|
+
"""
|
|
288
|
+
Execute an orchestration plan and parse the result into a structured format.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
messages: List of messages to process
|
|
292
|
+
model: Pydantic model to parse the response into
|
|
293
|
+
request_params: Optional request parameters
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
The parsed final response, or None if parsing fails
|
|
297
|
+
"""
|
|
298
|
+
# Generate orchestration result
|
|
299
|
+
response = await self.generate_impl(messages, request_params)
|
|
300
|
+
|
|
301
|
+
# Try to parse the response into the specified model
|
|
302
|
+
try:
|
|
303
|
+
result_text = response.last_text() or "<no text>"
|
|
304
|
+
prompt_message = PromptMessageExtended(
|
|
305
|
+
role="user", content=[TextContent(type="text", text=result_text)]
|
|
306
|
+
)
|
|
307
|
+
assert self._llm
|
|
308
|
+
return await self._llm.structured([prompt_message], model, request_params)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.warning(f"Failed to parse orchestration result: {str(e)}")
|
|
311
|
+
return None, Prompt.assistant(f"Failed to parse orchestration result: {str(e)}")
|
|
312
|
+
|
|
313
|
+
async def _execute_plan(
|
|
314
|
+
self, objective: str, request_params: RequestParams | None
|
|
315
|
+
) -> PlanResult:
|
|
316
|
+
"""
|
|
317
|
+
Execute a plan to achieve the given objective.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
objective: The objective to achieve
|
|
321
|
+
request_params: Request parameters for execution
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
PlanResult containing execution results and final output
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
objective_met: bool = False
|
|
328
|
+
terminate_plan: str | None = None
|
|
329
|
+
plan_result = PlanResult(objective=objective, step_results=[])
|
|
330
|
+
|
|
331
|
+
# Track plan entries for telemetry
|
|
332
|
+
plan_entries: list[PlanEntry] = []
|
|
333
|
+
|
|
334
|
+
while not objective_met and not terminate_plan:
|
|
335
|
+
next_step: PlanningStep | None = await self._get_next_step(
|
|
336
|
+
objective, plan_result, request_params
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if None is next_step:
|
|
340
|
+
terminate_plan = "Failed to generate plan, terminating early"
|
|
341
|
+
logger.error("Failed to generate next step, terminating plan early")
|
|
342
|
+
break
|
|
343
|
+
|
|
344
|
+
assert next_step # lets keep the indenting manageable!
|
|
345
|
+
|
|
346
|
+
if next_step.is_complete:
|
|
347
|
+
objective_met = True
|
|
348
|
+
terminate_plan = "Plan completed successfully"
|
|
349
|
+
# Mark all entries as completed
|
|
350
|
+
for entry in plan_entries:
|
|
351
|
+
entry.status = "completed"
|
|
352
|
+
await self.plan_telemetry.update_plan(plan_entries)
|
|
353
|
+
break
|
|
354
|
+
|
|
355
|
+
plan = Plan(steps=[next_step], is_complete=next_step.is_complete)
|
|
356
|
+
invalid_agents = self._validate_agent_names(plan)
|
|
357
|
+
if invalid_agents:
|
|
358
|
+
logger.error(f"Plan contains invalid agent names: {', '.join(invalid_agents)}")
|
|
359
|
+
terminate_plan = (
|
|
360
|
+
f"Invalid agent names found ({', '.join(invalid_agents)}), terminating plan"
|
|
361
|
+
)
|
|
362
|
+
break
|
|
363
|
+
|
|
364
|
+
# Add subtasks as plan entries for telemetry so clients see actionable work
|
|
365
|
+
step_entries = self._build_plan_entries_for_step(next_step)
|
|
366
|
+
plan_entries.extend(step_entries)
|
|
367
|
+
await self.plan_telemetry.update_plan(plan_entries)
|
|
368
|
+
|
|
369
|
+
# Mark subtasks as in progress while they execute
|
|
370
|
+
self._set_plan_entries_status(step_entries, "in_progress")
|
|
371
|
+
await self.plan_telemetry.update_plan(plan_entries)
|
|
372
|
+
|
|
373
|
+
for step in plan.steps: # this will only be one for iterative (change later)
|
|
374
|
+
step_result = await self._execute_step(step, plan_result)
|
|
375
|
+
plan_result.add_step_result(step_result)
|
|
376
|
+
|
|
377
|
+
# Mark subtasks as completed
|
|
378
|
+
self._set_plan_entries_status(step_entries, "completed")
|
|
379
|
+
await self.plan_telemetry.update_plan(plan_entries)
|
|
380
|
+
|
|
381
|
+
# Store plan in result
|
|
382
|
+
plan_result.plan = plan
|
|
383
|
+
|
|
384
|
+
if self.plan_iterations > 0:
|
|
385
|
+
if len(plan_result.step_results) >= self.plan_iterations:
|
|
386
|
+
terminate_plan = f"Reached maximum number of iterations ({self.plan_iterations}), terminating plan"
|
|
387
|
+
|
|
388
|
+
if not terminate_plan:
|
|
389
|
+
terminate_plan = "Unknown termination reason"
|
|
390
|
+
result_prompt = PLAN_RESULT_TEMPLATE.format(
|
|
391
|
+
plan_result=format_plan_result(plan_result), termination_reason=terminate_plan
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Generate final synthesis
|
|
395
|
+
final_message = await self._planner_generate_str(result_prompt, request_params)
|
|
396
|
+
plan_result.result = final_message.last_text() or "No final message generated"
|
|
397
|
+
await self.show_assistant_message(final_message)
|
|
398
|
+
return plan_result
|
|
399
|
+
|
|
400
|
+
async def _execute_step(self, step: Step, previous_result: PlanResult) -> Any:
|
|
401
|
+
"""
|
|
402
|
+
Execute a single step from the plan.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
step: The step to execute
|
|
406
|
+
previous_result: Results of the plan execution so far
|
|
407
|
+
request_params: Request parameters
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Result of executing the step
|
|
411
|
+
"""
|
|
412
|
+
from fast_agent.agents.workflow.orchestrator_models import StepResult
|
|
413
|
+
|
|
414
|
+
# Initialize step result
|
|
415
|
+
step_result = StepResult(step=step, task_results=[])
|
|
416
|
+
|
|
417
|
+
# Format context for tasks
|
|
418
|
+
context = format_plan_result(previous_result)
|
|
419
|
+
|
|
420
|
+
# Group tasks by agent and execute different agents in parallel
|
|
421
|
+
from collections import defaultdict
|
|
422
|
+
|
|
423
|
+
tasks_by_agent = defaultdict(list)
|
|
424
|
+
for task in step.tasks:
|
|
425
|
+
tasks_by_agent[task.agent].append(task)
|
|
426
|
+
|
|
427
|
+
async def execute_agent_tasks(agent_name: str, agent_tasks: List) -> List[TaskWithResult]:
|
|
428
|
+
"""Execute all tasks for a single agent sequentially (preserves history)"""
|
|
429
|
+
agent = self.agents.get(agent_name)
|
|
430
|
+
assert agent is not None
|
|
431
|
+
|
|
432
|
+
results = []
|
|
433
|
+
for task in agent_tasks:
|
|
434
|
+
try:
|
|
435
|
+
task_description = DELEGATED_TASK_TEMPLATE.format(
|
|
436
|
+
objective=previous_result.objective, task=task.description, context=context
|
|
437
|
+
)
|
|
438
|
+
result = await agent.generate(
|
|
439
|
+
[
|
|
440
|
+
PromptMessageExtended(
|
|
441
|
+
role="user",
|
|
442
|
+
content=[TextContent(type="text", text=task_description)],
|
|
443
|
+
)
|
|
444
|
+
]
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
task_model = task.model_dump()
|
|
448
|
+
results.append(
|
|
449
|
+
TaskWithResult(
|
|
450
|
+
description=task_model["description"],
|
|
451
|
+
agent=task_model["agent"],
|
|
452
|
+
result=result.last_text() or "<missing response>",
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
except Exception as e:
|
|
456
|
+
logger.error(f"Error executing task: {str(e)}")
|
|
457
|
+
task_model = task.model_dump()
|
|
458
|
+
results.append(
|
|
459
|
+
TaskWithResult(
|
|
460
|
+
description=task_model["description"],
|
|
461
|
+
agent=task_model["agent"],
|
|
462
|
+
result=f"ERROR: {str(e)}",
|
|
463
|
+
)
|
|
464
|
+
)
|
|
465
|
+
return results
|
|
466
|
+
|
|
467
|
+
# Execute different agents in parallel, tasks within each agent sequentially
|
|
468
|
+
agent_futures = [
|
|
469
|
+
execute_agent_tasks(agent_name, agent_tasks)
|
|
470
|
+
for agent_name, agent_tasks in tasks_by_agent.items()
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
all_results = await asyncio.gather(*agent_futures)
|
|
474
|
+
task_results = [result for agent_results in all_results for result in agent_results]
|
|
475
|
+
|
|
476
|
+
# Add all task results to step result
|
|
477
|
+
for task_result in task_results:
|
|
478
|
+
step_result.add_task_result(task_result)
|
|
479
|
+
|
|
480
|
+
# Format step result
|
|
481
|
+
step_result.result = format_step_result_text(step_result)
|
|
482
|
+
return step_result
|
|
483
|
+
|
|
484
|
+
async def _get_next_step(
|
|
485
|
+
self, objective: str, plan_result: PlanResult, request_params: RequestParams | None
|
|
486
|
+
) -> PlanningStep | None:
|
|
487
|
+
"""
|
|
488
|
+
Generate just the next step for iterative planning.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
objective: The objective to achieve
|
|
492
|
+
plan_result: Current plan execution state
|
|
493
|
+
request_params: Request parameters
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Next step to execute, or None if parsing fails
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
# Determine plan status
|
|
500
|
+
if plan_result.is_complete:
|
|
501
|
+
plan_status = "Plan Status: Complete"
|
|
502
|
+
elif plan_result.step_results:
|
|
503
|
+
plan_status = "Plan Status: In Progress"
|
|
504
|
+
else:
|
|
505
|
+
plan_status = "Plan Status: Not Started"
|
|
506
|
+
|
|
507
|
+
# Calculate iteration information
|
|
508
|
+
|
|
509
|
+
if self.plan_iterations > 0:
|
|
510
|
+
max_iterations = self.plan_iterations
|
|
511
|
+
current_iteration = len(plan_result.step_results)
|
|
512
|
+
iterations_remaining = max_iterations - current_iteration
|
|
513
|
+
iterations_info = (
|
|
514
|
+
f"Planning Budget: {iterations_remaining} of {max_iterations} iterations remaining"
|
|
515
|
+
)
|
|
516
|
+
else:
|
|
517
|
+
iterations_info = "Iterating until objective is met."
|
|
518
|
+
|
|
519
|
+
# Format the planning prompt
|
|
520
|
+
prompt = ITERATIVE_PLAN_PROMPT_TEMPLATE2.format(
|
|
521
|
+
objective=objective,
|
|
522
|
+
plan_result=format_plan_result(plan_result),
|
|
523
|
+
plan_status=plan_status,
|
|
524
|
+
iterations_info=iterations_info,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
request_params = self._merge_request_params(
|
|
528
|
+
request_params, RequestParams(systemPrompt=self.instruction)
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Get structured response from LLM
|
|
532
|
+
try:
|
|
533
|
+
plan_msg = PromptMessageExtended(
|
|
534
|
+
role="user", content=[TextContent(type="text", text=prompt)]
|
|
535
|
+
)
|
|
536
|
+
assert self._llm
|
|
537
|
+
self.show_user_message(plan_msg)
|
|
538
|
+
next_step, raw_response = await self._llm.structured(
|
|
539
|
+
[plan_msg], PlanningStep, request_params
|
|
540
|
+
)
|
|
541
|
+
await self.show_assistant_message(raw_response)
|
|
542
|
+
return next_step
|
|
543
|
+
except Exception as e:
|
|
544
|
+
logger.error(f"Failed to parse next step: {str(e)}")
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
def _validate_agent_names(self, plan: Plan) -> List[str]:
|
|
548
|
+
"""
|
|
549
|
+
Validate all agent names in a plan before execution.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
plan: The plan to validate
|
|
553
|
+
"""
|
|
554
|
+
invalid_agents = []
|
|
555
|
+
|
|
556
|
+
for step in plan.steps:
|
|
557
|
+
for task in step.tasks:
|
|
558
|
+
if task.agent not in self.agents:
|
|
559
|
+
invalid_agents.append(task.agent)
|
|
560
|
+
|
|
561
|
+
return invalid_agents
|
|
562
|
+
|
|
563
|
+
@staticmethod
|
|
564
|
+
def _format_agent_card_as_xml(agent_card) -> str:
|
|
565
|
+
"""
|
|
566
|
+
Format an agent card as XML for display in prompts.
|
|
567
|
+
|
|
568
|
+
This creates a structured XML representation that's more readable than JSON
|
|
569
|
+
and includes all relevant agent information in a hierarchical format.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
agent_card: The AgentCard object
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
XML formatted agent information
|
|
576
|
+
"""
|
|
577
|
+
xml_parts = [f'<fastagent:agent name="{agent_card.name}">']
|
|
578
|
+
|
|
579
|
+
# Add description if available
|
|
580
|
+
if agent_card.description:
|
|
581
|
+
xml_parts.append(
|
|
582
|
+
f" <fastagent:description>{agent_card.description}</fastagent:description>"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Add skills if available
|
|
586
|
+
if hasattr(agent_card, "skills") and agent_card.skills:
|
|
587
|
+
xml_parts.append(" <fastagent:skills>")
|
|
588
|
+
for skill in agent_card.skills:
|
|
589
|
+
xml_parts.append(f' <fastagent:skill name="{skill.name}">')
|
|
590
|
+
if hasattr(skill, "description") and skill.description:
|
|
591
|
+
xml_parts.append(
|
|
592
|
+
f" <fastagent:description>{skill.description}</fastagent:description>"
|
|
593
|
+
)
|
|
594
|
+
xml_parts.append(" </fastagent:skill>")
|
|
595
|
+
xml_parts.append(" </fastagent:skills>")
|
|
596
|
+
|
|
597
|
+
xml_parts.append("</fastagent:agent>")
|
|
598
|
+
|
|
599
|
+
return "\n".join(xml_parts)
|
|
600
|
+
|
|
601
|
+
async def _planner_generate_str(
|
|
602
|
+
self, message: str, request_params: RequestParams | None
|
|
603
|
+
) -> PromptMessageExtended:
|
|
604
|
+
"""
|
|
605
|
+
Generate string response from the orchestrator's own LLM.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
message: Message to send to the LLM
|
|
609
|
+
request_params: Request parameters
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
String response from the LLM
|
|
613
|
+
"""
|
|
614
|
+
# Create prompt message
|
|
615
|
+
prompt = PromptMessageExtended(
|
|
616
|
+
role="user", content=[TextContent(type="text", text=message)]
|
|
617
|
+
)
|
|
618
|
+
assert self._llm, "LLM must be initialized before generating text"
|
|
619
|
+
return await self._llm.generate([prompt], request_params)
|
|
620
|
+
|
|
621
|
+
@staticmethod
|
|
622
|
+
def _build_plan_entries_for_step(step: PlanningStep | Step) -> list[PlanEntry]:
|
|
623
|
+
"""
|
|
624
|
+
Build plan entries for telemetry using individual step tasks when available.
|
|
625
|
+
"""
|
|
626
|
+
if step.tasks:
|
|
627
|
+
entries = []
|
|
628
|
+
for task in step.tasks:
|
|
629
|
+
agent_name = getattr(task, "agent", None) or "Unknown agent"
|
|
630
|
+
entries.append(
|
|
631
|
+
PlanEntry(
|
|
632
|
+
content=f"{agent_name}: {task.description}",
|
|
633
|
+
priority="high",
|
|
634
|
+
status="pending",
|
|
635
|
+
)
|
|
636
|
+
)
|
|
637
|
+
return entries
|
|
638
|
+
|
|
639
|
+
# Fallback to a single entry if the planner supplied no explicit tasks
|
|
640
|
+
return [
|
|
641
|
+
PlanEntry(
|
|
642
|
+
content=step.description,
|
|
643
|
+
priority="high",
|
|
644
|
+
status="pending",
|
|
645
|
+
)
|
|
646
|
+
]
|
|
647
|
+
|
|
648
|
+
@staticmethod
|
|
649
|
+
def _set_plan_entries_status(entries: list[PlanEntry], status: PlanEntryStatus) -> None:
|
|
650
|
+
"""Helper to bulk update plan entry statuses."""
|
|
651
|
+
for entry in entries:
|
|
652
|
+
entry.status = status
|