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.
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/PKG-INFO +1 -1
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_feature_stage.py +2 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_mcp.py +150 -24
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_skills.py +97 -9
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent.py +31 -16
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent_executor.py +2 -2
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_edge_runner.py +34 -6
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_events.py +34 -18
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_functional.py +2 -1
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_runner_context.py +21 -1
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_validation.py +29 -3
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow.py +175 -27
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_builder.py +217 -13
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_context.py +28 -2
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_workflow_executor.py +17 -1
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/observability.py +177 -27
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/pyproject.toml +1 -1
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/LICENSE +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/README.md +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_agents.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_clients.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_compaction.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_docstrings.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_evaluation.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_harness/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_memory.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_mode.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_harness/_todo.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_middleware.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_serialization.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_sessions.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_settings.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_telemetry.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_tools.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_types.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent_utils.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_checkpoint.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_checkpoint_encoding.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_const.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_conversation_history.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_edge.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_executor.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_function_executor.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_message_utils.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_model_utils.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_request_info_mixin.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_runner.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_state.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_typing_utils.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_viz.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/a2a/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/a2a/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/ag_ui/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/ag_ui/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/amazon/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/amazon/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/anthropic/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/anthropic/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/azure/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/azure/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/chatkit/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/chatkit/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/declarative/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/declarative/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/devui/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/devui/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/exceptions.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/foundry/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/foundry/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/github/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/github/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/google/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/google/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/hyperlight/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/lab/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/mem0/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/mem0/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/microsoft/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/microsoft/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/ollama/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/ollama/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/openai/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/openai/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/orchestrations/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/orchestrations/__init__.pyi +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/py.typed +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/redis/__init__.py +0 -0
- {agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/redis/__init__.pyi +0 -0
- {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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
984
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1035
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
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":
|
{agent_framework_core-1.4.0 → agent_framework_core-1.6.0}/agent_framework/_workflows/_agent.py
RENAMED
|
@@ -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 `
|
|
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
|
|
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
|
|
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
|
-
#
|
|
541
|
-
#
|
|
542
|
-
#
|
|
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
|
-
"
|
|
545
|
-
"Please ensure executors emit AgentResponse
|
|
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
|
-
|
|
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
|
-
|
|
636
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
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)
|