agent-framework-declarative 1.0.4__tar.gz → 1.1.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 (26) hide show
  1. agent_framework_declarative-1.1.0/PKG-INFO +90 -0
  2. agent_framework_declarative-1.1.0/README.md +63 -0
  3. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_loader.py +6 -4
  4. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_declarative_base.py +116 -86
  5. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_executors_agents.py +17 -5
  6. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_executors_http.py +5 -33
  7. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_factory.py +1 -1
  8. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_http_handler.py +13 -4
  9. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_mcp_handler.py +142 -51
  10. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/pyproject.toml +3 -3
  11. agent_framework_declarative-1.0.4/PKG-INFO +0 -49
  12. agent_framework_declarative-1.0.4/README.md +0 -22
  13. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/LICENSE +0 -0
  14. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/__init__.py +0 -0
  15. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_feature_usage.py +0 -0
  16. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_models.py +0 -0
  17. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/__init__.py +0 -0
  18. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_declarative_builder.py +0 -0
  19. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_errors.py +0 -0
  20. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_executors_basic.py +0 -0
  21. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_executors_control_flow.py +0 -0
  22. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_executors_external_input.py +0 -0
  23. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_executors_mcp.py +0 -0
  24. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_executors_tools.py +0 -0
  25. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_powerfx_functions.py +0 -0
  26. {agent_framework_declarative-1.0.4 → agent_framework_declarative-1.1.0}/agent_framework_declarative/_workflows/_state.py +0 -0
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-framework-declarative
3
+ Version: 1.1.0
4
+ Summary: Declarative specification support for Microsoft Agent Framework.
5
+ Author-email: Microsoft <af-support@microsoft.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Typing :: Typed
17
+ License-File: LICENSE
18
+ Requires-Dist: agent-framework-core>=1.19.0,<2
19
+ Requires-Dist: httpx>=0.27,<1
20
+ Requires-Dist: powerfx>=0.0.32,<0.0.35; python_version < '3.14'
21
+ Requires-Dist: pyyaml>=6.0,<7.0
22
+ Project-URL: homepage, https://aka.ms/agent-framework
23
+ Project-URL: issues, https://github.com/microsoft/agent-framework/issues
24
+ Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
25
+ Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
26
+
27
+ # Get Started with Microsoft Agent Framework Declarative
28
+
29
+ Please install this package via pip:
30
+
31
+ ```bash
32
+ pip install agent-framework-declarative
33
+ ```
34
+
35
+ ## Release stage
36
+
37
+ This package ships at two different stability levels:
38
+
39
+ - **Declarative workflows** (`WorkflowFactory`, executors, handlers, and the
40
+ `_workflows` surface) are **stable**.
41
+ - **Declarative agents** (`AgentFactory` and the YAML agent loading/parsing path:
42
+ `DeclarativeLoaderError`, `ProviderLookupError`, `ProviderTypeMapping`) are
43
+ **experimental** and may change or be removed in future versions without notice.
44
+ Using any of these symbols emits an `ExperimentalWarning` on first use.
45
+
46
+ ## Declarative features
47
+
48
+ The declarative packages provides support for building agents based on a declarative yaml specification.
49
+
50
+ ## HTTP request client ownership and cookies
51
+
52
+ **Breaking change:** The HTTP client created by `DefaultHttpRequestHandler` no longer
53
+ persists response cookies. The handler still creates its client lazily, reuses it across
54
+ requests and workflows, and closes it on `aclose()` or async context-manager exit.
55
+ Timeout and redirect defaults are unchanged.
56
+
57
+ Applications requiring cookies for authentication, session continuity, or load-balancer
58
+ affinity must supply an `httpx.AsyncClient` through `client=` or `client_provider=`.
59
+ Supplied and provider-returned clients retain their configuration and cookie behavior
60
+ and must be closed by the caller. Scope cookie-bearing clients to one authenticated
61
+ principal; sharing a handler across workflows does not partition a client's cookies.
62
+ A provider returning `None` falls back to `client=`, if supplied, then to the internally
63
+ owned client with the default cookie policy.
64
+
65
+ Explicit `Cookie` request headers remain supported. Response `Set-Cookie` headers
66
+ remain available in `HttpRequestResult.headers`; disabling persistence does not redact
67
+ the response.
68
+
69
+ ## MCP handler session lifetime
70
+
71
+ `DefaultMCPToolHandler()` reuses MCP sessions through a bounded LRU cache.
72
+ When configured with `client_provider`, it instead invokes the provider and
73
+ creates a fresh MCP tool/session for every invocation, including `tools/list`.
74
+ Sessions are never reused in this mode, even if the provider returns `None`
75
+ or the same `httpx.AsyncClient` instance.
76
+
77
+ Each invocation closes its tool/session on success, failure, or cancellation.
78
+ Internally created fallback HTTP clients are also closed; caller-supplied HTTP
79
+ clients remain caller-owned and are never closed by the handler. Calling
80
+ `aclose()` rejects new invocations and waits for active provider-backed
81
+ invocations to finish cleaning up. Calling it from an active invocation's
82
+ context (including provider callbacks, inherited child tasks, and cleanup)
83
+ raises `RuntimeError` before changing handler state, rather than waiting on
84
+ itself. Close the handler outside that context or after the invocation completes.
85
+
86
+ This intentionally incurs connection and initialization overhead and does not
87
+ retain server session state between provider-backed invocations. Applications
88
+ that require shared session ownership must implement an explicitly scoped
89
+ custom `MCPToolHandler`; there is no provider-backed session-cache opt-in.
90
+
@@ -0,0 +1,63 @@
1
+ # Get Started with Microsoft Agent Framework Declarative
2
+
3
+ Please install this package via pip:
4
+
5
+ ```bash
6
+ pip install agent-framework-declarative
7
+ ```
8
+
9
+ ## Release stage
10
+
11
+ This package ships at two different stability levels:
12
+
13
+ - **Declarative workflows** (`WorkflowFactory`, executors, handlers, and the
14
+ `_workflows` surface) are **stable**.
15
+ - **Declarative agents** (`AgentFactory` and the YAML agent loading/parsing path:
16
+ `DeclarativeLoaderError`, `ProviderLookupError`, `ProviderTypeMapping`) are
17
+ **experimental** and may change or be removed in future versions without notice.
18
+ Using any of these symbols emits an `ExperimentalWarning` on first use.
19
+
20
+ ## Declarative features
21
+
22
+ The declarative packages provides support for building agents based on a declarative yaml specification.
23
+
24
+ ## HTTP request client ownership and cookies
25
+
26
+ **Breaking change:** The HTTP client created by `DefaultHttpRequestHandler` no longer
27
+ persists response cookies. The handler still creates its client lazily, reuses it across
28
+ requests and workflows, and closes it on `aclose()` or async context-manager exit.
29
+ Timeout and redirect defaults are unchanged.
30
+
31
+ Applications requiring cookies for authentication, session continuity, or load-balancer
32
+ affinity must supply an `httpx.AsyncClient` through `client=` or `client_provider=`.
33
+ Supplied and provider-returned clients retain their configuration and cookie behavior
34
+ and must be closed by the caller. Scope cookie-bearing clients to one authenticated
35
+ principal; sharing a handler across workflows does not partition a client's cookies.
36
+ A provider returning `None` falls back to `client=`, if supplied, then to the internally
37
+ owned client with the default cookie policy.
38
+
39
+ Explicit `Cookie` request headers remain supported. Response `Set-Cookie` headers
40
+ remain available in `HttpRequestResult.headers`; disabling persistence does not redact
41
+ the response.
42
+
43
+ ## MCP handler session lifetime
44
+
45
+ `DefaultMCPToolHandler()` reuses MCP sessions through a bounded LRU cache.
46
+ When configured with `client_provider`, it instead invokes the provider and
47
+ creates a fresh MCP tool/session for every invocation, including `tools/list`.
48
+ Sessions are never reused in this mode, even if the provider returns `None`
49
+ or the same `httpx.AsyncClient` instance.
50
+
51
+ Each invocation closes its tool/session on success, failure, or cancellation.
52
+ Internally created fallback HTTP clients are also closed; caller-supplied HTTP
53
+ clients remain caller-owned and are never closed by the handler. Calling
54
+ `aclose()` rejects new invocations and waits for active provider-backed
55
+ invocations to finish cleaning up. Calling it from an active invocation's
56
+ context (including provider callbacks, inherited child tasks, and cleanup)
57
+ raises `RuntimeError` before changing handler state, rather than waiting on
58
+ itself. Close the handler outside that context or after the invocation completes.
59
+
60
+ This intentionally incurs connection and initialization overhead and does not
61
+ retain server session state between provider-backed invocations. Applications
62
+ that require shared session ownership must implement an explicitly scoped
63
+ custom `MCPToolHandler`; there is no provider-backed session-cache opt-in.
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import sys
6
7
  from collections.abc import Callable, Mapping
