agent-framework-core 1.4.0__tar.gz → 1.6.0__tar.gz

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 (91) hide show
  1. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/PKG-INFO +1 -1
  2. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_feature_stage.py +2 -0
  3. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_mcp.py +150 -24
  4. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_skills.py +97 -9
  5. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent.py +31 -16
  6. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent_executor.py +2 -2
  7. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_edge_runner.py +34 -6
  8. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_events.py +34 -18
  9. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_functional.py +2 -1
  10. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_runner_context.py +21 -1
  11. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_validation.py +29 -3
  12. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow.py +175 -27
  13. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_builder.py +217 -13
  14. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_context.py +28 -2
  15. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_executor.py +17 -1
  16. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/observability.py +177 -27
  17. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/pyproject.toml +1 -1
  18. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/LICENSE +0 -0
  19. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/README.md +0 -0
  20. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/__init__.py +0 -0
  21. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_agents.py +0 -0
  22. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_clients.py +0 -0
  23. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_compaction.py +0 -0
  24. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_docstrings.py +0 -0
  25. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_evaluation.py +0 -0
  26. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_harness/__init__.py +0 -0
  27. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_memory.py +0 -0
  28. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_mode.py +0 -0
  29. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_todo.py +0 -0
  30. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_middleware.py +0 -0
  31. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_serialization.py +0 -0
  32. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_sessions.py +0 -0
  33. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_settings.py +0 -0
  34. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_telemetry.py +0 -0
  35. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_tools.py +0 -0
  36. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_types.py +0 -0
  37. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/__init__.py +0 -0
  38. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent_utils.py +0 -0
  39. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_checkpoint.py +0 -0
  40. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_checkpoint_encoding.py +0 -0
  41. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_const.py +0 -0
  42. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_conversation_history.py +0 -0
  43. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_edge.py +0 -0
  44. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_executor.py +0 -0
  45. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_function_executor.py +0 -0
  46. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_message_utils.py +0 -0
  47. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_model_utils.py +0 -0
  48. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_request_info_mixin.py +0 -0
  49. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_runner.py +0 -0
  50. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_state.py +0 -0
  51. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_typing_utils.py +0 -0
  52. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_viz.py +0 -0
  53. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/a2a/__init__.py +0 -0
  54. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/a2a/__init__.pyi +0 -0
  55. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/ag_ui/__init__.py +0 -0
  56. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/ag_ui/__init__.pyi +0 -0
  57. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/amazon/__init__.py +0 -0
  58. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/amazon/__init__.pyi +0 -0
  59. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/anthropic/__init__.py +0 -0
  60. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/anthropic/__init__.pyi +0 -0
  61. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/azure/__init__.py +0 -0
  62. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/azure/__init__.pyi +0 -0
  63. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/chatkit/__init__.py +0 -0
  64. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/chatkit/__init__.pyi +0 -0
  65. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/declarative/__init__.py +0 -0
  66. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/declarative/__init__.pyi +0 -0
  67. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/devui/__init__.py +0 -0
  68. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/devui/__init__.pyi +0 -0
  69. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/exceptions.py +0 -0
  70. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/foundry/__init__.py +0 -0
  71. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/foundry/__init__.pyi +0 -0
  72. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/github/__init__.py +0 -0
  73. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/github/__init__.pyi +0 -0
  74. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/google/__init__.py +0 -0
  75. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/google/__init__.pyi +0 -0
  76. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/hyperlight/__init__.py +0 -0
  77. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/lab/__init__.py +0 -0
  78. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/mem0/__init__.py +0 -0
  79. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/mem0/__init__.pyi +0 -0
  80. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/microsoft/__init__.py +0 -0
  81. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/microsoft/__init__.pyi +0 -0
  82. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/ollama/__init__.py +0 -0
  83. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/ollama/__init__.pyi +0 -0
  84. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/openai/__init__.py +0 -0
  85. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/openai/__init__.pyi +0 -0
  86. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/orchestrations/__init__.py +0 -0
  87. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/orchestrations/__init__.pyi +0 -0
  88. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/py.typed +0 -0
  89. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/redis/__init__.py +0 -0
  90. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/redis/__init__.pyi +0 -0
  91. {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-core
3
- Version: 1.4.0
3
+ Version: 1.6.0
4
4
  Summary: Microsoft Agent Framework for building AI Agents with Python. This is the core package that has all the core abstractions and implementations.
5
5
  Author-email: Microsoft <af-support@microsoft.com>
6
6
  Requires-Python: >=3.10
@@ -49,6 +49,8 @@ class ExperimentalFeature(str, Enum):
49
49
  EVALS = "EVALS"
50
50
  FILE_HISTORY = "FILE_HISTORY"
51
51
  FIDES = "FIDES"
52
+ FOUNDRY_TOOLS = "FOUNDRY_TOOLS"
53
+ FOUNDRY_PREVIEW_TOOLS = "FOUNDRY_PREVIEW_TOOLS"
52
54
  FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
53
55
  HARNESS = "HARNESS"
54
56
  SKILLS = "SKILLS"
@@ -255,7 +255,7 @@ class MCPTool:
255
255
  self._exit_stack = AsyncExitStack()
256
256
  self._lifecycle_lock = asyncio.Lock()
257
257
  self._lifecycle_request_lock = asyncio.Lock()
258
- self._lifecycle_queue: asyncio.Queue[tuple[str, bool, asyncio.Future[None]]] | None = None
258
+ self._lifecycle_queue: asyncio.Queue[tuple[str, bool, bool, asyncio.Future[None]]] | None = None
259
259
  self._lifecycle_owner_task: asyncio.Task[None] | None = None
260
260
  self.session = session
261
261
  self.request_timeout = request_timeout
@@ -265,6 +265,11 @@ class MCPTool:
265
265
  self.is_connected: bool = False
266
266
  self._tools_loaded: bool = False
267
267
  self._prompts_loaded: bool = False
268
+ self._server_capabilities: types.ServerCapabilities | None = None
269
+ self._supports_tools: bool = True
270
+ self._supports_prompts: bool = True
271
+ self._supports_logging: bool | None = None
272
+ self._ping_available: bool = True
268
273
  self._pending_reload_tasks: set[asyncio.Task[None]] = set()
269
274
 
270
275
  def __str__(self) -> str:
@@ -566,11 +571,11 @@ class MCPTool:
566
571
  stop_error: BaseException | None = None
567
572
  try:
568
573
  while True:
569
- action, reset, future = await queue.get()
574
+ action, reset, load_configured, future = await queue.get()
570
575
 
571
576
  try:
572
577
  if action == "connect":
573
- await self._connect_on_owner(reset=reset)
578
+ await self._connect_on_owner(reset=reset, load_configured=load_configured)
574
579
  elif action == "close":
575
580
  await self._close_on_owner()
576
581
  else:
@@ -595,7 +600,7 @@ class MCPTool:
595
600
  finally:
596
601
  while True:
597
602
  try:
598
- _, _, future = queue.get_nowait()
603
+ _, _, _, future = queue.get_nowait()
599
604
  except asyncio.QueueEmpty:
600
605
  break
601
606
  if not future.done():
@@ -608,12 +613,18 @@ class MCPTool:
608
613
  owner_task = self._lifecycle_owner_task
609
614
  return owner_task is not None and asyncio.current_task() is owner_task
610
615
 
611
- async def _run_on_lifecycle_owner(self, action: str, *, reset: bool = False) -> None:
616
+ async def _run_on_lifecycle_owner(
617
+ self,
618
+ action: str,
619
+ *,
620
+ reset: bool = False,
621
+ load_configured: bool = True,
622
+ ) -> None:
612
623
  await self._ensure_lifecycle_owner()
613
624
 
614
625
  if self._is_lifecycle_owner_task():
615
626
  if action == "connect":
616
- await self._connect_on_owner(reset=reset)
627
+ await self._connect_on_owner(reset=reset, load_configured=load_configured)
617
628
  elif action == "close":
618
629
  await self._close_on_owner()
619
630
  else:
@@ -625,7 +636,7 @@ class MCPTool:
625
636
  raise RuntimeError("MCP lifecycle owner is not available.")
626
637
 
627
638
  future = asyncio.get_running_loop().create_future()
628
- await queue.put((action, reset, future))
639
+ await queue.put((action, reset, load_configured, future))
629
640
  await future
630
641
 
631
642
  async def _safe_close_exit_stack(self) -> None:
@@ -656,6 +667,32 @@ class MCPTool:
656
667
  await self._safe_close_exit_stack()
657
668
  return _should_propagate_cancelled_error(ex)
658
669
 
670
+ def _reset_session_state(self) -> None:
671
+ self._server_capabilities = None
672
+ self._supports_tools = True
673
+ self._supports_prompts = True
674
+ self._supports_logging = None
675
+ self._ping_available = True
676
+
677
+ def _set_server_capabilities(self, capabilities: types.ServerCapabilities | None) -> None:
678
+ self._server_capabilities = capabilities
679
+ if capabilities is None:
680
+ self._supports_tools = False
681
+ self._supports_prompts = False
682
+ self._supports_logging = False
683
+ return
684
+
685
+ self._supports_tools = getattr(capabilities, "tools", None) is not None
686
+ self._supports_prompts = getattr(capabilities, "prompts", None) is not None
687
+ self._supports_logging = getattr(capabilities, "logging", None) is not None
688
+
689
+ async def _reconnect_without_loading(self) -> None:
690
+ if self._is_lifecycle_owner_task():
691
+ await self._connect_on_owner(reset=True, load_configured=False)
692
+ return
693
+
694
+ await self._run_on_lifecycle_owner("connect", reset=True, load_configured=False)
695
+
659
696
  async def connect(self, *, reset: bool = False) -> None:
660
697
  if self._is_lifecycle_owner_task():
661
698
  await self._connect_on_owner(reset=reset)
@@ -664,7 +701,7 @@ class MCPTool:
664
701
  async with self._lifecycle_request_lock:
665
702
  await self._run_on_lifecycle_owner("connect", reset=reset)
666
703
 
667
- async def _connect_on_owner(self, *, reset: bool = False) -> None:
704
+ async def _connect_on_owner(self, *, reset: bool = False, load_configured: bool = True) -> None:
668
705
  """Connect to the MCP server.
669
706
 
670
707
  Establishes a connection to the MCP server, initializes the session,
@@ -672,6 +709,7 @@ class MCPTool:
672
709
 
673
710
  Keyword Args:
674
711
  reset: If True, forces a reconnection even if already connected.
712
+ load_configured: If True, loads tools and prompts according to the constructor flags.
675
713
 
676
714
  Raises:
677
715
  ToolException: If connection or session initialization fails.
@@ -680,6 +718,7 @@ class MCPTool:
680
718
  await self._safe_close_exit_stack()
681
719
  self.session = None
682
720
  self.is_connected = False
721
+ self._reset_session_state()
683
722
  self._exit_stack = AsyncExitStack()
684
723
  if not self.session:
685
724
  try:
@@ -741,7 +780,8 @@ class MCPTool:
741
780
  inner_exception=ex if isinstance(ex, Exception) else None,
742
781
  ) from ex
743
782
  try:
744
- await session.initialize()
783
+ initialize_result = await session.initialize()
784
+ self._set_server_capabilities(getattr(initialize_result, "capabilities", None))
745
785
  except (Exception, asyncio.CancelledError) as ex:
746
786
  if await self._close_and_check_cancelled(ex):
747
787
  raise
@@ -759,17 +799,22 @@ class MCPTool:
759
799
  self.session = session
760
800
  elif self.session._request_id == 0: # type: ignore[attr-defined]
761
801
  # If the session is not initialized, we need to reinitialize it
762
- await self.session.initialize()
802
+ initialize_result = await self.session.initialize()
803
+ self._set_server_capabilities(getattr(initialize_result, "capabilities", None))
804
+ elif self._server_capabilities is None:
805
+ self._set_server_capabilities(getattr(self.session, "_server_capabilities", None))
763
806
  logger.debug("Connected to MCP server: %s", self.session)
764
807
  self.is_connected = True
765
- if self.load_tools_flag:
766
- await self.load_tools()
808
+ if load_configured and self.load_tools_flag:
809
+ if self._supports_tools:
810
+ await self.load_tools()
767
811
  self._tools_loaded = True
768
- if self.load_prompts_flag:
769
- await self.load_prompts()
812
+ if load_configured and self.load_prompts_flag:
813
+ if self._supports_prompts:
814
+ await self.load_prompts()
770
815
  self._prompts_loaded = True
771
816
 
772
- if logger.level != logging.NOTSET:
817
+ if logger.level != logging.NOTSET and self._supports_logging is not False:
773
818
  try:
774
819
  level_name = cast(
775
820
  Any, next(level for level, value in LOG_LEVEL_MAPPING.items() if value == logger.level)
@@ -973,17 +1018,49 @@ class MCPTool:
973
1018
  Raises:
974
1019
  ToolExecutionException: If the MCP server is not connected.
975
1020
  """
1021
+ from anyio import ClosedResourceError
976
1022
  from mcp import types
977
1023
 
1024
+ if not self._supports_prompts:
1025
+ logger.debug("Skipping MCP prompt loading because the server did not advertise prompts support.")
1026
+ return
1027
+
978
1028
  # Track existing function names to prevent duplicates
979
1029
  existing_names = {func.name for func in self._functions}
980
1030
 
981
1031
  params: types.PaginatedRequestParams | None = None
982
1032
  while True:
983
- # Ensure connection is still valid before each page request
984
- await self._ensure_connected()
1033
+ prompt_list: types.ListPromptsResult | None = None
1034
+ for attempt in range(2):
1035
+ try:
1036
+ # Ensure connection is still valid before each page request
1037
+ await self._ensure_connected()
1038
+ if not self._supports_prompts:
1039
+ logger.debug(
1040
+ "Skipping MCP prompt loading because the server did not advertise prompts support."
1041
+ )
1042
+ return
1043
+ prompt_list = await self.session.list_prompts(params=params) # type: ignore[union-attr]
1044
+ break
1045
+ except ClosedResourceError as cl_ex:
1046
+ if attempt == 0:
1047
+ logger.info("MCP connection closed unexpectedly while loading prompts. Reconnecting...")
1048
+ try:
1049
+ await self._reconnect_without_loading()
1050
+ except Exception as reconn_ex:
1051
+ raise ToolExecutionException(
1052
+ "Failed to reconnect to MCP server.",
1053
+ inner_exception=reconn_ex,
1054
+ ) from reconn_ex
1055
+ continue
1056
+ logger.error("MCP connection closed unexpectedly after reconnection: %s", cl_ex)
1057
+ raise ToolExecutionException(
1058
+ "Failed to load prompts - connection lost.",
1059
+ inner_exception=cl_ex,
1060
+ ) from cl_ex
985
1061
 
986
- prompt_list = await self.session.list_prompts(params=params) # type: ignore[union-attr]
1062
+ if prompt_list is None:
1063
+ raise ToolExecutionException("Failed to load prompts.")
987
1064
 
988
1065
  for prompt in prompt_list.prompts:
989
1066
  normalized_name = _normalize_mcp_name(prompt.name)
@@ -1010,7 +1087,7 @@ class MCPTool:
1010
1087
  existing_names.add(local_name)
1011
1088
 
1012
1089
  # Check if there are more pages
1013
- if not prompt_list or not prompt_list.nextCursor:
1090
+ if not prompt_list.nextCursor:
1014
1091
  break
1015
1092
  params = types.PaginatedRequestParams(cursor=prompt_list.nextCursor)
1016
1093
 
@@ -1023,18 +1100,48 @@ class MCPTool:
1023
1100
  Raises:
1024
1101
  ToolExecutionException: If the MCP server is not connected.
1025
1102
  """
1103
+ from anyio import ClosedResourceError
1026
1104
  from mcp import types
1027
1105
 
1106
+ if not self._supports_tools:
1107
+ logger.debug("Skipping MCP tool loading because the server did not advertise tools support.")
1108
+ return
1109
+
1028
1110
  # Track existing function names to prevent duplicates
1029
1111
  existing_names = {func.name for func in self._functions}
1030
1112
  self._tool_call_meta_by_name.clear()
1031
1113
 
1032
1114
  params: types.PaginatedRequestParams | None = None
1033
1115
  while True:
1034
- # Ensure connection is still valid before each page request
1035
- await self._ensure_connected()
1116
+ tool_list: types.ListToolsResult | None = None
1117
+ for attempt in range(2):
1118
+ try:
1119
+ # Ensure connection is still valid before each page request
1120
+ await self._ensure_connected()
1121
+ if not self._supports_tools:
1122
+ logger.debug("Skipping MCP tool loading because the server did not advertise tools support.")
1123
+ return
1124
+ tool_list = await self.session.list_tools(params=params) # type: ignore[union-attr]
1125
+ break
1126
+ except ClosedResourceError as cl_ex:
1127
+ if attempt == 0:
1128
+ logger.info("MCP connection closed unexpectedly while loading tools. Reconnecting...")
1129
+ try:
1130
+ await self._reconnect_without_loading()
1131
+ except Exception as reconn_ex:
1132
+ raise ToolExecutionException(
1133
+ "Failed to reconnect to MCP server.",
1134
+ inner_exception=reconn_ex,
1135
+ ) from reconn_ex
1136
+ continue
1137
+ logger.error("MCP connection closed unexpectedly after reconnection: %s", cl_ex)
1138
+ raise ToolExecutionException(
1139
+ "Failed to load tools - connection lost.",
1140
+ inner_exception=cl_ex,
1141
+ ) from cl_ex
1036
1142
 
1037
- tool_list = await self.session.list_tools(params=params) # type: ignore[union-attr]
1143
+ if tool_list is None:
1144
+ raise ToolExecutionException("Failed to load tools.")
1038
1145
 
1039
1146
  for tool in tool_list.tools:
1040
1147
  if tool.meta is not None:
@@ -1083,7 +1190,7 @@ class MCPTool:
1083
1190
  existing_names.add(local_name)
1084
1191
 
1085
1192
  # Check if there are more pages
1086
- if not tool_list or not tool_list.nextCursor:
1193
+ if not tool_list.nextCursor:
1087
1194
  break
1088
1195
  params = types.PaginatedRequestParams(cursor=tool_list.nextCursor)
1089
1196
 
@@ -1100,6 +1207,7 @@ class MCPTool:
1100
1207
  self._exit_stack = AsyncExitStack()
1101
1208
  self.session = None
1102
1209
  self.is_connected = False
1210
+ self._reset_session_state()
1103
1211
 
1104
1212
  async def close(self) -> None:
1105
1213
  """Disconnect from the MCP server.
@@ -1131,12 +1239,30 @@ class MCPTool:
1131
1239
  Raises:
1132
1240
  ToolExecutionException: If reconnection fails.
1133
1241
  """
1242
+ from mcp.shared.exceptions import McpError
1243
+
1244
+ if not self._ping_available:
1245
+ return
1246
+
1134
1247
  try:
1135
1248
  await self.session.send_ping() # type: ignore[union-attr]
1249
+ except McpError as mcp_exc:
1250
+ if mcp_exc.error.code == -32601:
1251
+ self._ping_available = False
1252
+ logger.debug("Skipping future MCP pings because the server does not support ping.")
1253
+ return
1254
+ logger.info("MCP connection invalid or closed. Reconnecting...")
1255
+ try:
1256
+ await self._reconnect_without_loading()
1257
+ except Exception as ex:
1258
+ raise ToolExecutionException(
1259
+ "Failed to establish MCP connection.",
1260
+ inner_exception=ex,
1261
+ ) from ex
1136
1262
  except Exception:
1137
1263
  logger.info("MCP connection invalid or closed. Reconnecting...")
1138
1264
  try:
1139
- await self.connect(reset=True)
1265
+ await self._reconnect_without_loading()
1140
1266
  except Exception as ex:
1141
1267
  raise ToolExecutionException(
1142
1268
  "Failed to establish MCP connection.",
@@ -651,9 +651,7 @@ def _validate_compatibility(compatibility: str | None) -> None:
651
651
  ValueError: If the value exceeds the maximum allowed length.
652
652
  """
653
653
  if compatibility is not None and len(compatibility) > MAX_COMPATIBILITY_LENGTH:
654
- raise ValueError(
655
- f"Skill compatibility must be {MAX_COMPATIBILITY_LENGTH} characters or fewer."
656
- )
654
+ raise ValueError(f"Skill compatibility must be {MAX_COMPATIBILITY_LENGTH} characters or fewer.")
657
655
 
658
656
 
659
657
  def _build_skill_content(
@@ -733,6 +731,7 @@ class InlineSkill(Skill):
733
731
  instructions="Use this skill for DB tasks.",
734
732
  )
735
733
 
734
+
736
735
  @skill.resource
737
736
  def get_schema() -> str:
738
737
  return "CREATE TABLE ..."
@@ -1513,6 +1512,97 @@ YAML_INDENTED_KV_RE = re.compile(
1513
1512
  # must not start or end with a hyphen, and must not contain consecutive hyphens.
1514
1513
  VALID_NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9]*-[a-z0-9])*[a-z0-9]*$")
