agent-framework-declarative 1.0.3__tar.gz → 1.0.4__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 (24) hide show
  1. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/PKG-INFO +1 -1
  2. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_declarative_base.py +14 -20
  3. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_executors_control_flow.py +1 -0
  4. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_http_handler.py +63 -5
  5. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/pyproject.toml +2 -2
  6. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/LICENSE +0 -0
  7. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/README.md +0 -0
  8. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/__init__.py +0 -0
  9. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_feature_usage.py +0 -0
  10. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_loader.py +0 -0
  11. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_models.py +0 -0
  12. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/__init__.py +0 -0
  13. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_declarative_builder.py +0 -0
  14. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_errors.py +0 -0
  15. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_executors_agents.py +0 -0
  16. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_executors_basic.py +0 -0
  17. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_executors_external_input.py +0 -0
  18. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_executors_http.py +0 -0
  19. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_executors_mcp.py +0 -0
  20. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_executors_tools.py +0 -0
  21. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_factory.py +0 -0
  22. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_mcp_handler.py +0 -0
  23. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_powerfx_functions.py +0 -0
  24. {agent_framework_declarative-1.0.3 → agent_framework_declarative-1.0.4}/agent_framework_declarative/_workflows/_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-declarative
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: Declarative specification support for Microsoft Agent Framework.
5
5
  Author-email: Microsoft <af-support@microsoft.com>
6
6
  Requires-Python: >=3.10
@@ -1084,6 +1084,8 @@ class DeclarativeActionExecutor(Executor):
1084
1084
  Follows .NET's DefaultTransform pattern - accepts any input type:
1085
1085
  - dict/Mapping: Used directly as workflow.inputs
1086
1086
  - str: Converted to {"input": value}
1087
+ - Message: Starts a fresh run (e.g. from DevUI), with text and ID
1088
+ surfaced through Inputs.input and System.LastMessage*.
1087
1089
  - list[Message]: Treated as the agent-facing message contract
1088
1090
  (e.g. from WorkflowAgent / as_agent()). The prior conversation
1089
1091
  history is stored in ``Conversation.messages``/
@@ -1113,26 +1115,18 @@ class DeclarativeActionExecutor(Executor):
1113
1115
  if isinstance(trigger, dict):
1114
1116
  # Structured inputs - use directly
1115
1117
  state.initialize(trigger) # type: ignore
1116
- elif isinstance(trigger, list) and all(isinstance(m, Message) for m in trigger): # pyright: ignore[reportUnknownVariableType]
1117
- # list[Message] (e.g. from WorkflowAgent / as_agent()).
1118
- messages_list = cast(list[Message], trigger)
1119
-
1120
- # Detect continuation: if the workflow's shared state already
1121
- # carries declarative data from a prior turn (because the host
1122
- # restored a checkpoint and dispatched this run with
1123
- # reset_context=False), we MUST NOT call state.initialize() -
1124
- # that would wipe Conversation.messages, Local.*, System.* etc.
1125
- # Instead, treat the trigger as the new turn's user input only:
1126
- # update Inputs.input, append the new user message to existing
1127
- # Conversation history, and refresh System.LastMessage*.
1128
- #
1129
- # Continuation = declarative state already exists in the workflow's
1130
- # shared state (either left over in-memory from a prior turn on
1131
- # the same instance, or restored from a checkpoint just before
1132
- # this run). In that case state.initialize() would wipe Local.*,
1133
- # System.*, Conversation.* etc., destroying the cross-turn
1134
- # context we're trying to preserve.
1135
- is_continuation = state.is_initialized()
1118
+ elif isinstance(trigger, Message) or (
1119
+ isinstance(trigger, list) and all(isinstance(m, Message) for m in trigger) # pyright: ignore[reportUnknownVariableType]
1120
+ ):
1121
+ # Message (e.g. from DevUI) or list[Message] (e.g. from WorkflowAgent / as_agent()).
1122
+ messages_list = [trigger] if isinstance(trigger, Message) else cast(list[Message], trigger)
1123
+
1124
+ # Preserve the existing list[Message] continuation contract used
1125
+ # by WorkflowAgent. A bare Message starts a fresh run: DevUI can
1126
+ # reuse this workflow instance across unrelated conversations.
1127
+ # State left on the instance is not evidence of session ownership.
1128
+ # Explicit checkpoint/HIL restores use the runner's resume path.
1129
+ is_continuation = not isinstance(trigger, Message) and state.is_initialized()
1136
1130
 
1137
1131
  # Locate the trailing user message in the trigger.
1138
1132
  last_user_index = -1
@@ -400,6 +400,7 @@ class JoinExecutor(DeclarativeActionExecutor):
400
400
  self,
401
401
  trigger: dict[str, Any]
402
402
  | str
403
+ | Message
403
404
  | list[Message]
404
405
  | ActionTrigger
405
406
  | ActionComplete
@@ -24,6 +24,7 @@ import asyncio
24
24
  from collections.abc import Awaitable, Callable, Mapping
25
25
  from dataclasses import dataclass, field
26
26
  from typing import Any, Protocol, runtime_checkable
27
+ from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit
27
28
 
28
29
  import httpx
29
30
 
@@ -173,16 +174,56 @@ class DefaultHttpRequestHandler:
173
174
  # callers (not just the YAML executor) get sensible defaults.