7
8
  from pathlib import Path
@@ -338,7 +339,7 @@ class AgentFactory:
338
339
  yaml_path = Path(yaml_path)
339
340
  if not yaml_path.exists():
340
341
  raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}")
341
- with open(yaml_path) as f:
342
+ with open(yaml_path, encoding="utf-8") as f:
342
343
  yaml_str = f.read()
343
344
  return self.create_agent_from_yaml(yaml_str)
344
345
 
@@ -508,9 +509,10 @@ class AgentFactory:
508
509
  """
509
510
  if not isinstance(yaml_path, Path):
510
511
  yaml_path = Path(yaml_path)
511
- if not yaml_path.exists():
512
- raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}")
513
- yaml_str = yaml_path.read_text()
512
+ try:
513
+ yaml_str = await asyncio.to_thread(yaml_path.read_text, encoding="utf-8")
514
+ except FileNotFoundError as exc:
515
+ raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}") from exc
514
516
  return await self.create_agent_from_yaml_async(yaml_str)
515
517
 
516
518
  async def create_agent_from_yaml_async(self, yaml_str: str) -> Agent:
@@ -31,7 +31,7 @@ import os
31
31
  import re
32
32
  import sys
33
33
  import uuid
34
- from collections.abc import Mapping
34
+ from collections.abc import Iterator, Mapping
35
35
  from dataclasses import dataclass, field
36
36
  from decimal import Decimal as _Decimal
37
37
  from enum import Enum
@@ -67,6 +67,73 @@ _ENV_REFERENCE_RE = re.compile(r"\bEnv\.([A-Za-z_][A-Za-z0-9_]*)")
67
67
  _SAFE_PATH_SEGMENT_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*$")
68
68
 
69
69
 
70
+ def _skip_powerfx_opaque_token(formula: str, start: int) -> int:
71
+ """Return the end of an ordinary quoted token or comment, or start if neither."""
72
+ quote = formula[start]
73
+ if quote in ('"', "'"):
74
+ pos = start + 1
75
+ while pos < len(formula):
76
+ if formula[pos] == quote:
77
+ if pos + 1 < len(formula) and formula[pos + 1] == quote:
78
+ pos += 2
79
+ continue
80
+ return pos + 1
81
+ pos += 1
82
+ return pos
83
+ if formula.startswith("//", start):
84
+ pos = start + 2
85
+ while pos < len(formula) and formula[pos] not in "\r\n":
86
+ pos += 1
87
+ return pos
88
+ if formula.startswith("/*", start):
89
+ end = formula.find("*/", start + 2)
90
+ return len(formula) if end == -1 else end + 2
91
+ return start
92
+
93
+
94
+ def _iter_powerfx_expression_indices(formula: str) -> Iterator[int]:
95
+ """Yield code positions, excluding literals/comments but including interpolation expressions."""
96
+ # None denotes interpolated text; integers track record braces in expression sections.
97
+ scopes: list[int | None] = [0]
98
+ cursor = 0
99
+ while cursor < len(formula):
100
+ depth = scopes[-1]
101
+ char = formula[cursor]
102
+ if depth is None:
103
+ if formula.startswith(('""', "{{", "}}"), cursor):
104
+ cursor += 2
105
+ continue
106
+ if char == "{":
107
+ scopes.append(0)
108
+ elif char == '"':
109
+ scopes.pop()
110
+ cursor += 1
111
+ continue
112
+
113
+ if formula.startswith('$"', cursor):
114
+ scopes.append(None)
115
+ cursor += 2
116
+ continue
117
+
118
+ token_end = _skip_powerfx_opaque_token(formula, cursor)
119
+ if token_end != cursor:
120
+ cursor = token_end
121
+ continue
122
+
123
+ if char == "{":
124
+ scopes[-1] = depth + 1
125
+ elif char == "}":
126
+ if depth > 0:
127
+ scopes[-1] = depth - 1
128
+ elif len(scopes) > 1:
129
+ scopes.pop()
130
+ cursor += 1
131
+ continue
132
+
133
+ yield cursor
134
+ cursor += 1
135
+
136
+
70
137
  @dataclass(frozen=True)
71
138
  class DeclarativeEnvConfig:
72
139
  """Configuration that populates the PowerFx ``Env`` symbol for a workflow.
