agent-framework-orchestrations 1.3.0__tar.gz → 1.3.2__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 (18) hide show
  1. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/PKG-INFO +4 -2
  2. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_concurrent.py +10 -1
  3. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_group_chat.py +15 -3
  4. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_handoff.py +20 -3
  5. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_magentic.py +40 -11
  6. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_orchestration_request_info.py +18 -0
  7. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_sequential.py +6 -1
  8. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/pyproject.toml +3 -2
  9. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/LICENSE +0 -0
  10. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/README.md +0 -0
  11. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/__init__.py +0 -0
  12. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_base_group_chat_orchestrator.py +0 -0
  13. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_feature_usage.py +0 -0
  14. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_orchestration_state.py +0 -0
  15. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_orchestrator_helpers.py +0 -0
  16. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_participant_output_config.py +0 -0
  17. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/_workflow_builder.py +0 -0
  18. {agent_framework_orchestrations-1.3.0 → agent_framework_orchestrations-1.3.2}/agent_framework_orchestrations/py.typed +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: agent-framework-orchestrations
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Orchestration patterns for Microsoft Agent Framework. Includes SequentialBuilder, ConcurrentBuilder, HandoffBuilder, GroupChatBuilder, and MagenticBuilder.
5
5
  Author-email: Microsoft <af-support@microsoft.com>
6
6
  Requires-Python: >=3.10
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
16
  Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
17
18
  Classifier: Typing :: Typed
18
19
  License-File: LICENSE
19
20
  Requires-Dist: agent-framework-core>=1.19.0,<2
@@ -21,6 +22,7 @@ Project-URL: homepage, https://aka.ms/agent-framework
21
22
  Project-URL: issues, https://github.com/microsoft/agent-framework/issues
22
23
  Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
23
24
  Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
25
+ Import-Name: agent_framework_orchestrations
24
26
 
25
27
  # Agent Framework Orchestrations
26
28
 
@@ -18,7 +18,7 @@ from agent_framework._workflows._workflow_context import WorkflowContext
18
18
  from typing_extensions import Never
19
19
 
20
20
  from ._feature_usage import FeatureIndex
21
- from ._orchestration_request_info import AgentApprovalExecutor
21
+ from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter
22
22
  from ._participant_output_config import (
23
23
  UNSET,
24
24
  _coalesce_output_from, # pyright: ignore[reportPrivateUsage]
@@ -172,6 +172,10 @@ class _CallbackAggregator(Executor):
172
172
  else:
173
173
  ret = await asyncio.to_thread(self._callback, results)
174
174
 
175
+ # Async callable objects and wrappers returning a coroutine are not coroutine functions
176
+ if inspect.isawaitable(ret):
177
+ ret = await ret
178
+
175
179
  # If the callback returned a value, finalize the workflow with it
176
180
  if ret is not None:
177
181
  await ctx.yield_output(ret)
@@ -382,6 +386,11 @@ class ConcurrentBuilder:
382
386
  else:
383
387
  raise TypeError(f"Participants must be SupportsAgentRun or Executor instances. Got {type(p).__name__}.")
384
388
 
389
+ if self._request_info_enabled:
390
+ validate_request_info_filter(
391
+ self._request_info_filter,
392
+ [resolve_agent_id(p) for p in participants if not isinstance(p, Executor)],
393
+ )
385
394
  return executors
386
395
 
387
396
  def build(self) -> Workflow:
@@ -51,7 +51,7 @@ from ._base_group_chat_orchestrator import (
51
51
  TerminationCondition,
52
52
  )
53
53
  from ._feature_usage import FeatureIndex
54
- from ._orchestration_request_info import AgentApprovalExecutor
54
+ from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter
55
55
  from ._orchestrator_helpers import clean_conversation_for_handoff, extract_markdown_fence_bodies
56
56
  from ._participant_output_config import (
57
57
  UNSET,
@@ -519,8 +519,15 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator):
519
519
  # Parse and validate the structured output
520
520
  agent_orchestration_output = self._parse_agent_output(agent_response)
521
521
 
522
- if not agent_orchestration_output.terminate and not agent_orchestration_output.next_speaker:
523
- raise ValueError("next_speaker must be provided if not terminating the conversation.")
522
+ if not agent_orchestration_output.terminate:
523
+ next_speaker = agent_orchestration_output.next_speaker
524
+ if not next_speaker:
525
+ raise ValueError("next_speaker must be provided if not terminating the conversation.")
526
+ if next_speaker not in self._participant_registry.participants:
527
+ raise ValueError(
528
+ f"Orchestrator agent selected unknown participant '{next_speaker}'. "
529
+ f"Valid participants: {', '.join(self._participant_registry.participants)}."
530
+ )
524
531
 
525
532
  return agent_orchestration_output
526
533
 
@@ -1023,6 +1030,11 @@ class GroupChatBuilder:
1023
1030
  f"Participants must be SupportsAgentRun or Executor instances. Got {type(participant).__name__}."
1024
1031
  )