174
175
  headers["Content-Type"] = info.body_content_type or "text/plain"
175
176
 
176
- params: Mapping[str, str] | None = info.query_parameters or None
177
-
178
- response = await client.request(
177
+ # Compose the query as raw bytes rather than routing it through ``params=``.
178
+ # Anything that reaches ``httpx.QueryParams`` -- ``params=`` or the client's own
179
+ # ``AsyncClient.params`` merge -- is decoded and re-encoded. Measured against
180
+ # httpx 0.28.1, that rewrites ``%20`` in a value as ``+``, expands a bare
181
+ # ``download`` into ``download=``, and reorders interleaved duplicates
182
+ # (``x=1&y=2&x=3`` becomes ``x=1&x=3&y=2``), all of it even when nothing needed
183
+ # merging. Base64 and ``%3A`` round-trip unchanged, so a signature is not
184
+ # rewritten by itself, but any scheme that signs a value holding an encoded
185
+ # space is, and a server that distinguishes ``download`` from ``download=`` or
186
+ # reads repeated keys positionally sees a different request either way.
187
+ # Building the query here and writing it back over the request's ``raw_path``
188
+ # keeps the caller's bytes exactly as given.
189
+ raw = urlsplit(info.url)
190
+
191
+ # ``query_parameters`` append rather than replace, so a URL carrying
192
+ # ``filter=region&filter=status`` plus ``{"filter": "tenant"}`` sends all three,
193
+ # matching the .NET handler. Client-level params are defaults only: they apply
194
+ # for a key absent from both request-level sources.
195
+ explicit_pairs = [(key, value) for key, value in info.query_parameters.items() if key]
196
+ request_keys = {key for key, _ in parse_qsl(raw.query, keep_blank_values=True)}
197
+ request_keys.update(key for key, _ in explicit_pairs)
198
+ client_defaults = [(key, value) for key, value in client.params.multi_items() if key not in request_keys]
199
+
200
+ query_segments: list[str] = []
201
+ if raw.query:
202
+ query_segments.append(raw.query)
203
+ # ``quote_via=quote`` keeps spaces as ``%20`` instead of ``+``, so appended
204
+ # parameters are encoded the same way the preserved URL query is.
205
+ for pairs in (explicit_pairs, client_defaults):
206
+ if pairs:
207
+ query_segments.append(urlencode(pairs, quote_via=quote))
208
+ query = "&".join(segment for segment in query_segments if segment)
209
+
210
+ # Build without the query so the client's params merge has nothing to clobber --
211
+ # it drops the URL's query outright -- then overwrite the query httpx composed
212
+ # with ours. The fragment stays on the built URL; httpx does not send it.
213
+ request = client.build_request(
179
214
  method=info.method,
180
- url=info.url,
181
- params=params,
215
+ url=urlunsplit((raw.scheme, raw.netloc, raw.path, "", raw.fragment)),
182
216
  headers=headers or None,
183
217
  content=content,
184
218
  timeout=timeout,
185
219
  )
220
+ # Reuse httpx's own path encoding and replace only the query part of ``raw_path``.
221
+ raw_path = request.url.raw_path.split(b"?", 1)[0]
222
+ if query:
223
+ raw_path += b"?" + _encode_query(query)
224
+ request.url = request.url.copy_with(raw_path=raw_path)
225
+
226
+ response = await client.send(request)
186
227
 
187
228
  # Preserve multi-value headers (e.g. multiple Set-Cookie) as list[str].
188
229
  # Normalize names to lowercase so lookups are consistent and case
@@ -231,6 +272,23 @@ class DefaultHttpRequestHandler:
231
272
  await self.aclose()
232
273
 
233
274
 
275
+ #: Characters a query string may carry literally: RFC 3986 sub-delims plus ``:@/?``, the
276
+ #: square brackets many APIs use in key names, and ``%`` so escapes already in the
277
+ #: caller's URL are left alone instead of being double-encoded. Everything legal stays
278
+ #: as authored and only bytes that cannot appear in a URL get escaped.
279
+ _QUERY_SAFE = "!$&'()*+,;=:@/?%[]~"
280
+
281
+
282
+ def _encode_query(query: str) -> bytes:
283
+ """Return *query* as URL-safe bytes without disturbing what is already valid.
284
+
285
+ A caller can hand us a URL whose query holds non-ASCII text -- ``?q=café`` -- which
286
+ cannot go on the wire as-is. Percent-encoding only the characters that need it keeps
287
+ an already-valid query byte-identical, which is the point of composing it by hand.
288
+ """
289
+ return quote(query, safe=_QUERY_SAFE).encode("ascii")
290
+
291
+
234
292
  def _has_header(headers: Mapping[str, str], name: str) -> bool:
235
293
  """Case-insensitive header presence check."""
236
294
  needle = name.lower()
@@ -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.3"
7
+ version = "1.0.4"
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"
@@ -33,7 +33,7 @@ dev = [
33
33
  ]
34
34
 
35
35
  [tool.uv]
36
- prerelease = "if-necessary-or-explicit"
36
+ prerelease = "if-necessary"
37
37
  environments = [
38
38
  "sys_platform == 'darwin'",
39
39
  "sys_platform == 'linux'",