@@ -667,11 +734,11 @@ class DeclarativeWorkflowState:
667
734
  When they appear nested inside other functions (e.g., Upper(MessageText(...))),
668
735
  we need to evaluate them first and replace with the result.
669
736
 
670
- For long strings (>500 chars), the result is stored in a temporary state variable
671
- to avoid exceeding PowerFx's 1000 character expression limit. This is a limitation
672
- of the Python PowerFx wrapper (powerfx package), which doesn't expose the
673
- MaximumExpressionLength configuration that the .NET PowerFxConfig provides.
674
- The .NET implementation defaults to 10,000 characters, while Python defaults to 1,000.
737
+ Results are stored in temporary state variables so untrusted message text is
738
+ passed to PowerFx as data rather than inserted into formula source.
739
+ Temporary names avoid existing Local keys and references in the original formula.
740
+ Literal text, quoted identifiers, and comments are left untouched; expression
741
+ sections inside PowerFx interpolated strings are preprocessed as code.
675
742
 
676
743
  Args:
677
744
  formula: The PowerFx formula to pre-process
@@ -684,90 +751,53 @@ class DeclarativeWorkflowState:
684
751
  Returns:
685
752
  The rewritten formula.
686
753
  """
687
- import re
688
-
689
- # Threshold for storing in state vs embedding as literal.
690
- # The Python PowerFx wrapper defaults to a 1000 char expression limit (vs 10,000 in .NET).
691
- # We use 500 to leave room for the rest of the expression around the replaced value.
692
- MAX_INLINE_LENGTH = 500
693
-
694
754
  temp_var_counter = 0
695
-
696
- # Custom functions that need pre-processing: (regex pattern, handler)
697
- custom_functions = [
698
- (r"MessageText\(", self._eval_and_replace_message_text),
699
- ]
700
-
701
- for pattern, handler in custom_functions:
702
- # Find all occurrences of the custom function
703
- while True:
704
- match = re.search(pattern, formula)
705
- if not match:
755
+ reserved_names = {name.casefold() for name in self.get_state_data().get("Local", {})}
756
+ # Reserve formula references too, so previously undefined names stay undefined.
757
+ folded_formula = formula.casefold()
758
+ function_name = "MessageText"
759
+ call_prefix = f"{function_name}("
760
+ result: list[str] = []
761
+ copied_until = 0
762
+ positions = _iter_powerfx_expression_indices(formula)
763
+
764
+ for cursor in positions:
765
+ if not formula.startswith(call_prefix, cursor):
766
+ continue
767
+
768
+ paren_start = cursor + len(function_name)
769
+ depth = 1
770
+ for pos in positions:
771
+ if pos <= paren_start:
772
+ continue
773
+ char = formula[pos]
774
+ if char == "(":
775
+ depth += 1
776
+ elif char == ")":
777
+ depth -= 1
778
+ if depth == 0:
706
779
  break
780
+ else:
781
+ break
707
782
 
708
- # Find the matching closing parenthesis
709
- start = match.start()
710
- paren_start = match.end() - 1 # Position of opening (
711
- depth = 1
712
- pos = paren_start + 1
713
- in_string = False
714
- escape_next = False
715
-
716
- while pos < len(formula) and depth > 0:
717
- char = formula[pos]
718
- if escape_next:
719
- escape_next = False
720
- pos += 1
721
- continue
722
- if char == "\\":
723
- escape_next = True
724
- pos += 1
725
- continue
726
- if char == '"' and not escape_next:
727
- in_string = not in_string
728
- elif not in_string:
729
- if char == "(":
730
- depth += 1
731
- elif char == ")":
732
- depth -= 1
733
- pos += 1
734
-
735
- if depth != 0:
736
- # Malformed expression, skip
783
+ inner_expr = formula[paren_start + 1 : pos]
784
+ replacement = self._eval_and_replace_message_text(inner_expr)
785
+ while True:
786
+ temp_var_name = f"_TempMessageText{temp_var_counter}"
787
+ temp_var_counter += 1
788
+ folded_name = temp_var_name.casefold()
789
+ if folded_name not in reserved_names and folded_name not in folded_formula:
737
790
  break
738
-
739
- # Extract the inner expression (between parentheses)
740
- end = pos
741
- inner_expr = formula[paren_start + 1 : end - 1]
742
-
743
- # Evaluate and get replacement
744
- replacement = handler(inner_expr)
745
-
746
- # Replace in formula
747
- if isinstance(replacement, str):
748
- if len(replacement) > MAX_INLINE_LENGTH:
749
- # Store long results in an underscore-prefixed temp key;
750
- # record the prior value so eval() can restore it.
751
- temp_var_name = f"_TempMessageText{temp_var_counter}"
752
- temp_var_counter += 1
753
- temp_var_path = f"Local.{temp_var_name}"
754
- temp_writes.append((temp_var_path, self.get(temp_var_path, default=self._MISSING)))
755
- self.set(temp_var_path, replacement)
756
- replacement_str = temp_var_path
757
- logger.debug(
758
- f"Stored long MessageText result ({len(replacement)} chars) "
759
- f"in temp variable {temp_var_name}"
760
- )
761
- else:
762
- # Short strings can be embedded directly
763
- escaped = replacement.replace('"', '""')
764
- replacement_str = f'"{escaped}"'
765
- else:
766
- replacement_str = str(replacement) if replacement is not None else '""'
767
-
768
- formula = formula[:start] + replacement_str + formula[end:]
769
-
770
- return formula
791
+ temp_var_path = f"Local.{temp_var_name}"
792
+ temp_writes.append((temp_var_path, self.get(temp_var_path, default=self._MISSING)))
793
+ self.set(temp_var_path, replacement)
794
+ result.append(formula[copied_until:cursor])
795
+ result.append(temp_var_path)
796
+ logger.debug(f"Stored MessageText result ({len(replacement)} chars) in temp variable {temp_var_name}")
797
+ copied_until = pos + 1
798
+
799
+ result.append(formula[copied_until:])
800
+ return "".join(result)
771
801
 
772
802
  def _eval_and_replace_message_text(self, inner_expr: str) -> str:
773
803
  """Evaluate MessageText() and return the text result.