1025
1032
 
1033
+ if self._request_info_enabled:
1034
+ validate_request_info_filter(
1035
+ self._request_info_filter,
1036
+ [resolve_agent_id(p) for p in participants if not isinstance(p, Executor)],
1037
+ )
1026
1038
  return executors
1027
1039
 
1028
1040
  def build(self) -> Workflow:
@@ -32,6 +32,7 @@ Key properties:
32
32
  import inspect
33
33
  import json
34
34
  import logging
35
+ import re
35
36
  import sys
36
37
  from collections.abc import Awaitable, Callable, Mapping, Sequence
37
38
  from copy import deepcopy
@@ -122,9 +123,17 @@ class HandoffConfiguration:
122
123
  return hash(self.target_id)
123
124
 
124
125
 
126
+ _INVALID_TOOL_NAME_CHARACTERS = re.compile(r"[^a-zA-Z0-9_-]")
127
+ _MAX_TOOL_NAME_LENGTH = 64
128
+
129
+
125
130
  def get_handoff_tool_name(target_id: str) -> str:
126
- """Get the standardized handoff tool name for a given target agent ID."""
127
- return f"handoff_to_{target_id}"
131
+ """Get the standardized handoff tool name for a given target agent ID.
132
+
133
+ Characters that model providers reject in tool names, such as spaces, are replaced with underscores,
134
+ and the name is cut to the 64 characters that providers allow.
135
+ """
136
+ return f"handoff_to_{_INVALID_TOOL_NAME_CHARACTERS.sub('_', target_id)}"[:_MAX_TOOL_NAME_LENGTH]
128
137
 
129
138
 
130
139
  HANDOFF_FUNCTION_RESULT_KEY = "handoff_to"
@@ -266,6 +275,9 @@ class HandoffAgentExecutor(AgentExecutor):
266
275
  autonomous_mode_prompt: Prompt to provide to the agent when continuing in autonomous mode.
267
276
  This will guide the agent in the absence of user input.
268
277
  autonomous_mode_turn_limit: Maximum number of autonomous turns before requesting user input.
278
+ ``0`` returns control to the user as soon as the agent responds
279
+ without a handoff; the default limit is only used when this is
280
+ left as ``None``.
269
281
  """
270
282
  self._auto_handoff_middleware = _AutoHandoffMiddleware(handoffs)
271
283
  cloned_agent = self._prepare_agent_with_handoffs(agent, handoffs, self._auto_handoff_middleware)
@@ -279,7 +291,9 @@ class HandoffAgentExecutor(AgentExecutor):
279
291
  # Autonomous mode members
280
292
  self._autonomous_mode = autonomous_mode
281
293
  self._autonomous_mode_prompt = autonomous_mode_prompt or _AUTONOMOUS_MODE_DEFAULT_PROMPT
282
- self._autonomous_mode_turn_limit = autonomous_mode_turn_limit or _DEFAULT_AUTONOMOUS_TURN_LIMIT
294
+ self._autonomous_mode_turn_limit = (
295
+ _DEFAULT_AUTONOMOUS_TURN_LIMIT if autonomous_mode_turn_limit is None else autonomous_mode_turn_limit
296
+ )
283
297
  self._autonomous_mode_turns = 0
284
298
 
285
299
  def _prepare_agent_with_handoffs(
@@ -372,6 +386,7 @@ class HandoffAgentExecutor(AgentExecutor):
372
386
  f"Handoff tool name '{handoff_tool.name}' conflicts with existing tool."
373
387
  "Please rename the existing tool or modify the target agent ID to avoid conflicts."
374
388
  )
389
+ existing_names.add(handoff_tool.name)
375
390
  new_tools.append(handoff_tool)
376
391
 
377
392
  if new_tools:
@@ -928,6 +943,8 @@ class HandoffBuilder:
928
943
  in autonomous mode. If not provided, a default prompt will be used.
929
944
  turn_limits: Optional mapping of agent identifiers/factory names to maximum number of autonomous turns
930
945
  before returning control to the user. If not provided, a default turn limit will be used.
946
+ A limit of ``0`` disables autonomous continuation for that agent instead of falling back
947
+ to the default.
931
948
  """