1515
1514
 
1515
+ # Block scalar indicator characters recognised by the lightweight YAML parser.
1516
+ _BLOCK_SCALAR_INDICATORS = ("|", ">")
1517
+
1518
+
1519
+ def _parse_yaml_scalar_value(yaml_content: str, kv_match: re.Match[str]) -> str:
1520
+ """Resolve the scalar value for an unquoted YAML key-value match.
1521
+
1522
+ If the captured value starts with a YAML block scalar indicator (``|`` or
1523
+ ``>``), the function reads subsequent indented continuation lines, strips
1524
+ the common leading indentation, and joins them according to the scalar
1525
+ style (literal preserves newlines, folded replaces them with spaces).
1526
+
1527
+ Chomping indicators are respected per YAML 1.2 §8.1.1.2:
1528
+
1529
+ * ``-`` (strip) — final line break and trailing empty lines excluded
1530
+ * ``+`` (keep) — final line break and any trailing empty lines preserved
1531
+ * default (clip) — final line break preserved, trailing empty lines excluded
1532
+
1533
+ For plain (non-block-scalar) values the captured text is returned as-is.
1534
+ Note: explicit indentation indicators (e.g. ``|2``) are not supported;
1535
+ indentation is auto-detected from the common leading whitespace.
1536
+ """
1537
+ value: str = kv_match.group(3)
1538
+
1539
+ if not value or value[0] not in _BLOCK_SCALAR_INDICATORS:
1540
+ return value
1541
+
1542
+ scalar_style = value[0]
1543
+ keep_trailing_newline = len(value) > 1 and value[1] == "+"
1544
+ strip_trailing_newline = len(value) > 1 and value[1] == "-"
1545
+
1546
+ # Find the start of the next line after this key-value match.
1547
+ next_line_start = yaml_content.find("\n", kv_match.end())
1548
+ if next_line_start < 0:
1549
+ return value
1550
+ next_line_start += 1 # skip the newline character itself
1551
+
1552
+ # Collect indented continuation lines (or blank lines within the block).
1553
+ block_lines: list[str] = []
1554
+ pos = next_line_start
1555
+ while pos < len(yaml_content):
1556
+ line_end = yaml_content.find("\n", pos)
1557
+ if line_end < 0:
1558
+ line = yaml_content[pos:]
1559
+ line_end = len(yaml_content)
1560
+ else:
1561
+ line = yaml_content[pos:line_end]
1562
+
1563
+ if not line or line.isspace():
1564
+ # Blank / whitespace-only lines are part of the block.
1565
+ block_lines.append("")
1566
+ pos = line_end + 1 if line_end < len(yaml_content) else line_end
1567
+ continue
1568
+
1569
+ if line[0] not in (" ", "\t"):
1570
+ # Non-indented, non-blank line — end of the block.
1571
+ break
1572
+
1573
+ block_lines.append(line)
1574
+ pos = line_end + 1 if line_end < len(yaml_content) else line_end
1575
+
1576
+ # Strip trailing blank lines collected from the block.
1577
+ while block_lines and block_lines[-1] == "":
1578
+ block_lines.pop()
1579
+
1580
+ if not block_lines:
1581
+ return ""
1582
+
1583
+ # Determine the common leading indentation across non-empty lines.
1584
+ # Only space/tab characters count as indentation (matches YAML semantics).
1585
+ def _indent_width(s: str) -> int:
1586
+ i = 0
1587
+ while i < len(s) and s[i] in (" ", "\t"):
1588
+ i += 1
1589
+ return i
1590
+
1591
+ common_indent = min(_indent_width(line) for line in block_lines if line)
1592
+ normalized = [line[common_indent:] if line else "" for line in block_lines]
1593
+
1594
+ # Literal preserves newlines; folded joins non-empty lines with spaces.
1595
+ parsed = "\n".join(normalized) if scalar_style == "|" else " ".join(line for line in normalized if line)
1596
+
1597
+ if keep_trailing_newline:
1598
+ return parsed + "\n"
1599
+ if strip_trailing_newline:
1600
+ return parsed
1601
+ # Clip (default): literal gets a trailing newline, folded does not.
1602
+ if scalar_style == "|":
1603
+ return parsed + "\n"
1604
+ return parsed
1605
+
1516
1606
 