@@ -788,16 +788,28 @@ class InvokeAzureAgentExecutor(DeclarativeActionExecutor):
788
788
  _validate_conversation_history(messages_for_agent, agent_name)
789
789
 
790
790
  # Retrieve kwargs passed to workflow.run() so they propagate to agent tools
791
- from agent_framework._workflows._const import WORKFLOW_RUN_KWARGS_KEY
792
-
793
- run_kwargs: dict[str, Any] = ctx.get_state(WORKFLOW_RUN_KWARGS_KEY, {})
791
+ from agent_framework._workflows import _agent_utils as workflow_agent_utils
792
+ from agent_framework._workflows import _const as workflow_const
793
+
794
+ run_kwargs: dict[str, Any] = ctx.get_state(workflow_const.WORKFLOW_RUN_KWARGS_KEY, {})
795
+ prepare_run_kwargs = getattr(workflow_agent_utils, "prepare_executor_run_kwargs", None)
796
+ resolved_state_key = getattr(workflow_const, "RESOLVED_WORKFLOW_RUN_KWARGS_KEY", None)
797
+ if callable(prepare_run_kwargs) and isinstance(resolved_state_key, str):
798
+ missing_resolved_state = object()
799
+ resolved_run_kwargs: Any = ctx.get_state(resolved_state_key, missing_resolved_state)
800
+ if resolved_run_kwargs is not missing_resolved_state:
801
+ if not isinstance(resolved_run_kwargs, dict):
802
+ raise TypeError("Resolved workflow run kwargs state must be a dict.")
803
+ run_kwargs = cast(Any, prepare_run_kwargs)(self.id, run_kwargs, resolved_run_kwargs)
794
804
  options: dict[str, Any] | None = None
795
805
  if run_kwargs:
796
806
  # Merge caller-provided options to avoid duplicate keyword argument
797
807
  options = dict(run_kwargs.get("options") or {})
798
808
  options["additional_function_arguments"] = run_kwargs
799
- # Exclude 'options' from splat to avoid TypeError on duplicate keyword
800
- run_kwargs = {k: v for k, v in run_kwargs.items() if k != "options"}
809
+ # Exclude 'options' from splat to avoid TypeError on duplicate keyword,
810
+ # and keep internal workflow-routing copies (stored under underscore
811
+ # keys for nested executors) out of the public Agent.run signature
812
+ run_kwargs = {k: v for k, v in run_kwargs.items() if k != "options" and not k.startswith("_")}
801
813
 
802
814
  # Use run() method to get properly structured messages (including tool calls and results)
803
815
  # This is critical for multi-turn conversations where tool calls must be followed
@@ -6,9 +6,8 @@ Mirrors the .NET ``HttpRequestExecutor``: dispatches an HTTP request through the
6
6
  configured :class:`HttpRequestHandler`, parses the response body, and assigns
7
7
  the parsed body and response headers to the declared state paths.
8
8
 
9
- Security note: response bodies can echo secrets and may be very large. Diagnostic
10
- messages produced for non-2xx responses truncate the body to 256 characters and
11
- collapse CR/LF/TAB to spaces (parity with .NET ``FormatBodyForDiagnostics``).
9
+ Response bodies are excluded from non-2xx exceptions because they may contain
10
+ private backend data. Request URLs and status codes remain available for diagnostics.
12
11
  """
13
12
 
14
13
  from __future__ import annotations
@@ -40,10 +39,6 @@ __all__ = [
40
39
 
41
40
  logger = logging.getLogger(__name__)
42
41
 
43
- _MAX_BODY_DIAGNOSTIC_LENGTH = 256
44
- _BODY_TRUNCATION_SUFFIX = " \u2026 [truncated]"
45
-
46
-
47
42
  # Body discriminator aliases. Long forms match the .NET object-model type
48
43
  # names so YAML produced by .NET round-trips. Short forms are the .NET YAML
49
44
  # convention used in test fixtures.
@@ -69,24 +64,6 @@ def _get_path(action_def: Mapping[str, Any], key: str) -> str | None:
69
64
  return None
70
65
 
71
66
 
72
- def _format_body_for_diagnostics(body: str | None) -> str:
73
- """Truncate and sanitise a response body for inclusion in error messages.
74
-
75
- Mirrors the .NET ``FormatBodyForDiagnostics`` helper:
76
-
77
- - Empty/None -> empty string.
78
- - Replaces CR/LF/TAB with spaces.
79
- - Truncates to 256 chars with a unicode-ellipsis ``[truncated]`` suffix.
80
- """
81
- if not body:
82
- return ""
83
-
84
- truncated = len(body) > _MAX_BODY_DIAGNOSTIC_LENGTH
85
- head = body[:_MAX_BODY_DIAGNOSTIC_LENGTH] if truncated else body
86
- sanitized = head.replace("\r", " ").replace("\n", " ").replace("\t", " ")
87
- return sanitized + _BODY_TRUNCATION_SUFFIX if truncated else sanitized
88
-
89
-
90
67
  def _parse_response_body(body: str | None) -> Any:
91
68
  """Parse an HTTP response body the same way the .NET executor does.