932
949
  self._autonomous_mode = True
933
950
  self._autonomous_mode_prompts = prompts or {}
@@ -427,8 +427,20 @@ def _extract_json(text: str) -> dict[str, Any]:
427
427
  raise ValueError("No JSON object found.")
428
428
  depth = 0
429
429
  end = None
430
+ in_string = False
431
+ escaped = False
430
432
  for i, ch in enumerate(text[start:], start=start):
431
- if ch == "{":
433
+ # Braces inside string values, such as code in an instruction, do not count.
434
+ if in_string:
435
+ if escaped:
436
+ escaped = False
437
+ elif ch == "\\":
438
+ escaped = True
439
+ elif ch == '"':
440
+ in_string = False
441
+ elif ch == '"':
442
+ in_string = True
443
+ elif ch == "{":
432
444
  depth += 1
433
445
  elif ch == "}":
434
446
  depth -= 1
@@ -517,7 +529,7 @@ class MagenticManagerBase(ABC):
517
529
 
518
530
 
519
531
  class StandardMagenticManager(MagenticManagerBase):
520
- """Standard Magentic manager that performs real LLM calls via a Agent.
532
+ """Standard Magentic manager that performs real LLM calls via an Agent.
521
533
 
522
534
  The manager constructs prompts that mirror the original Magentic One orchestration:
523
535
  - Facts gathering
@@ -724,21 +736,37 @@ class StandardMagenticManager(MagenticManagerBase):
724
736
  # Include full context to help the model decide current stage, with small retry loop
725
737
  attempts = 0
726
738
  last_error: Exception | None = None
739
+ unknown_speaker_ledger: MagenticProgressLedger | None = None
727
740
  while attempts < self.progress_ledger_retry_count:
728
741
  raw = await self._complete([*magentic_context.chat_history, user_message])
729
742
  try:
730
743
  ledger_dict = _extract_json(raw.text)
731
- return _coerce_model(MagenticProgressLedger, ledger_dict)
744
+ ledger = _coerce_model(MagenticProgressLedger, ledger_dict)
732
745
  except Exception as ex:
733
746
  last_error = ex
734
- attempts += 1
735
- logger.warning(
736
- f"Progress ledger JSON parse failed (attempt {attempts}/{self.progress_ledger_retry_count}): {ex}"
737
- )
738
- if attempts < self.progress_ledger_retry_count:
739
- # brief backoff before next try
740
- await asyncio.sleep(0.25 * attempts)
747
+ unknown_speaker_ledger = None
748
+ else:
749
+ next_speaker = ledger.next_speaker.answer
750
+ if (
751
+ ledger.is_request_satisfied.answer
752
+ or not isinstance(next_speaker, str)
753
+ or next_speaker in agent_names
754
+ ):
755
+ return ledger
756
+ # Retry an unknown name, but keep the ledger so running out of attempts ends the run
757
+ # through the orchestrator's invalid speaker handling instead of another reset.
758
+ unknown_speaker_ledger = ledger
759
+ last_error = ValueError(f"Unknown next speaker '{next_speaker}'. Valid names: {names_csv}.")
760
+ attempts += 1
761
+ logger.warning(
762
+ f"Progress ledger attempt failed ({attempts}/{self.progress_ledger_retry_count}): {last_error}"
763
+ )
764
+ if attempts < self.progress_ledger_retry_count:
765
+ # brief backoff before next try
766
+ await asyncio.sleep(0.25 * attempts)
741
767
 
