fast-agent-mcp 0.0.15__py3-none-any.whl → 0.1.0__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.
Files changed (27) hide show
  1. {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.1.0.dist-info}/METADATA +121 -21
  2. {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.1.0.dist-info}/RECORD +27 -25
  3. mcp_agent/cli/__main__.py +3 -0
  4. mcp_agent/cli/commands/bootstrap.py +1 -1
  5. mcp_agent/cli/commands/setup.py +4 -1
  6. mcp_agent/cli/main.py +13 -3
  7. mcp_agent/config.py +19 -11
  8. mcp_agent/core/agent_app.py +1 -1
  9. mcp_agent/core/enhanced_prompt.py +13 -5
  10. mcp_agent/core/fastagent.py +87 -49
  11. mcp_agent/resources/examples/data-analysis/analysis-campaign.py +188 -0
  12. mcp_agent/resources/examples/data-analysis/analysis.py +26 -0
  13. mcp_agent/resources/examples/workflows/evaluator.py +3 -3
  14. mcp_agent/resources/examples/workflows/orchestrator.py +1 -1
  15. mcp_agent/resources/examples/workflows/parallel.py +0 -4
  16. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +229 -91
  17. mcp_agent/workflows/llm/augmented_llm_anthropic.py +16 -2
  18. mcp_agent/workflows/llm/augmented_llm_openai.py +13 -1
  19. mcp_agent/workflows/llm/prompt_utils.py +137 -0
  20. mcp_agent/workflows/orchestrator/orchestrator.py +252 -50
  21. mcp_agent/workflows/orchestrator/orchestrator_models.py +81 -9
  22. mcp_agent/workflows/orchestrator/orchestrator_prompts.py +112 -42
  23. mcp_agent/workflows/router/router_base.py +113 -21
  24. mcp_agent/workflows/router/router_llm.py +19 -5
  25. {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.1.0.dist-info}/WHEEL +0 -0
  26. {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.1.0.dist-info}/entry_points.txt +0 -0
  27. {fast_agent_mcp-0.0.15.dist-info → fast_agent_mcp-0.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,7 +21,7 @@ from mcp_agent.workflows.llm.augmented_llm import (
21
21
  )
22
22
  from mcp_agent.workflows.orchestrator.orchestrator_models import (
23
23
  format_plan_result,
24
- format_step_result,
24
+ format_step_result_text,
25
25
  NextStep,
26
26
  Plan,
27
27
  PlanResult,
@@ -129,6 +129,27 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
129
129
  self.logger = logger
130
130
  self.name = name
131
131
 
132
+ # Store agents by name - COMPLETE REWRITE OF AGENT STORAGE
133
+ self.agents = {}
134
+ for agent in available_agents:
135
+ # Fix: Remove all special handling of agent names and store them exactly as they are
136
+ agent_name = agent.name
137
+
138
+ # Verify if the name is actually "None" (string) or None (NoneType)
139
+ if agent_name == "None":
140
+ # Try to get a better name from config if available
141
+ if hasattr(agent, "config") and agent.config and agent.config.name:
142
+ agent_name = agent.config.name
143
+ elif agent_name is None:
144
+ # Try to get a better name from config if available
145
+ if hasattr(agent, "config") and agent.config and agent.config.name:
146
+ agent_name = agent.config.name
147
+ else:
148
+ agent_name = f"unnamed_agent_{len(self.agents)}"
149
+
150
+ self.logger.info(f"Adding agent '{agent_name}' to orchestrator")
151
+ self.agents[agent_name] = agent
152
+
132
153
  async def generate(
133
154
  self,
134
155
  message: str | MessageParamT | List[MessageParamT],
@@ -163,15 +184,29 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
163
184
  request_params: RequestParams | None = None,
164
185
  ) -> ModelT:
165
186
  """Request a structured LLM generation and return the result as a Pydantic model."""
187
+ import json
188
+ from pydantic import ValidationError
189
+
166
190
  params = self.get_request_params(request_params)
167
191
  result_str = await self.generate_str(message=message, request_params=params)
168
192
 
169
- # Use AugmentedLLM's structured output handling
170
- return await super().generate_structured(
171
- message=result_str,
172
- response_model=response_model,
173
- request_params=params,
174
- )
193
+ try:
194
+ # Directly parse JSON and create model instance
195
+ parsed_data = json.loads(result_str)
196
+ return response_model(**parsed_data)
197
+ except (json.JSONDecodeError, ValidationError) as e:
198
+ # Log the error and fall back to the original method if direct parsing fails
199
+ self.logger.error(
200
+ f"Direct JSON parsing failed: {str(e)}. Falling back to standard method."
201
+ )
202
+ self.logger.debug(f"Failed JSON content: {result_str}")
203
+
204
+ # Use AugmentedLLM's structured output handling as fallback
205
+ return await super().generate_structured(
206
+ message=result_str,
207
+ response_model=response_model,
208
+ request_params=params,
209
+ )
175
210
 
176
211
  async def execute(
177
212
  self, objective: str, request_params: RequestParams | None = None
@@ -205,11 +240,15 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
205
240
  )
206
241
  logger.debug(f"Iteration {iterations}: Iterative plan:", data=next_step)
207
242
  plan = Plan(steps=[next_step], is_complete=next_step.is_complete)
243
+ # Validate agent names in the plan early
244
+ self._validate_agent_names(plan)
208
245
  elif self.plan_type == "full":
209
246
  plan = await self._get_full_plan(
210
247
  objective=objective, plan_result=plan_result, request_params=params
211
248
  )
212
249
  logger.debug(f"Iteration {iterations}: Full Plan:", data=plan)
250
+ # Validate agent names in the plan early
251
+ self._validate_agent_names(plan)
213
252
  else:
214
253
  raise ValueError(f"Invalid plan type {self.plan_type}")
215
254
 
@@ -221,6 +260,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
221
260
  plan_result.is_complete = True
222
261
 
223
262
  # Synthesize final result into a single message
263
+ # Use the structured XML format for better context
224
264
  synthesis_prompt = SYNTHESIZE_PLAN_PROMPT_TEMPLATE.format(
225
265
  plan_result=format_plan_result(plan_result)
226
266
  )
@@ -265,6 +305,7 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
265
305
  """Execute a step's subtasks in parallel and synthesize results"""
266
306
 
267
307
  step_result = StepResult(step=step, task_results=[])
308
+ # Use structured XML format for context to help agents better understand the context
268
309
  context = format_plan_result(previous_result)
269
310
 
270
311
  # Execute tasks
@@ -272,16 +313,18 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
272
313
  error_tasks = []
273
314
 
274
315
  for task in step.tasks:
316
+ # Make sure we're using a valid agent name
275
317
  agent = self.agents.get(task.agent)
276
318
  if not agent:
277
- # Instead of failing the entire step, track this as an error task
319
+ # Log a more prominent error - this is a serious problem that shouldn't happen
320
+ # with the improved prompt
278
321
  self.logger.error(
279
- f"No agent found matching '{task.agent}'. Available agents: {list(self.agents.keys())}"
322
+ f"AGENT VALIDATION ERROR: No agent found matching '{task.agent}'. Available agents: {list(self.agents.keys())}"
280
323
  )
281
324
  error_tasks.append(
282
325
  (
283
326
  task,
284
- f"Error: Agent '{task.agent}' not found. Available agents: {', '.join(self.agents.keys())}",
327
+ f"Error: Agent '{task.agent}' not found. This indicates a problem with the plan generation. Available agents: {', '.join(self.agents.keys())}",
285
328
  )
286
329
  )
287
330
  continue
@@ -292,8 +335,14 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
292
335
  context=context,
293
336
  )
294
337
 
295
- # All agents should now be LLM-capable
296
- futures.append(agent._llm.generate_str(message=task_description))
338
+ # Handle both Agent objects and AugmentedLLM objects
339
+ from mcp_agent.workflows.llm.augmented_llm import AugmentedLLM
340
+
341
+ if isinstance(agent, AugmentedLLM):
342
+ futures.append(agent.generate_str(message=task_description))
343
+ else:
344
+ # Traditional Agent objects with _llm property
345
+ futures.append(agent._llm.generate_str(message=task_description))
297
346
 
298
347
  # Wait for all tasks (only if we have valid futures)
299
348
  results = await self.executor.execute(*futures) if futures else []
@@ -307,18 +356,29 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
307
356
 
308
357
  if task_index < len(results):
309
358
  result = results[task_index]
310
- step_result.add_task_result(
311
- TaskWithResult(**task.model_dump(), result=str(result))
359
+ # Create a TaskWithResult that includes the agent name for attribution
360
+ task_model = task.model_dump()
361
+ task_result = TaskWithResult(
362
+ description=task_model["description"],
363
+ agent=task_model["agent"], # Track which agent produced this result
364
+ result=str(result),
312
365
  )
366
+ step_result.add_task_result(task_result)
313
367
  task_index += 1
314
368
 
315
369
  # Add error task results
316
370
  for task, error_message in error_tasks:
371
+ task_model = task.model_dump()
317
372
  step_result.add_task_result(
318
- TaskWithResult(**task.model_dump(), result=error_message)
373
+ TaskWithResult(
374
+ description=task_model["description"],
375
+ agent=task_model["agent"],
376
+ result=f"ERROR: {error_message}",
377
+ )
319
378
  )
320
379
 
321
- step_result.result = format_step_result(step_result)
380
+ # Use text formatting for display in logs
381
+ step_result.result = format_step_result_text(step_result)
322
382
  return step_result
323
383
 
324
384
  async def _get_full_plan(
@@ -328,30 +388,92 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
328
388
  request_params: RequestParams | None = None,
329
389
  ) -> Plan:
330
390
  """Generate full plan considering previous results"""
391
+ import json
392
+ from pydantic import ValidationError
393
+ from mcp_agent.workflows.orchestrator.orchestrator_models import (
394
+ Plan,
395
+ Step,
396
+ AgentTask,
397
+ )
398
+
331
399
  params = self.get_request_params(request_params)
332
400
  params = params.model_copy(update={"use_history": False})
333
401
 
334
- agents = "\n".join(
335
- [
336
- f"{idx}. {self._format_agent_info(agent)}"
337
- for idx, agent in enumerate(self.agents, 1)
338
- ]
339
- )
402
+ # Debug: Print agent names before formatting
403
+ print("\n------ AGENT NAMES BEFORE FORMATTING ------")
404
+ for agent_name in self.agents.keys():
405
+ print(f"Agent name: '{agent_name}'")
406
+ print("------------------------------------------\n")
407
+
408
+ # Format agents without numeric prefixes for cleaner XML
409
+ agent_formats = []
410
+ for agent_name in self.agents.keys():
411
+ formatted = self._format_agent_info(agent_name)
412
+ agent_formats.append(formatted)
413
+ print(f"Formatted agent '{agent_name}':\n{formatted[:200]}...\n")
414
+
415
+ agents = "\n".join(agent_formats)
416
+
417
+ # Create clear plan status indicator for the template
418
+ plan_status = "Plan Status: Not Started"
419
+ if hasattr(plan_result, "is_complete"):
420
+ plan_status = (
421
+ "Plan Status: Complete"
422
+ if plan_result.is_complete
423
+ else "Plan Status: In Progress"
424
+ )
340
425
 
341
426
  prompt = FULL_PLAN_PROMPT_TEMPLATE.format(
342
427
  objective=objective,
343
428
  plan_result=format_plan_result(plan_result),
429
+ plan_status=plan_status,
344
430
  agents=agents,
345
431
  )
346
432
 
347
- # Use planner directly - no verb manipulation needed
348
- plan = await self.planner.generate_structured(
433
+ # Get raw JSON response from LLM
434
+ result_str = await self.planner.generate_str(
349
435
  message=prompt,
350
- response_model=Plan,
351
436
  request_params=params,
352
437
  )
353
438
 
354
- return plan
439
+ try:
440
+ # Parse JSON directly
441
+ data = json.loads(result_str)
442
+
443
+ # Create models manually to ensure agent names are preserved exactly as returned
444
+ steps = []
445
+ for step_data in data.get("steps", []):
446
+ tasks = []
447
+ for task_data in step_data.get("tasks", []):
448
+ # Create AgentTask directly from dict, preserving exact agent string
449
+ task = AgentTask(
450
+ description=task_data.get("description", ""),
451
+ agent=task_data.get("agent", ""), # Preserve exact agent name
452
+ )
453
+ tasks.append(task)
454
+
455
+ # Create Step with the exact task objects we created
456
+ step = Step(description=step_data.get("description", ""), tasks=tasks)
457
+ steps.append(step)
458
+
459
+ # Create final Plan
460
+ plan = Plan(steps=steps, is_complete=data.get("is_complete", False))
461
+
462
+ return plan
463
+
464
+ except (json.JSONDecodeError, ValidationError, KeyError) as e:
465
+ # Log detailed error and fall back to the original method as last resort
466
+ self.logger.error(f"Error parsing plan JSON: {str(e)}")
467
+ self.logger.debug(f"Failed JSON content: {result_str}")
468
+
469
+ # Use the normal structured parsing as fallback
470
+ plan = await self.planner.generate_structured(
471
+ message=result_str,
472
+ response_model=Plan,
473
+ request_params=params,
474
+ )
475
+
476
+ return plan
355
477
 
356
478
  async def _get_next_step(
357
479
  self,
@@ -360,54 +482,134 @@ class Orchestrator(AugmentedLLM[MessageParamT, MessageT]):
360
482
  request_params: RequestParams | None = None,
361
483
  ) -> NextStep:
362
484
  """Generate just the next needed step"""
485
+ import json
486
+ from pydantic import ValidationError
487
+ from mcp_agent.workflows.orchestrator.orchestrator_models import (
488
+ NextStep,
489
+ AgentTask,
490
+ )
491
+
363
492
  params = self.get_request_params(request_params)
364
493
  params = params.model_copy(update={"use_history": False})
365
494
 
495
+ # Format agents without numeric prefixes for cleaner XML
496
+ # FIX: Iterate over agent names instead of agent objects
366
497
  agents = "\n".join(
367
- [
368
- f"{idx}. {self._format_agent_info(agent)}"
369
- for idx, agent in enumerate(self.agents, 1)
370
- ]
498
+ [self._format_agent_info(agent_name) for agent_name in self.agents.keys()]
371
499
  )
372
500
 
501
+ # Create clear plan status indicator for the template
502
+ plan_status = "Plan Status: Not Started"
503
+ if hasattr(plan_result, "is_complete"):
504
+ plan_status = (
505
+ "Plan Status: Complete"
506
+ if plan_result.is_complete
507
+ else "Plan Status: In Progress"
508
+ )
509
+
373
510
  prompt = ITERATIVE_PLAN_PROMPT_TEMPLATE.format(
374
511
  objective=objective,
375
512
  plan_result=format_plan_result(plan_result),
513
+ plan_status=plan_status,
376
514
  agents=agents,
377
515
  )
378
516
 
379
- # Use planner directly - no verb manipulation needed
380
- next_step = await self.planner.generate_structured(
517
+ # Get raw JSON response from LLM
518
+ result_str = await self.planner.generate_str(
381
519
  message=prompt,
382
- response_model=NextStep,
383
520
  request_params=params,
384
521
  )
385
- return next_step
522
+
523
+ try:
524
+ # Parse JSON directly
525
+ data = json.loads(result_str)
526
+
527
+ # Create task objects manually to preserve exact agent names
528
+ tasks = []
529
+ for task_data in data.get("tasks", []):
530
+ # Preserve the exact agent name as specified in the JSON
531
+ task = AgentTask(
532
+ description=task_data.get("description", ""),
533
+ agent=task_data.get("agent", ""),
534
+ )
535
+ tasks.append(task)
536
+
537
+ # Create step with manually constructed tasks
538
+ next_step = NextStep(
539
+ description=data.get("description", ""),
540
+ tasks=tasks,
541
+ is_complete=data.get("is_complete", False),
542
+ )
543
+
544
+ return next_step
545
+
546
+ except (json.JSONDecodeError, ValidationError, KeyError) as e:
547
+ # Log detailed error and fall back to the original method
548
+ self.logger.error(f"Error parsing next step JSON: {str(e)}")
549
+ self.logger.debug(f"Failed JSON content: {result_str}")
550
+
551
+ # Use the normal structured parsing as fallback
552
+ next_step = await self.planner.generate_structured(
553
+ message=result_str,
554
+ response_model=NextStep,
555
+ request_params=params,
556
+ )
557
+
558
+ return next_step
386
559
 
387
560
  def _format_server_info(self, server_name: str) -> str:
388
- """Format server information for display to planners"""
561
+ """Format server information for display to planners using XML tags"""
562
+ from mcp_agent.workflows.llm.prompt_utils import format_server_info
563
+
389
564
  server_config = self.server_registry.get_server_config(server_name)
390
- server_str = f"Server Name: {server_name}"
391
- if not server_config:
392
- return server_str
393
565
 
394
- description = server_config.description
395
- if description:
396
- server_str = f"{server_str}\nDescription: {description}"
566
+ # Get description or empty string if not available
567
+ description = ""
568
+ if server_config and server_config.description:
569
+ description = server_config.description
570
+
571
+ return format_server_info(server_name, description)
572
+
573
+ def _validate_agent_names(self, plan: Plan) -> None:
574
+ """
575
+ Validate all agent names in a plan before execution.
576
+ This helps catch invalid agent references early.
577
+ """
578
+ invalid_agents = []
579
+
580
+ for step in plan.steps:
581
+ for task in step.tasks:
582
+ if task.agent not in self.agents:
583
+ invalid_agents.append(task.agent)
397
584
 
398
- return server_str
585
+ if invalid_agents:
586
+ available_agents = ", ".join(self.agents.keys())
587
+ invalid_list = ", ".join(invalid_agents)
588
+ error_msg = f"Plan contains invalid agent names: {invalid_list}. Available agents: {available_agents}"
589
+ self.logger.error(error_msg)
590
+ # We don't raise an exception here as the execution will handle invalid agents
591
+ # by logging errors for individual tasks
399
592
 
400
593
  def _format_agent_info(self, agent_name: str) -> str:
401
- """Format Agent information for display to planners"""
594
+ """Format Agent information for display to planners using XML tags"""
595
+ from mcp_agent.workflows.llm.prompt_utils import format_agent_info
596
+
402
597
  agent = self.agents.get(agent_name)
403
598
  if not agent:
599
+ self.logger.error(f"Agent '{agent_name}' not found in orchestrator agents")
404
600
  return ""
601
+ instruction = agent.instruction
405
602
 
406
- servers = "\n".join(
407
- [
408
- f"- {self._format_server_info(server_name)}"
409
- for server_name in agent.server_names
410
- ]
411
- )
603
+ # Get servers information
604
+ server_info = []
605
+ for server_name in agent.server_names:
606
+ server_config = self.server_registry.get_server_config(server_name)
607
+ description = ""
608
+ if server_config and server_config.description:
609
+ description = server_config.description
610
+
611
+ server_info.append({"name": server_name, "description": description})
412
612
 
413
- return f"Agent Name: {agent.name}\nDescription: {agent.instruction}\nServers in Agent: {servers}"
613
+ return format_agent_info(
614
+ agent.name, instruction, server_info if server_info else None
615
+ )
@@ -62,6 +62,10 @@ class TaskWithResult(Task):
62
62
  description="Result of executing the task", default="Task completed"
63
63
  )
64
64
 
65
+ agent: str = Field(
66
+ description="Name of the agent that executed this task", default=""
67
+ )
68
+
65
69
  model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
66
70
 
67
71
 
@@ -116,17 +120,17 @@ class NextStep(Step):
116
120
  )
117
121
 
118
122
 
119
- def format_task_result(task_result: TaskWithResult) -> str:
120
- """Format a task result for display to planners"""
123
+ def format_task_result_text(task_result: TaskWithResult) -> str:
124
+ """Format a task result as plain text for display"""
121
125
  return TASK_RESULT_TEMPLATE.format(
122
126
  task_description=task_result.description, task_result=task_result.result
123
127
  )
124
128
 
125
129
 
126
- def format_step_result(step_result: StepResult) -> str:
127
- """Format a step result for display to planners"""
130
+ def format_step_result_text(step_result: StepResult) -> str:
131
+ """Format a step result as plain text for display"""
128
132
  tasks_str = "\n".join(
129
- f" - {format_task_result(task)}" for task in step_result.task_results
133
+ f" - {format_task_result_text(task)}" for task in step_result.task_results
130
134
  )
131
135
  return STEP_RESULT_TEMPLATE.format(
132
136
  step_description=step_result.step.description,
@@ -135,11 +139,11 @@ def format_step_result(step_result: StepResult) -> str:
135
139
  )
136
140
 
137
141
 
138
- def format_plan_result(plan_result: PlanResult) -> str:
139
- """Format the full plan execution state for display to planners"""
142
+ def format_plan_result_text(plan_result: PlanResult) -> str:
143
+ """Format the full plan execution state as plain text for display"""
140
144
  steps_str = (
141
145
  "\n\n".join(
142
- f"{i + 1}:\n{format_step_result(step)}"
146
+ f"{i + 1}:\n{format_step_result_text(step)}"
143
147
  for i, step in enumerate(plan_result.step_results)
144
148
  )
145
149
  if plan_result.step_results
@@ -149,6 +153,74 @@ def format_plan_result(plan_result: PlanResult) -> str:
149
153
  return PLAN_RESULT_TEMPLATE.format(
150
154
  plan_objective=plan_result.objective,
151
155
  steps_str=steps_str,
152
- plan_status="Complete" if plan_result.is_complete else "In Progress",
153
156
  plan_result=plan_result.result if plan_result.is_complete else "In Progress",
154
157
  )
158
+
159
+
160
+ def format_task_result_xml(task_result: TaskWithResult) -> str:
161
+ """Format a task result with XML tags for better semantic understanding"""
162
+ from mcp_agent.workflows.llm.prompt_utils import format_fastagent_tag
163
+
164
+ return format_fastagent_tag(
165
+ "task-result",
166
+ f"\n<fastagent:description>{task_result.description}</fastagent:description>\n"
167
+ f"<fastagent:result>{task_result.result}</fastagent:result>\n",
168
+ {
169
+ "description": task_result.description[:50] + "..."
170
+ if len(task_result.description) > 50
171
+ else task_result.description
172
+ },
173
+ )
174
+
175
+
176
+ def format_step_result_xml(step_result: StepResult) -> str:
177
+ """Format a step result with XML tags for better semantic understanding"""
178
+ from mcp_agent.workflows.llm.prompt_utils import format_fastagent_tag
179
+
180
+ # Format each task result with XML
181
+ task_results = []
182
+ for task in step_result.task_results:
183
+ task_results.append(format_task_result_xml(task))
184
+
185
+ # Combine task results
186
+ task_results_str = "\n".join(task_results)
187
+
188
+ # Build step result with metadata and tasks
189
+ step_content = (
190
+ f"<fastagent:description>{step_result.step.description}</fastagent:description>\n"
191
+ f"<fastagent:summary>{step_result.result}</fastagent:summary>\n"
192
+ f"<fastagent:task-results>\n{task_results_str}\n</fastagent:task-results>\n"
193
+ )
194
+
195
+ return format_fastagent_tag("step-result", step_content)
196
+
197
+
198
+ def format_plan_result(plan_result: PlanResult) -> str:
199
+ """Format the full plan execution state with XML for better semantic understanding"""
200
+ from mcp_agent.workflows.llm.prompt_utils import format_fastagent_tag
201
+
202
+ # Format objective
203
+ objective_tag = format_fastagent_tag("objective", plan_result.objective)
204
+
205
+ # Format step results
206
+ step_results = []
207
+ for step in plan_result.step_results:
208
+ step_results.append(format_step_result_xml(step))
209
+
210
+ # Build progress section
211
+ if step_results:
212
+ steps_content = "\n".join(step_results)
213
+ progress_content = (
214
+ f"{objective_tag}\n"
215
+ f"<fastagent:steps>\n{steps_content}\n</fastagent:steps>\n"
216
+ f"<fastagent:status>{plan_result.result if plan_result.is_complete else 'In Progress'}</fastagent:status>\n"
217
+ )
218
+ else:
219
+ # No steps executed yet
220
+ progress_content = (
221
+ f"{objective_tag}\n"
222
+ f"<fastagent:steps>No steps executed yet</fastagent:steps>\n"
223
+ f"<fastagent:status>Not Started</fastagent:status>\n"
224
+ )
225
+
226
+ return format_fastagent_tag("progress", progress_content)