92
69
 
@@ -148,8 +125,8 @@ class HttpRequestActionExecutor(DeclarativeActionExecutor):
148
125
  an Assistant :class:`agent_framework.Message` to
149
126
  ``System.conversations.{id}.messages``.
150
127
  - On non-2xx, still publishes ``responseHeaders`` (diagnostic) and raises
151
- :class:`DeclarativeActionError` with a status-coded message containing a
152
- truncated/sanitised body preview.
128
+ :class:`DeclarativeActionError` with the request URL and status code,
129
+ without including the response body.
153
130
 
154
131
  Transport errors (``httpx.TimeoutException``, ``TimeoutError``,
155
132
  ``httpx.HTTPError``) become :class:`DeclarativeActionError`. ``CancelledError``
@@ -229,12 +206,7 @@ class HttpRequestActionExecutor(DeclarativeActionExecutor):
229
206
 
230
207
  # Non-success path: still publish headers diagnostically, then raise.
231
208
  self._assign_response_headers(state, result)
232
- body_preview = _format_body_for_diagnostics(result.body)
233
- if body_preview:
234
- message = f"HTTP request to '{url}' failed with status code {result.status_code}. Body: '{body_preview}'"
235
- else:
236
- message = f"HTTP request to '{url}' failed with status code {result.status_code}."
237
- raise DeclarativeActionError(message)
209
+ raise DeclarativeActionError(f"HTTP request to '{url}' failed with status code {result.status_code}.")
238
210
 
239
211
  # ----- Field resolution ----------------------------------------------------
240
212
 
@@ -241,7 +241,7 @@ class WorkflowFactory:
241
241
  if not yaml_path.exists():
242
242
  raise FileNotFoundError(f"Workflow YAML file not found: {yaml_path}")
243
243
 
244
- with open(yaml_path) as f:
244
+ with open(yaml_path, encoding="utf-8") as f:
245
245
  yaml_content = f.read()
246
246
 
247
247
  return self.create_workflow_from_yaml(yaml_content, base_path=yaml_path.parent)
@@ -23,6 +23,7 @@ from __future__ import annotations
23
23
  import asyncio
24
24
  from collections.abc import Awaitable, Callable, Mapping
25
25
  from dataclasses import dataclass, field
26
+ from http.cookiejar import CookieJar, DefaultCookiePolicy
26
27
  from typing import Any, Protocol, runtime_checkable
27
28
  from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit
28
29
 
@@ -122,12 +123,18 @@ class DefaultHttpRequestHandler:
122
123
  Construction modes:
123
124
 
124
125
  1. ``DefaultHttpRequestHandler()`` — owns an internal client created lazily
125
- on first ``send()``. Closed by :meth:`aclose`.
126
+ on first ``send()`` without response-cookie persistence. Closed by :meth:`aclose`.
126
127
  2. ``DefaultHttpRequestHandler(client=existing)`` — caller-owned client.
127
- Not closed by :meth:`aclose`.
128
+ Retains its cookie behavior and is not closed by :meth:`aclose`.
128
129
  3. ``DefaultHttpRequestHandler(client_provider=cb)`` — per-request client