768
+ if unknown_speaker_ledger is not None:
769
+ return unknown_speaker_ledger
742
770
  raise RuntimeError(
743
771
  f"Progress ledger parse failed after {self.progress_ledger_retry_count} attempt(s): {last_error}"
744
772
  )
@@ -1374,8 +1402,9 @@ class MagenticAgentExecutor(AgentExecutor):
1374
1402
  # Request into related
1375
1403
  self._pending_agent_requests.clear()
1376
1404
  self._pending_responses_to_agent.clear()
1405
+ self._pending_request_order.clear()
1377
1406
  # Reset sessions
1378
- self._agent_thread = self._agent.create_session()
1407
+ self._session = self._agent.create_session()
1379
1408
 
1380
1409
 
1381
1410
  # endregion Magentic Agent Executor
@@ -41,6 +41,24 @@ def resolve_request_info_filter(agents: list[str | SupportsAgentRun] | None) ->
41
41
  return result
42
42
 
43
43
 
44
+ def validate_request_info_filter(request_info_filter: set[str] | None, agent_ids: list[str]) -> None:
45
+ """Raise if the request info filter names an agent that is not a participant.
46
+
47
+ Args:
48
+ request_info_filter: Agent IDs passed to ``with_request_info(agents=...)``. Empty or None means all agents.
49
+ agent_ids: IDs of the agent participants in the orchestration.
50
+
51
+ Raises:
52
+ ValueError: If a name in the filter does not match any agent participant.
53
+ """
54
+ unknown = sorted((request_info_filter or set()) - set(agent_ids))
55
+ if unknown:
56
+ raise ValueError(
57
+ f"with_request_info() names agents that are not agent participants: "
58
+ f"{', '.join(repr(name) for name in unknown)}. Agent participants: {', '.join(agent_ids)}."
59
+ )
60
+
61
+
44
62
  @dataclass
45
63
  class AgentRequestInfoResponse:
46
64
  """Response containing additional information requested from users for agents.
@@ -32,7 +32,7 @@ from agent_framework._workflows._workflow import Workflow
32
32
  from agent_framework._workflows._workflow_context import WorkflowContext
33
33
 
34
34
  from ._feature_usage import FeatureIndex
35
- from ._orchestration_request_info import AgentApprovalExecutor
35
+ from ._orchestration_request_info import AgentApprovalExecutor, validate_request_info_filter
36
36
  from ._participant_output_config import (
37
37
  UNSET,
38
38
  _coalesce_output_from, # pyright: ignore[reportPrivateUsage]
@@ -230,6 +230,11 @@ class SequentialBuilder:
230
230
  else:
231
231
  raise TypeError(f"Participants must be SupportsAgentRun or Executor instances. Got {type(p).__name__}.")
232
232
 
233
+ if self._request_info_enabled:
234
+ validate_request_info_filter(
235
+ self._request_info_filter,
236
+ [resolve_agent_id(p) for p in participants if not isinstance(p, Executor)],
237
+ )
233
238
  return executors
234
239
 
235
240
  def build(self) -> Workflow:
@@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se
4
4
  authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
- version = "1.3.0"
7
+ version = "1.3.2"
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"
@@ -20,6 +20,7 @@ classifiers = [
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
22
  "Programming Language :: Python :: 3.14",
23
+ "Programming Language :: Python :: 3.15",
23
24
  "Typing :: Typed",
24
25
  ]
25
26
  dependencies = [
@@ -92,5 +93,5 @@ help = "Run the default unit test suite for this package."
92
93
  cmd = 'pytest -m "not integration" --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests'
93
94
 
94
95
  [build-system]
95
- requires = ["flit-core >= 3.11,<4.0"]
96
+ requires = ["flit-core>=3.11,<5.0"]
96
97
  build-backend = "flit_core.buildapi"