1517
1607
  # Default system prompt template for advertising available skills to the model.
1518
1608
  # Use {skills} as the placeholder for the generated skills XML list.
@@ -2522,11 +2612,7 @@ class FileSkillsSource(SkillsSource):
2522
2612
 
2523
2613
  # Reject absolute paths (check both POSIX and Windows-style roots
2524
2614
  # so validation is consistent regardless of the host OS)
2525
- if (
2526
- os.path.isabs(directory)
2527
- or normalized.startswith("/")
2528
- or re.match(r"^[A-Za-z]:[/\\]", directory)
2529
- ):
2615
+ if os.path.isabs(directory) or normalized.startswith("/") or re.match(r"^[A-Za-z]:[/\\]", directory):
2530
2616
  logger.warning(
2531
2617
  "Skipping directory '%s': absolute paths are not allowed.",
2532
2618
  directory,
@@ -2879,7 +2965,9 @@ class FileSkillsSource(SkillsSource):
2879
2965
 
2880
2966
  for kv_match in YAML_KV_RE.finditer(yaml_content):
2881
2967
  key = kv_match.group(1)
2882
- value = kv_match.group(2) if kv_match.group(2) is not None else kv_match.group(3)
2968
+ value = (
2969
+ kv_match.group(2) if kv_match.group(2) is not None else _parse_yaml_scalar_value(yaml_content, kv_match)
2970
+ )
2883
2971
 
2884
2972
  key_lower = key.lower()
2885
2973
  if key_lower == "name":
@@ -32,6 +32,7 @@ from .._types import (
32
32
  from ..exceptions import AgentInvalidRequestException, AgentInvalidResponseException
33
33
  from ._checkpoint import CheckpointStorage
34
34
  from ._events import (
35
+ AGENT_FORWARDED_EVENT_TYPES,
35
36
  WorkflowEvent,
36
37
  )
37
38
  from ._message_utils import normalize_messages_input
@@ -104,7 +105,7 @@ class WorkflowAgent(BaseAgent):
104
105
  Note:
105
106
  Only output events (type='output') and request_info events (type='request_info') from
106
107
  the workflow are considered and converted to agent responses of the WorkflowAgent.
107
- Other workflow events are ignored. Use `with_output_from` in WorkflowBuilder to control
108
+ Other workflow events are ignored. Use `output_from` in WorkflowBuilder to control
108
109
  which executors' outputs are surfaced as agent responses.
109
110
  """
110
111
  if id is None:
@@ -300,7 +301,7 @@ class WorkflowAgent(BaseAgent):
300
301
  function_invocation_kwargs=function_invocation_kwargs,
301
302
  client_kwargs=client_kwargs,
302
303
  ):
303
- if event.type == "output" or event.type == "request_info":
304
+ if event.type in AGENT_FORWARDED_EVENT_TYPES:
304
305
  output_events.append(event)
305
306
 
306
307
  result = self._convert_workflow_events_to_agent_response(response_id, output_events)
@@ -514,7 +515,11 @@ class WorkflowAgent(BaseAgent):
514
515
  response_id: str,
515
516
  output_events: list[WorkflowEvent[Any]],
516
517
  ) -> AgentResponse:
517
- """Convert a list of workflow output events to an AgentResponse."""
518
+ """Convert a list of workflow events to an AgentResponse.
519
+
520
+ Caller-facing workflow events are forwarded as agent messages. Terminal and
521
+ intermediate event payloads keep their original content types.
522
+ """
518
523
  messages: list[Message] = []
519
524
  raw_representations: list[object] = []
520
525
  merged_usage: UsageDetails | None = None
@@ -535,14 +540,19 @@ class WorkflowAgent(BaseAgent):
535
540
  raw_representations.append(output_event)
536
541
  else:
537
542
  data = output_event.data
543
+ # Anything that isn't `output` is intermediate — this branch only sees
544
+ # events that already passed the lifecycle filter and weren't request_info.
545
+ is_intermediate = output_event.type != "output"
538
546
 
539
547
  if isinstance(data, AgentResponseUpdate):
540
- # We cannot support AgentResponseUpdate in non-streaming mode. This is because the message
541
- # sequence cannot be guaranteed when there are streaming updates in between non-streaming
542
- # responses.
548
+ # AgentResponseUpdate is a streaming-only payload. Accepting it
549
+ # in non-streaming runs would make message ordering depend on
550
+ # partial chunks for both terminal and intermediate events.
551
+ event_label = "Intermediate" if is_intermediate else "Output"
543
552
  raise AgentInvalidRequestException(
544
- "Output event with AgentResponseUpdate data cannot be emitted in non-streaming mode. "
545
- "Please ensure executors emit AgentResponse for non-streaming workflows."
553
+ f"{event_label} event with AgentResponseUpdate data cannot be emitted "
554
+ "in non-streaming mode. Please ensure executors emit AgentResponse "
555
+ "for non-streaming workflows."
546
556
  )
547
557
 
548
558
  if isinstance(data, AgentResponse):
@@ -626,16 +636,21 @@ class WorkflowAgent(BaseAgent):
626
636
  ) -> list[AgentResponseUpdate]:
627
637
  """Convert a workflow event to a list of AgentResponseUpdate objects.
628
638
 
629
- Events with type='output' and type='request_info' are processed.
630
- Other workflow events are ignored as they are workflow-internal.
631
-
632
- For 'output' events, AgentExecutor yields AgentResponseUpdate for streaming updates
633
- via ctx.yield_output(). This method converts those to agent response updates.
639
+ Forwarding rule:
634
640
 
635
- Returns:
636
- A list of AgentResponseUpdate objects. Empty list if the event is not relevant.
641
+ - ``type='output'`` — terminal user-facing emission. Forwarded as-is.
642
+ - ``type='intermediate'`` (and the deprecated ``type='data'``) forwarded
643
+ as-is.
644
+ - ``type='request_info'`` — request-info translation (unchanged).
645
+ - Everything else (lifecycle, diagnostics, executor bookkeeping,
646
+ orchestration-internal events like ``group_chat``/``handoff_sent``/
647
+ ``magentic_orchestrator``) is dropped.
637
648
  """
638
- if event.type == "output":
649
+ # TODO(evmattso): https://github.com/microsoft/agent-framework/issues/5885
650
+ if event.type not in AGENT_FORWARDED_EVENT_TYPES:
651
+ return []
652
+
653
+ if event.type != "request_info":
639
654
  data = event.data
640
655
  executor_id = event.executor_id
641
656
 
@@ -123,7 +123,7 @@ class AgentExecutor(Executor):
123
123
  - run(stream=True): Emits incremental output events (type='output') as the agent produces tokens
124
124
  - run(): Emits a single output event (type='output') containing the complete response
125
125
 
126
- Use `with_output_from` in WorkflowBuilder to control whether the AgentResponse
126
+ Use `output_from` in WorkflowBuilder to control whether the AgentResponse
127
127
  or AgentResponseUpdate objects are yielded as workflow outputs.
128
128
 
129
129
  Messages sent to downstream executors will always be the complete AgentResponse. In
@@ -478,7 +478,7 @@ class AgentExecutor(Executor):
478
478
 
479
479
  # Prefer stream finalization when available so result hooks run
480
480
  # (e.g., thread conversation updates). Fall back to reconstructing from updates
481
- # for legacy/custom agents that return a plain async iterable.
481
+ # for compatibility/custom agents that return a plain async iterable.
482
482
  # TODO(evmattso): Integrate workflow agent run handling around ResponseStream so
483
483
  # AgentExecutor does not need this conditional stream-finalization branch.
484
484
  maybe_get_final_response = getattr(stream, "get_final_response", None)