129
130
  lookup (parity with .NET's ``httpClientProvider`` callback). The
130
- provider may return ``None`` to fall back to the owned/default client.
131
+ provider's clients retain their cookie behavior and are not closed by :meth:`aclose`.
132
+ Returning ``None`` falls back to ``client``, if supplied, then to the owned client.
133
+
134
+ Applications requiring persistent cookies must supply a client through ``client``
135
+ or ``client_provider`` scoped to one authenticated principal and manage its lifetime.
136
+ Explicit outbound ``Cookie`` headers and response ``Set-Cookie`` headers are preserved;
137
+ the owned-client policy only prevents automatic cookie persistence.
131
138
 
132
139
  .. warning::
133
140
 
@@ -262,7 +269,9 @@ class DefaultHttpRequestHandler:
262
269
  # one of them.
263
270
  async with self._owned_client_lock:
264
271
  if self._owned_client is None:
265
- self._owned_client = httpx.AsyncClient()
272
+ self._owned_client = httpx.AsyncClient(
273
+ cookies=CookieJar(policy=DefaultCookiePolicy(allowed_domains=[])),
274
+ )
266
275
  return self._owned_client
267
276
 
268
277
  async def __aenter__(self) -> DefaultHttpRequestHandler:
@@ -32,6 +32,7 @@ import json
32
32
  import logging
33
33
  from collections import OrderedDict
34
34
  from collections.abc import Awaitable, Callable
35
+ from contextvars import ContextVar, Token
35
36
  from dataclasses import dataclass, field
36
37
  from typing import TYPE_CHECKING, Any, ClassVar, Protocol, cast, runtime_checkable
37
38
 
@@ -157,14 +158,14 @@ class _CacheEntry:
157
158
  class DefaultMCPToolHandler:
158
159
  """Default :class:`MCPToolHandler` backed by :class:`agent_framework.MCPStreamableHTTPTool`.
159
160
 
160
- Caches one :class:`agent_framework.MCPStreamableHTTPTool` instance per
161
- ``(server_url, server_label, connection_name, headers_hash)`` in a
162
- bounded LRU. The cache prevents re-establishing an MCP session for every
161
+ Without a ``client_provider``, caches one
162
+ :class:`agent_framework.MCPStreamableHTTPTool` instance per
163
+ ``(server_url, server_label, connection_name, headers_hash)`` in a bounded
164
+ LRU. The cache prevents re-establishing an MCP session for every
163
165
  invocation while ensuring different header sets (auth tokens) cannot
164
166
  share a session — matches the .NET design intent while bounding
165
- cardinality. ``server_label`` and ``connection_name`` participate in
166
- the key so that callers using ``client_provider`` to dispatch on those
167
- fields receive a fresh client per logical connection (see below).
167
+ cardinality. ``server_label`` and ``connection_name`` also participate
168
+ in the key to distinguish logical connections.
168
169
  Header *names* are lower-cased inside the hash payload only — the
169
170
  headers passed on the wire keep the caller's original casing — so two
170
171
  YAML actions that spell ``Authorization`` differently still share a
@@ -174,12 +175,20 @@ class DefaultMCPToolHandler:
174
175
 
175
176
  1. ``DefaultMCPToolHandler()`` — owns its own ``httpx.AsyncClient``
176
177
  instances created lazily per cache entry. Closed by :meth:`aclose`.
177
- 2. ``DefaultMCPToolHandler(client_provider=cb)`` — per-server client
178
+ 2. ``DefaultMCPToolHandler(client_provider=cb)`` — per-invocation client
178
179
  lookup (parity with .NET ``httpClientProvider`` callback). The
179
180
  callback receives the full :class:`MCPToolInvocation` so it can
180
181
  dispatch on ``server_url`` / ``connection_name`` / ``server_label``.
181
- Returning ``None`` falls back to an internally-created client. Caller
182
- supplied clients are NOT closed by :meth:`aclose`.
182
+ The callback is invoked for every call, including ``tools/list``.
183
+ Each call creates and closes its own MCP tool/session, even when the
184
+ callback returns ``None`` or the same client object. Returning ``None``
185
+ falls back to an internally-created client, closed with the invocation.
186
+ Caller-supplied clients are never closed by this handler.
187
+
188
+ This intentionally trades connection/session reuse for invocation
189
+ isolation. Server session state is not retained across invocations;
190
+ callers needing shared session ownership must implement an explicitly
191
+ scoped custom :class:`MCPToolHandler`.
183
192
 
184
193
  .. warning::
185
194
 
@@ -187,11 +196,11 @@ class DefaultMCPToolHandler:
187
196
  or replace it with a custom handler in production deployments.
188
197
 
189
198
  Args:
190
- client_provider: Optional per-server ``httpx.AsyncClient`` provider.
191
- cache_max_size: Maximum number of cached MCP clients. When exceeded,
192
- the least-recently-used entry is evicted and its client closed
193
- (only owned clients are closed; caller-supplied ones are not).
194
- Defaults to ``32``.
199
+ client_provider: Optional per-invocation ``httpx.AsyncClient`` provider.
200
+ cache_max_size: Maximum number of cached MCP clients in no-provider mode.
201
+ When exceeded, the least-recently-used entry is evicted and its
202
+ owned client closed. Defaults to ``32``. Does not enable session
203
+ caching when a provider is configured.
195
204
  """
196
205
 
197
206
  LIST_TOOLS_TOOL_NAME: ClassVar[str] = "tools/list"
@@ -227,12 +236,19 @@ class DefaultMCPToolHandler:
227
236
  # tasks awaiting the same key will await the same future and share
228
237
  # the resulting cache entry.
229
238
  self._inflight: dict[tuple[str, str, str, str], asyncio.Future[_CacheEntry]] = {}
239
+ # Completion signals only: provider-backed calls never share entries.
240
+ self._active_invocations: set[asyncio.Future[None]] = set()
241
+ # Keep ancestry so a completed nested call cannot hide an active parent
242
+ # in the context inherited by child tasks, including cleanup tasks.
243
+ self._invocation_context: ContextVar[tuple[asyncio.Future[None], ...]] = ContextVar(
244
+ f"default_mcp_tool_handler_invocations_{id(self)}", default=()
245
+ )
230
246
  # Set by ``aclose`` to prevent post-close cache insertions and to
231
247
  # reject new ``invoke_tool`` calls. Once set, never cleared.
232
248
  self._closed = False
233
249
 
234
250
  async def invoke_tool(self, invocation: MCPToolInvocation) -> MCPToolResult:
235
- """Invoke ``invocation.tool_name`` on the cached MCP client for the server.
251
+ """Invoke ``invocation.tool_name`` on an MCP client for the server.
236
252
 
237
253
  The reserved name :attr:`LIST_TOOLS_TOOL_NAME` (``"tools/list"``) is
238
254
  intercepted client-side: instead of being forwarded as a tool call,
@@ -241,7 +257,6 @@ class DefaultMCPToolHandler:
241
257
  ``TextContent`` containing a JSON tool catalog.
242
258
  """
243
259
  from agent_framework import Content
244
- from agent_framework.exceptions import ToolExecutionException
245
260
 
246
261
  # Reserved-name args validation runs before connect: rejecting bad
247
262
  # input shouldn't require establishing an MCP session.
@@ -253,23 +268,55 @@ class DefaultMCPToolHandler:
253
268
  error_message=message,
254
269
  )
255
270
 
271
+ entry: _CacheEntry | None = None
272
+ completion: asyncio.Future[None] | None = None
273
+ context_token: Token[tuple[asyncio.Future[None], ...]] | None = None
256
274
  try:
257
- entry = await self._get_or_create_entry(invocation)
258
- except Exception as exc:
259
- # Connect / cache lookup failures surface as tool errors so the
260
- # workflow can store them at output.result without crashing.
261
- logger.warning(
262
- "DefaultMCPToolHandler: failed to obtain MCP client for url=%s tool=%s: %s",
263
- invocation.server_url,
264
- invocation.tool_name,
265
- exc,
266
- )
267
- message = f"Failed to connect to MCP server: {type(exc).__name__}: {exc}".rstrip(": ")
268
- return MCPToolResult(
269
- outputs=[Content.from_text(f"Error: {message}")],
270
- is_error=True,
271
- error_message=message,
272
- )
275
+ try:
276
+ if self._client_provider is None:
277
+ entry = await self._get_or_create_entry(invocation)
278
+ else:
279
+ async with self._cache_lock:
280
+ if self._closed:
281
+ raise RuntimeError("DefaultMCPToolHandler is closed")
282
+ completion = asyncio.get_running_loop().create_future()
283
+ self._active_invocations.add(completion)
284
+ context_token = self._invocation_context.set((*self._invocation_context.get(), completion))
285
+ entry = await self._create_entry(invocation)
286
+ if self._closed:
287
+ raise RuntimeError("DefaultMCPToolHandler is closed")
288
+ except Exception as exc:
289
+ # Connect / cache lookup failures surface as tool errors so the
290
+ # workflow can store them at output.result without crashing.
291
+ logger.warning(
292
+ "DefaultMCPToolHandler: failed to obtain MCP client for url=%s tool=%s: %s",
293
+ invocation.server_url,
294
+ invocation.tool_name,
295
+ exc,
296
+ )
297
+ message = f"Failed to connect to MCP server: {type(exc).__name__}: {exc}".rstrip(": ")
298
+ return MCPToolResult(
299
+ outputs=[Content.from_text(f"Error: {message}")],
300
+ is_error=True,
301
+ error_message=message,
302
+ )
303
+
304
+ return await self._invoke_entry(entry, invocation)
305
+ finally:
306
+ if completion is not None:
307
+ try:
308
+ if entry is not None:
309
+ await self._close_invocation_entry(entry)
310
+ finally:
311
+ if context_token is not None:
312
+ self._invocation_context.reset(context_token)
313
+ self._active_invocations.discard(completion)
314
+ completion.set_result(None)
315
+
316
+ async def _invoke_entry(self, entry: _CacheEntry, invocation: MCPToolInvocation) -> MCPToolResult:
317
+ """Dispatch a connected invocation and preserve tool error mapping."""
318
+ from agent_framework import Content
319
+ from agent_framework.exceptions import ToolExecutionException
273
320
 
274
321
  try:
275
322
  if invocation.tool_name == self.LIST_TOOLS_TOOL_NAME:
@@ -372,24 +419,42 @@ class DefaultMCPToolHandler:
372
419
  return MCPToolResult(outputs=[Content.from_text(json.dumps(payload, indent=2, allow_nan=False))])
373
420
 
374
421
  async def aclose(self) -> None:
375
- """Close all cached MCP clients and the owned httpx clients.
422
+ """Close cached clients and wait for provider-backed invocations to clean up.
376
423
 
377
424
  Caller-supplied :class:`httpx.AsyncClient` instances (returned by the
378
425
  ``client_provider`` callback) are NOT closed.
379
426
 
380
- Idempotent a second call returns immediately. Drains any in-flight
381
- ``_create_entry`` tasks before returning so their resources are
427
+ Provider-backed calls already executing may finish; pending connections
428
+ are rejected after connecting and cleaned up by their invocation.
429
+ Concurrent shutdown calls wait for those invocations as well.
430
+
431
+ Idempotent. In no-provider mode, a second call returns immediately.
432
+ Drains any in-flight ``_create_entry`` tasks before returning so their resources are
382
433
  cleaned up; the in-flight tasks see ``self._closed`` in phase 3 of
383
434
  :meth:`_get_or_create_entry`, close their own entry, and resolve
384
435
  their future with ``RuntimeError("DefaultMCPToolHandler is closed")``.
436
+
437
+ Raises:
438
+ RuntimeError: If called from an active provider-backed invocation's
439
+ context, including inherited child tasks and cleanup. Rejected
440
+ before changing handler state to avoid waiting on itself.
385
441
  """
442
+ if any(not completion.done() for completion in self._invocation_context.get()):
443
+ raise RuntimeError(
444
+ "DefaultMCPToolHandler.aclose() cannot be called from an active provider-backed invocation"
445
+ )
386
446
  async with self._cache_lock:
387
- if self._closed:
447
+ if self._closed and self._client_provider is None:
388
448
  return
389
449
  self._closed = True
390
450
  entries = list(self._cache.values())
391
451
  self._cache.clear()
392
452
  inflight_futures = list(self._inflight.values())
453
+ active_invocations = list(self._active_invocations)
454
+
455
+ for completion in active_invocations:
456
+ # Cancelling shutdown must not cancel an invocation's completion signal.
457
+ await asyncio.shield(completion)
393
458
 
394
459
  # Wait for in-flight creations to finish their self-cleanup. Each
395
460
  # in-flight task self-closes its entry under the closed-flag branch
@@ -509,7 +574,9 @@ class DefaultMCPToolHandler:
509
574
  provided_client: httpx.AsyncClient | None = None
510
575
  if self._client_provider is not None:
511
576
  provided_client = await self._client_provider(invocation)
512
- # Capture headers for this cache entry so the header_provider closure
577
+ if self._closed:
578
+ raise RuntimeError("DefaultMCPToolHandler is closed")
579
+ # Capture headers for this entry so the header_provider closure
513
580
  # always returns the same set, regardless of the runtime kwargs.
514
581
  captured_headers = dict(invocation.headers)
515
582
 
@@ -526,10 +593,18 @@ class DefaultMCPToolHandler:
526
593
  try:
527
594
  await tool.connect()
528
595
  except BaseException:
529
- try:
530
- await tool.close()
531
- except Exception: # pragma: no cover - best effort
532
- logger.debug("DefaultMCPToolHandler: error closing tool after failed connect", exc_info=True)
596
+ failed_entry = _CacheEntry(
597
+ tool=tool,
598
+ owned_httpx_client=(
599
+ cast("httpx.AsyncClient | None", getattr(tool, "_httpx_client", None))
600
+ if provided_client is None
601
+ else None
602
+ ),
603
+ )
604
+ if self._client_provider is not None:
605
+ await self._close_invocation_entry(failed_entry)
606
+ else:
607
+ await self._close_entry(failed_entry)
533
608
  raise
534
609
 
535
610
  # ``MCPStreamableHTTPTool.get_mcp_client`` lazily creates an
@@ -542,17 +617,34 @@ class DefaultMCPToolHandler:
542
617
  owned_client = cast("httpx.AsyncClient | None", getattr(tool, "_httpx_client", None))
543
618
  return _CacheEntry(tool=tool, owned_httpx_client=owned_client)
544
619
 
620
+ async def _close_invocation_entry(self, entry: _CacheEntry) -> None:
621
+ """Finish invocation cleanup even if the caller is cancelled again."""
622
+ # MCPStreamableHTTPTool dispatches connect/close to its lifecycle owner,
623
+ # keeping the SDK's cancel-scope entry and exit on that same task.
624
+ cleanup = asyncio.create_task(self._close_entry(entry))
625
+ cancelled = False
626
+ while not cleanup.done():
627
+ try:
628
+ await asyncio.shield(cleanup)
629
+ except asyncio.CancelledError:
630
+ cancelled = True
631
+ cleanup.result() # Propagate cancellation/errors from cleanup itself.
632
+ if cancelled:
633
+ raise asyncio.CancelledError
634
+
545
635
  async def _close_entry(self, entry: _CacheEntry) -> None:
546
636
  """Close the MCP tool and any owned httpx client."""
547
637
  try:
548
- await entry.tool.close()
549
- except Exception: # pragma: no cover - best effort
550
- logger.debug("DefaultMCPToolHandler: error closing MCP tool", exc_info=True)
551
- if entry.owned_httpx_client is not None:
552
638
  try:
553
- await entry.owned_httpx_client.aclose()
639
+ await entry.tool.close()
554
640
  except Exception: # pragma: no cover - best effort
555
- logger.debug("DefaultMCPToolHandler: error closing owned httpx client", exc_info=True)
641
+ logger.debug("DefaultMCPToolHandler: error closing MCP tool", exc_info=True)
642
+ finally:
643
+ if entry.owned_httpx_client is not None and not entry.owned_httpx_client.is_closed:
644
+ try:
645
+ await entry.owned_httpx_client.aclose()
646
+ except Exception: # pragma: no cover - best effort
647
+ logger.debug("DefaultMCPToolHandler: error closing owned httpx client", exc_info=True)
556
648
 
557
649
  @staticmethod
558
650
  def _cache_key(
@@ -563,10 +655,9 @@ class DefaultMCPToolHandler:
563
655
  ) -> tuple[str, str, str, str]:
564
656
  """Build an order-independent cache key for the invocation identity.
565
657
 
566
- The key includes ``server_label`` and ``connection_name`` so that
567
- callers using ``client_provider`` to dispatch on those fields
568
- receive a fresh client per logical connection (matches the
569
- documented dispatch contract).
658
+ Used only without a ``client_provider``. The key includes
659
+ ``server_label`` and ``connection_name`` to distinguish logical
660
+ connections.
570
661
 
571
662
  Header *names* are lower-cased inside the hash payload only so
572
663
  that ``Authorization`` and ``authorization`` map to the same
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
4
4
  authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
- version = "1.0.4"
7
+ version = "1.1.0"
8
8
  license-files = ["LICENSE"]
9
9
  urls.homepage = "https://aka.ms/agent-framework"
10
10
  urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,14 +22,14 @@ classifiers = [
22
22
  "Typing :: Typed",
23
23
  ]
24
24
  dependencies = [
25
- "agent-framework-core>=1.15.0,<2",
25
+ "agent-framework-core>=1.19.0,<2",
26
26
  "httpx>=0.27,<1",
27
27
  "powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
28
28
  "pyyaml>=6.0,<7.0",
29
29
  ]
30
30
  [dependency-groups]
31
31
  dev = [
32
- "types-PyYaml==6.0.12.20260518"
32
+ "types-PyYaml==6.0.12.20260906"
33
33
  ]
34
34
 
35
35
  [tool.uv]
@@ -1,49 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: agent-framework-declarative
3
- Version: 1.0.4
4
- Summary: Declarative specification support for Microsoft Agent Framework.
5
- Author-email: Microsoft <af-support@microsoft.com>
6
- Requires-Python: >=3.10
7
- Description-Content-Type: text/markdown
8
- Classifier: License :: OSI Approved :: MIT License
9
- Classifier: Development Status :: 5 - Production/Stable
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
16
- Classifier: Typing :: Typed
17
- License-File: LICENSE
18
- Requires-Dist: agent-framework-core>=1.15.0,<2
19
- Requires-Dist: httpx>=0.27,<1
20
- Requires-Dist: powerfx>=0.0.32,<0.0.35; python_version < '3.14'
21
- Requires-Dist: pyyaml>=6.0,<7.0
22
- Project-URL: homepage, https://aka.ms/agent-framework
23
- Project-URL: issues, https://github.com/microsoft/agent-framework/issues
24
- Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
25
- Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
26
-
27
- # Get Started with Microsoft Agent Framework Declarative
28
-
29
- Please install this package via pip:
30
-
31
- ```bash
32
- pip install agent-framework-declarative
33
- ```
34
-
35
- ## Release stage
36
-
37
- This package ships at two different stability levels:
38
-
39
- - **Declarative workflows** (`WorkflowFactory`, executors, handlers, and the
40
- `_workflows` surface) are **stable**.
41
- - **Declarative agents** (`AgentFactory` and the YAML agent loading/parsing path:
42
- `DeclarativeLoaderError`, `ProviderLookupError`, `ProviderTypeMapping`) are
43
- **experimental** and may change or be removed in future versions without notice.
44
- Using any of these symbols emits an `ExperimentalWarning` on first use.
45
-
46
- ## Declarative features
47
-
48
- The declarative packages provides support for building agents based on a declarative yaml specification.
49
-
@@ -1,22 +0,0 @@
1
- # Get Started with Microsoft Agent Framework Declarative
2
-
3
- Please install this package via pip:
4
-
5
- ```bash
6
- pip install agent-framework-declarative
7
- ```
8
-
9
- ## Release stage
10
-
11
- This package ships at two different stability levels:
12
-
13
- - **Declarative workflows** (`WorkflowFactory`, executors, handlers, and the
14
- `_workflows` surface) are **stable**.
15
- - **Declarative agents** (`AgentFactory` and the YAML agent loading/parsing path:
16
- `DeclarativeLoaderError`, `ProviderLookupError`, `ProviderTypeMapping`) are
17
- **experimental** and may change or be removed in future versions without notice.
18
- Using any of these symbols emits an `ExperimentalWarning` on first use.
19
-
20
- ## Declarative features
21
-
22
- The declarative packages provides support for building agents based on a declarative yaml specification.