agent-framework-core 1.14.0__tar.gz → 1.16.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 (109) hide show
  1. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/PKG-INFO +1 -1
  2. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/__init__.py +4 -0
  3. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/__init__.pyi +4 -0
  4. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_agent_hooks.py +112 -64
  5. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_agents.py +29 -6
  6. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_compaction.py +4 -0
  7. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_evaluation.py +1 -2
  8. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_agent.py +17 -3
  9. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_agent.pyi +2 -0
  10. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_background_agents.py +26 -3
  11. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_loop.py +169 -119
  12. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_tool_approval.py +27 -5
  13. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_mcp.py +90 -33
  14. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_middleware.py +65 -0
  15. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_serialization.py +2 -2
  16. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_sessions.py +89 -5
  17. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_settings.py +30 -4
  18. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_skills.py +3 -1
  19. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_tools.py +381 -30
  20. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_types.py +43 -10
  21. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_agent.py +21 -8
  22. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_checkpoint.py +12 -10
  23. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_checkpoint_encoding.py +24 -0
  24. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_edge_runner.py +20 -3
  25. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_events.py +15 -5
  26. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_request_info_mixin.py +369 -369
  27. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_state.py +17 -10
  28. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_typing_utils.py +158 -17
  29. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_workflow.py +3 -1
  30. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/observability.py +759 -123
  31. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/pyproject.toml +1 -1
  32. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/LICENSE +0 -0
  33. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/README.md +0 -0
  34. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_clients.py +0 -0
  35. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_docstrings.py +0 -0
  36. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_feature_stage.py +0 -0
  37. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_filesystem.py +0 -0
  38. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/__init__.py +0 -0
  39. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_file_access.py +0 -0
  40. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_file_memory.py +0 -0
  41. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_memory.py +0 -0
  42. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_mode.py +0 -0
  43. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_harness/_todo.py +0 -0
  44. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_telemetry.py +0 -0
  45. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/__init__.py +0 -0
  46. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_agent_executor.py +0 -0
  47. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_agent_utils.py +0 -0
  48. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_const.py +0 -0
  49. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_conversation_history.py +0 -0
  50. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_edge.py +0 -0
  51. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_executor.py +0 -0
  52. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_function_executor.py +0 -0
  53. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_functional.py +0 -0
  54. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_message_utils.py +0 -0
  55. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_model_utils.py +0 -0
  56. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_runner.py +0 -0
  57. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_runner_context.py +0 -0
  58. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_validation.py +0 -0
  59. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_viz.py +0 -0
  60. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_workflow_builder.py +0 -0
  61. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_workflow_context.py +0 -0
  62. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/_workflows/_workflow_executor.py +0 -0
  63. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/a2a/__init__.py +0 -0
  64. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/a2a/__init__.pyi +0 -0
  65. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/ag_ui/__init__.py +0 -0
  66. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/ag_ui/__init__.pyi +0 -0
  67. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/amazon/__init__.py +0 -0
  68. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/amazon/__init__.pyi +0 -0
  69. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/anthropic/__init__.py +0 -0
  70. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/anthropic/__init__.pyi +0 -0
  71. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/azure/__init__.py +0 -0
  72. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/azure/__init__.pyi +0 -0
  73. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/chatkit/__init__.py +0 -0
  74. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/chatkit/__init__.pyi +0 -0
  75. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/declarative/__init__.py +0 -0
  76. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/declarative/__init__.pyi +0 -0
  77. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/devui/__init__.py +0 -0
  78. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/devui/__init__.pyi +0 -0
  79. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/exceptions.py +0 -0
  80. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/foundry/__init__.py +0 -0
  81. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/foundry/__init__.pyi +0 -0
  82. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/gemini/__init__.py +0 -0
  83. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/gemini/__init__.pyi +0 -0
  84. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/github/__init__.py +0 -0
  85. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/github/__init__.pyi +0 -0
  86. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/google/__init__.py +0 -0
  87. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/google/__init__.pyi +0 -0
  88. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/hyperlight/__init__.py +0 -0
  89. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/lab/__init__.py +0 -0
  90. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/mem0/__init__.py +0 -0
  91. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/mem0/__init__.pyi +0 -0
  92. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/microsoft/__init__.py +0 -0
  93. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/microsoft/__init__.pyi +0 -0
  94. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/mistral/__init__.py +0 -0
  95. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/mistral/__init__.pyi +0 -0
  96. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/monty/__init__.py +0 -0
  97. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/monty/__init__.pyi +0 -0
  98. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/ollama/__init__.py +0 -0
  99. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/ollama/__init__.pyi +0 -0
  100. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/openai/__init__.py +0 -0
  101. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/openai/__init__.pyi +0 -0
  102. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/orchestrations/__init__.py +0 -0
  103. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/orchestrations/__init__.pyi +0 -0
  104. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/py.typed +0 -0
  105. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/redis/__init__.py +0 -0
  106. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/redis/__init__.pyi +0 -0
  107. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/security.py +0 -0
  108. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/tools/__init__.py +0 -0
  109. {agent_framework_core-1.14.0 → agent_framework_core-1.16.0}/agent_framework/tools/__init__.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-core
3
- Version: 1.14.0
3
+ Version: 1.16.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
@@ -185,6 +185,7 @@ _LAZY_MODULE_EXPORTS: Final[Mapping[str, tuple[str, ...]]] = {
185
185
  "FunctionMiddleware",
186
186
  "FunctionMiddlewareTypes",
187
187
  "MiddlewareBundle",
188
+ "MiddlewareFailure",
188
189
  "MiddlewareTermination",
189
190
  "MiddlewareType",
190
191
  "MiddlewareTypes",
@@ -293,6 +294,7 @@ _LAZY_MODULE_EXPORTS: Final[Mapping[str, tuple[str, ...]]] = {
293
294
  "InMemoryCheckpointStorage",
294
295
  "WorkflowCheckpoint",
295
296
  ),
297
+ "._workflows._checkpoint_encoding": ("register_checkpoint_type",),
296
298
  "._workflows._const": (
297
299
  "DEFAULT_MAX_ITERATIONS",
298
300
  "INTERNAL_SOURCE_ID",
@@ -509,6 +511,7 @@ __all__ = [
509
511
  "MessageInjectionMiddleware",
510
512
  "MiddlewareBundle",
511
513
  "MiddlewareException",
514
+ "MiddlewareFailure",
512
515
  "MiddlewareTermination",
513
516
  "MiddlewareType",
514
517
  "MiddlewareTypes",
@@ -631,6 +634,7 @@ __all__ = [
631
634
  "normalize_tools",
632
635
  "prepend_agent_framework_to_user_agent",
633
636
  "prepend_instructions_to_messages",
637
+ "register_checkpoint_type",
634
638
  "register_state_type",
635
639
  "resolve_agent_id",
636
640
  "response_handler",
@@ -144,6 +144,7 @@ from ._middleware import (
144
144
  FunctionMiddleware,
145
145
  FunctionMiddlewareTypes,
146
146
  MiddlewareBundle,
147
+ MiddlewareFailure,
147
148
  MiddlewareTermination,
148
149
  MiddlewareType,
149
150
  MiddlewareTypes,
@@ -259,6 +260,7 @@ from ._workflows._checkpoint import (
259
260
  InMemoryCheckpointStorage,
260
261
  WorkflowCheckpoint,
261
262
  )
263
+ from ._workflows._checkpoint_encoding import register_checkpoint_type
262
264
  from ._workflows._const import DEFAULT_MAX_ITERATIONS, INTERNAL_SOURCE_ID
263
265
  from ._workflows._edge import (
264
266
  Case,
@@ -473,6 +475,7 @@ __all__ = [
473
475
  "MessageInjectionMiddleware",
474
476
  "MiddlewareBundle",
475
477
  "MiddlewareException",
478
+ "MiddlewareFailure",
476
479
  "MiddlewareTermination",
477
480
  "MiddlewareType",
478
481
  "MiddlewareTypes",
@@ -595,6 +598,7 @@ __all__ = [
595
598
  "normalize_tools",
596
599
  "prepend_agent_framework_to_user_agent",
597
600
  "prepend_instructions_to_messages",
601
+ "register_checkpoint_type",
598
602
  "register_state_type",
599
603
  "resolve_agent_id",
600
604
  "response_handler",
@@ -37,7 +37,13 @@ Enforcement semantics (``mode="enforce"``):
37
37
  not executed (or its result is discarded) and a tool-error payload is surfaced to the
38
38
  model so the agent loop can continue, per the spec's block-propagation rules. A
39
39
  ``host_error:*`` deny at the tool seam additionally halts the run (the enforcement
40
- layer itself failed, so continuing would be unreliable).
40
+ layer itself failed, so continuing would be unreliable): the run is aborted through
41
+ the function-invocation loop's fail-closed escape
42
+ (:class:`~agent_framework.MiddlewareFailure`) and the
43
+ :class:`agent_hooks.InterceptionBlocked` propagates to the caller, exactly like a
44
+ run-level deny. Other unexpected failures inside the enforcement layer (projection
45
+ bug, emitter fault) abort the run the same way and surface as
46
+ :class:`~agent_framework.MiddlewareFailure`.
41
47
  - Framework middleware short-circuits (``MiddlewareTermination``) are guarded: a result
42
48
  substituted by another middleware still passes ``output`` / ``post_model_call`` /
43
49
  ``post_tool_call`` before it egresses or enters the transcript.
@@ -113,6 +119,7 @@ from ._middleware import (
113
119
  FunctionInvocationContext,
114
120
  FunctionMiddleware,
115
121
  MiddlewareBundle,
122
+ MiddlewareFailure,
116
123
  MiddlewareTermination,
117
124
  )
118
125
  from ._serialization import make_json_safe
@@ -231,7 +238,6 @@ class _RunState:
231
238
  builder: AgentContextBuilder
232
239
  session_scoped: bool
233
240
  config: _AgentHooksConfig
234
- halted: BaseException | None = None
235
241
 
236
242
 
237
243
  _RUN_STATE: ContextVar[_RunState | None] = ContextVar("agent_framework_agent_hooks_run_state", default=None)
@@ -895,27 +901,63 @@ def _is_approval_request(result: Any) -> bool:
895
901
  return isinstance(result, Content) and result.type == "function_approval_request"
896
902
 
897
903
 
898
- def _halt_on_enforcement_failure(
899
- state: _RunState, context: FunctionInvocationContext, exc: BaseException, point: str
900
- ) -> NoReturn:
901
- """Route an unexpected failure inside the enforcement layer through the fail-closed halt path.
904
+ def _halt_on_enforcement_failure(exc: BaseException, point: str) -> NoReturn:
905
+ """Abort the run fail-closed on an unexpected failure inside the enforcement layer.
902
906
 
903
907
  The function-invocation loop converts arbitrary exceptions raised by function
904
908
  middleware into tool-error results and keeps running; for a failure of the
905
909
  enforcement layer itself (projection bug, emitter fault) that would fail open —
906
910
  the failure would vanish from the audit trail and the run would continue unguarded.
907
- Instead the loop is stopped via ``MiddlewareTermination`` (its only loud escape) and
908
- the agent middleware re-raises the failure to the caller at the run boundary.
911
+ :class:`MiddlewareFailure` is the loop's explicit fail-closed escape: it propagates
912
+ to the caller at the run boundary, with the underlying failure chained as its cause.
909
913
  """
910
- message = f"agent-hooks {point} enforcement failed: {type(exc).__name__}"
911
- context.result = {"error": message}
912
914
  if isinstance(exc, MiddlewareException):
913
- failure: BaseException = exc
914
- else:
915
- failure = MiddlewareException(message)
916
- failure.__cause__ = exc
917
- state.halted = failure
918
- raise MiddlewareTermination(message) from exc
915
+ raise MiddlewareFailure(str(exc)) from exc
916
+ raise MiddlewareFailure(f"agent-hooks {point} enforcement failed: {type(exc).__name__}") from exc
917
+
918
+
919
+ class _ToolSeamBlockFailure(MiddlewareFailure):
920
+ """Private tag for this feature's own tool-seam ``host_error`` halts.
921
+
922
+ Only failures raised by :meth:`_AgentHooksFunctionMiddleware._maybe_halt` carry
923
+ this type, which is what authorizes :func:`_reraise_tool_seam_block` to unwrap
924
+ the chained :class:`agent_hooks.InterceptionBlocked` at the run boundary. A plain
925
+ ``MiddlewareFailure`` raised by third-party middleware is never unwrapped — even
926
+ when its ``__cause__`` happens to be an ``InterceptionBlocked`` — so untrusted
927
+ code cannot launder a crafted interception record into this feature's deny
928
+ surface.
929
+ """
930
+
931
+
932
+ def _reraise_tool_seam_block(failure: MiddlewareFailure) -> NoReturn:
933
+ """Surface a tool-seam ``host_error`` block as the block itself at the run boundary.
934
+
935
+ The tool seam sits behind the function-invocation loop, so its fail-closed halts
936
+ travel as :class:`MiddlewareFailure` (the loop's loud escape) with the triggering
937
+ exception chained as the cause. When the failure is this feature's own tagged
938
+ :class:`_ToolSeamBlockFailure`, re-raise its :class:`agent_hooks.InterceptionBlocked`
939
+ cause so callers see the same deny surface — ``InterceptionBlocked`` carrying the
940
+ interception record — at every seam (run-, model-, and tool-level). Any other
941
+ failure (including a third-party ``MiddlewareFailure`` with a crafted
942
+ ``InterceptionBlocked`` cause) propagates exactly as raised.
943
+ """
944
+ from agent_hooks import InterceptionBlocked
945
+
946
+ block = failure.__cause__
947
+ if isinstance(failure, _ToolSeamBlockFailure) and isinstance(block, InterceptionBlocked):
948
+ # Detach the transport wrapper's back-links (both were set to the block when
949
+ # _maybe_halt raised the wrapper from it) before re-raising: re-raising the
950
+ # block while they are intact would make the two exceptions each other's
951
+ # cause/context — a chain cycle that every consumer walking
952
+ # __cause__/__context__ would have to guard against. The bare raise below
953
+ # records the wrapper as the block's __context__ instead (truthful: the block
954
+ # is re-raised while the wrapper is being handled), keeping both exceptions
955
+ # visible in tracebacks, acyclically.
956
+ failure.__cause__ = None
957
+ if failure.__context__ is block:
958
+ failure.__context__ = None
959
+ raise block
960
+ raise failure
919
961
 
920
962
 
921
963
  # endregion
@@ -1025,8 +1067,6 @@ class _AgentHooksAgentMiddleware(_AgentHooksMiddlewareBase, AgentMiddleware):
1025
1067
  # A middleware short-circuited the run; any substituted result
1026
1068
  # still egresses to the caller, so it passes the output point.
1027
1069
  termination = exc
1028
- if state.halted is not None:
1029
- raise state.halted
1030
1070
  result = context.result
1031
1071
  if isinstance(result, AgentResponse):
1032
1072
  await self._emit_output(state, result)
@@ -1046,6 +1086,14 @@ class _AgentHooksAgentMiddleware(_AgentHooksMiddlewareBase, AgentMiddleware):
1046
1086
  shutdown_reason = "error"
1047
1087
  context.result = None
1048
1088
  raise
1089
+ except MiddlewareFailure as failure:
1090
+ # A fail-closed abort from behind the function-invocation loop: the
1091
+ # gated persistence is dropped and, for a tool-seam host_error block,
1092
+ # the block itself is surfaced (one deny surface at every seam).
1093
+ gate.drop()
1094
+ shutdown_reason = "error"
1095
+ context.result = None
1096
+ _reraise_tool_seam_block(failure)
1049
1097
  except asyncio.CancelledError:
1050
1098
  shutdown_reason = "cancelled"
1051
1099
  raise
@@ -1093,10 +1141,6 @@ class _AgentHooksAgentMiddleware(_AgentHooksMiddlewareBase, AgentMiddleware):
1093
1141
  termination = exc
1094
1142
  inner = context.result
1095
1143
  if inner is None and termination is not None:
1096
- if state.halted is not None:
1097
- # The enforcement layer itself failed: strand the deferred
1098
- # persistence (fail-closed) and surface the halt.
1099
- raise state.halted
1100
1144
  # Terminated without a result: nothing will egress, so the no-egress
1101
1145
  # termination is a permitted outcome. Release the persistence the
1102
1146
  # drained in-pipeline work deferred — history of model calls that
@@ -1119,6 +1163,12 @@ class _AgentHooksAgentMiddleware(_AgentHooksMiddlewareBase, AgentMiddleware):
1119
1163
  # A middleware substituted its own stream: it is guarded, and the
1120
1164
  # termination still short-circuits the rest of the pipeline.
1121
1165
  raise termination
1166
+ except MiddlewareFailure as failure:
1167
+ # A fail-closed abort during the pipeline descent (e.g. a retry middleware
1168
+ # drained an attempt whose tool seam hit a host_error block): surface the
1169
+ # block itself for tool-seam blocks (one deny surface at every seam). The
1170
+ # deferred persistence is stranded un-flushed — fail-closed.
1171
+ _reraise_tool_seam_block(failure)
1122
1172
  except asyncio.CancelledError:
1123
1173
  shutdown_reason = "cancelled"
1124
1174
  raise
@@ -1151,6 +1201,14 @@ class _AgentHooksAgentMiddleware(_AgentHooksMiddlewareBase, AgentMiddleware):
1151
1201
  except asyncio.CancelledError:
1152
1202
  await self._emit_shutdown(state, "cancelled")
1153
1203
  raise
1204
+ except MiddlewareFailure as failure:
1205
+ # A fail-closed abort from behind the function-invocation loop: close
1206
+ # the trail and, for a tool-seam host_error block, surface the block
1207
+ # itself (one deny surface at every seam). The deferred persistence
1208
+ # is dropped — denied content never becomes durable.
1209
+ gate_handle.drop()
1210
+ await self._emit_shutdown(state, "error")
1211
+ _reraise_tool_seam_block(failure)
1154
1212
  except BaseException:
1155
1213
  await self._emit_shutdown(state, "error")
1156
1214
  raise
@@ -1164,8 +1222,6 @@ class _AgentHooksAgentMiddleware(_AgentHooksMiddlewareBase, AgentMiddleware):
1164
1222
  from agent_hooks import InterceptionBlocked
1165
1223
 
1166
1224
  try:
1167
- if state.halted is not None:
1168
- raise state.halted
1169
1225
  if not isinstance(final, AgentResponse):
1170
1226
  raise MiddlewareException(
1171
1227
  f"agent-hooks cannot guard a streamed run result of type {type(final).__name__}; "
@@ -1323,21 +1379,20 @@ class _AgentHooksChatMiddleware(_AgentHooksMiddlewareBase, ChatMiddleware):
1323
1379
  class _AgentHooksFunctionMiddleware(_AgentHooksMiddlewareBase, FunctionMiddleware):
1324
1380
  """Tool bracket: ``pre_tool_call`` and ``post_tool_call``."""
1325
1381
 
1326
- def _block(
1327
- self, state: _RunState, context: FunctionInvocationContext, exc: InterceptionBlocked, point: str
1328
- ) -> None:
1382
+ def _block(self, context: FunctionInvocationContext, exc: InterceptionBlocked, point: str) -> None:
1329
1383
  """Enforce a tool-seam deny: surface a tool error and, on host errors, halt the run."""
1330
1384
  context.result = _blocked_tool_result(point, exc.result)
1331
- self._maybe_halt(state, exc, point)
1385
+ self._maybe_halt(exc, point)
1332
1386
 
1333
- def _maybe_halt(self, state: _RunState, exc: InterceptionBlocked, point: str) -> None:
1387
+ def _maybe_halt(self, exc: InterceptionBlocked, point: str) -> None:
1334
1388
  record: InterceptionRecord = exc.result
1335
1389
  if _is_host_error(record):
1336
1390
  # The enforcement layer itself failed (interceptor crash/timeout, invalid
1337
- # context): continuing the loop would run unguarded. Halt the run; the
1338
- # agent middleware re-raises the block to the caller.
1339
- state.halted = exc
1340
- raise MiddlewareTermination(
1391
+ # context): continuing the loop would run unguarded. Abort the run through
1392
+ # the loop's fail-closed escape; the agent middleware re-raises the block
1393
+ # itself to the caller (the private tag is what authorizes the unwrap —
1394
+ # see _reraise_tool_seam_block).
1395
+ raise _ToolSeamBlockFailure(
1341
1396
  f"agent-hooks {point} failed closed: {record.verdict.reason}",
1342
1397
  ) from exc
1343
1398
 
@@ -1346,21 +1401,15 @@ class _AgentHooksFunctionMiddleware(_AgentHooksMiddlewareBase, FunctionMiddlewar
1346
1401
 
1347
1402
  state = _RUN_STATE.get()
1348
1403
  if state is None:
1349
- # No run state means the bundle's agent middleware never ran. A plain
1350
- # exception raised here would be converted into a tool error by the
1351
- # function-invocation loop and the run would continue unguarded (fail
1352
- # open); MiddlewareTermination is the only loud escape: the tool is never
1353
- # dispatched and the loop stops.
1354
- message = _TRIO_REQUIRED_MESSAGE.format(seam="function")
1355
- context.result = {"error": message}
1356
- raise MiddlewareTermination(message)
1404
+ # No run state means the bundle's agent middleware never ran (a partial
1405
+ # install the public API makes impossible). The tool must never be
1406
+ # dispatched: abort the run through the loop's fail-closed escape.
1407
+ raise MiddlewareFailure(_TRIO_REQUIRED_MESSAGE.format(seam="function"))
1357
1408
  if not self._shares_config(state.config):
1358
1409
  # A different bundle owns the innermost run state (stacked bundles):
1359
- # binding to it would silently misroute emissions. Halt the run
1360
- # fail-closed (the loop swallows plain exceptions, so route through the
1361
- # halt path).
1410
+ # binding to it would silently misroute emissions. Abort fail-closed.
1362
1411
  _halt_on_enforcement_failure(
1363
- state, context, MiddlewareException(_FOREIGN_TRIO_MESSAGE.format(seam="function")), "pre_tool_call"
1412
+ MiddlewareException(_FOREIGN_TRIO_MESSAGE.format(seam="function")), "pre_tool_call"
1364
1413
  )
1365
1414
 
1366
1415
  try:
@@ -1379,23 +1428,20 @@ class _AgentHooksFunctionMiddleware(_AgentHooksMiddlewareBase, FunctionMiddlewar
1379
1428
  args = effective
1380
1429
  except InterceptionBlocked as exc:
1381
1430
  # §6.2: the tool is not dispatched and no post_tool_call is emitted.
1382
- self._block(state, context, exc, "pre_tool_call")
1431
+ self._block(context, exc, "pre_tool_call")
1383
1432
  return
1384
- except (MiddlewareTermination, asyncio.CancelledError):
1433
+ except (MiddlewareTermination, MiddlewareFailure, asyncio.CancelledError):
1385
1434
  raise
1386
1435
  except BaseException as exc:
1387
- _halt_on_enforcement_failure(state, context, exc, "pre_tool_call")
1436
+ _halt_on_enforcement_failure(exc, "pre_tool_call")
1388
1437
 
1389
1438
  termination: MiddlewareTermination | None = None
1390
1439
  try:
1391
1440
  await call_next()
1392
1441
  except MiddlewareTermination as exc:
1393
- if state.halted is not None or _is_approval_request(context.result):
1394
- # Our own halt path, or framework approval control flow (the tool did
1395
- # not run; an approved replay re-enters through pre_tool_call).
1396
- raise
1397
1442
  # A middleware short-circuited with a substituted result; that result
1398
- # still enters the transcript, so it is bracketed below.
1443
+ # still enters the transcript, so it is bracketed below (approval-request
1444
+ # control flow is passed through un-emitted further down instead).
1399
1445
  termination = exc
1400
1446
  except asyncio.CancelledError:
1401
1447
  raise
@@ -1411,19 +1457,21 @@ class _AgentHooksFunctionMiddleware(_AgentHooksMiddlewareBase, FunctionMiddlewar
1411
1457
  except InterceptionBlocked as blocked:
1412
1458
  # A policy deny over an already-errored call changes nothing (the
1413
1459
  # result is discarded either way); a host error still halts the run.
1414
- self._maybe_halt(state, blocked, "post_tool_call")
1415
- except asyncio.CancelledError:
1460
+ self._maybe_halt(blocked, "post_tool_call")
1461
+ except (MiddlewareTermination, MiddlewareFailure, asyncio.CancelledError):
1416
1462
  raise
1417
1463
  except BaseException as emit_exc:
1418
- _halt_on_enforcement_failure(state, context, emit_exc, "post_tool_call")
1464
+ _halt_on_enforcement_failure(emit_exc, "post_tool_call")
1419
1465
  raise
1420
1466
 
1421
1467
  if _is_approval_request(context.result):
1422
- # Framework approval control flow on the normal return path (a middleware
1423
- # set an approval request and returned): the tool has not run, so there is
1424
- # no result to bracket — pass the control object through un-emitted, exactly
1425
- # like the termination branch above. The approved replay re-enters through
1426
- # pre_tool_call.
1468
+ # Framework approval control flow (a middleware set an approval request,
1469
+ # whether it returned or short-circuited): the tool has not run, so there
1470
+ # is no result to bracket — pass the control object through un-emitted.
1471
+ # The approved replay re-enters through pre_tool_call. A short-circuit is
1472
+ # re-raised so the loop still stops and surfaces the request.
1473
+ if termination is not None:
1474
+ raise termination
1427
1475
  return
1428
1476
 
1429
1477
  try:
@@ -1434,11 +1482,11 @@ class _AgentHooksFunctionMiddleware(_AgentHooksMiddlewareBase, FunctionMiddlewar
1434
1482
  context.result = _ToolResultCodec.write_back(context.result, value, outcome.target)
1435
1483
  except InterceptionBlocked as exc:
1436
1484
  # §6.1: the result must be discarded as if the call had errored.
1437
- self._block(state, context, exc, "post_tool_call")
1438
- except (MiddlewareTermination, asyncio.CancelledError):
1485
+ self._block(context, exc, "post_tool_call")
1486
+ except (MiddlewareTermination, MiddlewareFailure, asyncio.CancelledError):
1439
1487
  raise
1440
1488
  except BaseException as exc:
1441
- _halt_on_enforcement_failure(state, context, exc, "post_tool_call")
1489
+ _halt_on_enforcement_failure(exc, "post_tool_call")
1442
1490
  if termination is not None:
1443
1491
  raise termination
1444
1492
 
@@ -56,6 +56,7 @@ from ._types import (
56
56
  ChatResponseUpdate,
57
57
  Message,
58
58
  ResponseStream,
59
+ _append_instructions, # pyright: ignore[reportPrivateUsage]
59
60
  _build_agent_response_from_chat_response, # pyright: ignore[reportPrivateUsage]
60
61
  map_chat_to_agent_update,
61
62
  normalize_messages,
@@ -84,6 +85,15 @@ if TYPE_CHECKING:
84
85
 
85
86
  logger = logging.getLogger("agent_framework")
86
87
 
88
+ # AgentLoopMiddleware stamps this key into the run options while a loop
89
+ # iteration is running, so providers scoped to the whole user turn
90
+ # (``after_run_once_per_turn``) skip their per-iteration ``after_run`` and only
91
+ # fire once at the loop boundary. It rides the run's options rather than a
92
+ # context variable: options reach only the runs the loop itself drives, so a
93
+ # nested ``agent.run()`` (fresh options, its own session) keeps its own turn,
94
+ # and nothing leaks into the caller's context while a stream is paused.
95
+ _LOOP_ITERATION_TOKEN_KEY = "_agent_loop_iteration" # nosec B105 - a context-options key, not a credential # ruff: ignore[hardcoded-password-string]
96
+
87
97
  if TYPE_CHECKING:
88
98
  ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel)
89
99
  else:
@@ -159,8 +169,8 @@ def _merge_options(base: dict[str, Any], override: dict[str, Any]) -> dict[str,
159
169
  # Merge metadata dicts
160
170
  result["metadata"] = {**result["metadata"], **value}
161
171
  elif key == "instructions" and result.get("instructions"):
162
- # Concatenate instructions
163
- result["instructions"] = f"{result['instructions']}\n{value}"
172
+ # Concatenate instructions, preserving provider-native structured values
173
+ result["instructions"] = _append_instructions(result["instructions"], value)
164
174
  else:
165
175
  result[key] = value
166
176
  return {key: value for key, value in result.items() if value is not None}
@@ -545,6 +555,7 @@ class BaseAgent(SerializationMixin):
545
555
  *,
546
556
  session: AgentSession | None,
547
557
  context: SessionContext,
558
+ only_per_turn: bool = False,
548
559
  ) -> None:
549
560
  """Run after_run on all context providers in reverse order.
550
561
 
@@ -557,6 +568,10 @@ class BaseAgent(SerializationMixin):
557
568
  Keyword Args:
558
569
  session: The conversation session.
559
570
  context: The invocation context with response populated.
571
+ only_per_turn: When True, run only providers that opted into
572
+ once-per-turn semantics (``after_run_once_per_turn``); used by
573
+ AgentLoopMiddleware when a loop ends. When False, those
574
+ providers are skipped while a loop iteration is in progress.
560
575
  """
561
576
  if _defer_run_persistence(partial(self._run_after_providers, session=session, context=context)):
562
577
  return
@@ -570,9 +585,17 @@ class BaseAgent(SerializationMixin):
570
585
  per_service_call_history_required = self.require_per_service_call_history_persistence and any(
571
586
  isinstance(provider, HistoryProvider) for provider in self.context_providers
572
587
  )
588
+ # The loop stamps the runs it drives via their options; anything else
589
+ # (nested run, caller-side run while a stream is paused) is its own turn.
590
+ in_loop_iteration = context.options.get(_LOOP_ITERATION_TOKEN_KEY) is not None
573
591
  for provider in reversed(self.context_providers):
574
592
  if per_service_call_history_required and isinstance(provider, HistoryProvider):
575
593
  continue
594
+ once_per_turn = getattr(provider, "after_run_once_per_turn", False)
595
+ if only_per_turn and not once_per_turn:
596
+ continue
597
+ if in_loop_iteration and once_per_turn:
598
+ continue
576
599
  if provider_session is None:
577
600
  raise RuntimeError("Provider session must be available when context providers are configured.")
578
601
  await provider.after_run(
@@ -1489,6 +1512,9 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]):
1489
1512
  # _merge_options strips unset (None) options, so e.g. an unset `store` is not forwarded
1490
1513
  # and the service decides its own default.
1491
1514
  co = _merge_options(chat_options, run_opts)
1515
+ # The loop marker must remain on SessionContext.options for after_run provider
1516
+ # scoping, but it is framework-private metadata and must not reach the client.
1517
+ co.pop(_LOOP_ITERATION_TOKEN_KEY, None)
1492
1518
 
1493
1519
  # Build session_messages from session context: context messages + input messages
1494
1520
  session_messages: list[Message] = session_context.get_messages(include_input=True)
@@ -1623,10 +1649,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]):
1623
1649
  # Merge provider-contributed instructions into chat_options
1624
1650
  if session_context.instructions:
1625
1651
  combined_instructions = "\n".join(session_context.instructions)
1626
- if "instructions" in chat_options:
1627
- chat_options["instructions"] = f"{chat_options['instructions']}\n{combined_instructions}"
1628
- else:
1629
- chat_options["instructions"] = combined_instructions
1652
+ chat_options["instructions"] = _append_instructions(chat_options.get("instructions"), combined_instructions)
1630
1653
 
1631
1654
  return session_context, chat_options
1632
1655
 
@@ -1529,6 +1529,10 @@ class CompactionProvider(ContextProvider):
1529
1529
  await agent.run("Hello", session=session)
1530
1530
  """
1531
1531
 
1532
+ # Compacting persisted history mid-task rewrites the transcript the loop
1533
+ # still works from, so defer it to the end of the user turn.
1534
+ after_run_once_per_turn = True
1535
+
1532
1536
  def __init__(
1533
1537
  self,
1534
1538
  *,
@@ -797,8 +797,7 @@ class AgentEvalConverter:
797
797
  "tool_call_id": c.call_id or "",
798
798
  "name": c.name or "",
799
799
  }
800
- if args:
801
- tc["arguments"] = args
800
+ tc["arguments"] = args if args is not None else {}
802
801
  content_items.append(tc)
803
802
  elif c.type == "function_result":
804
803
  result_val = c.result
@@ -24,7 +24,7 @@ from .._sessions import ContextProvider, HistoryProvider, InMemoryHistoryProvide
24
24
  from .._skills import SkillsProvider
25
25
  from .._telemetry import FeatureIndex, mark_feature_used
26
26
  from .._types import ChatOptions
27
- from ._background_agents import BackgroundAgentsProvider
27
+ from ._background_agents import DEFAULT_BACKGROUND_AGENTS_WAIT_TIMEOUT_SECONDS, BackgroundAgentsProvider
28
28
  from ._file_access import AgentFileStore, FileAccessProvider, FileSystemAgentFileStore
29
29
  from ._file_memory import FileMemoryProvider
30
30
  from ._loop import DEFAULT_MAX_ITERATIONS, AgentLoopMiddleware
@@ -160,6 +160,7 @@ def _assemble_context_providers(
160
160
  skills_paths: str | Path | Sequence[str | Path] | None,
161
161
  background_agents: Sequence[SupportsAgentRun] | None,
162
162
  background_agents_instructions: str | None,
163
+ background_agents_wait_timeout_seconds: int,
163
164
  shell_context_provider: ContextProvider | None,
164
165
  extra_context_providers: Sequence[ContextProvider] | None,
165
166
  ) -> list[ContextProvider]:
@@ -205,7 +206,13 @@ def _assemble_context_providers(
205
206
 
206
207
  # Background agents are opt-in: only added when agents are provided.
207
208
  if background_agents:
208
- providers.append(BackgroundAgentsProvider(background_agents, instructions=background_agents_instructions))
209
+ providers.append(
210
+ BackgroundAgentsProvider(
211
+ background_agents,
212
+ instructions=background_agents_instructions,
213
+ wait_timeout_seconds=background_agents_wait_timeout_seconds,
214
+ )
215
+ )
209
216
 
210
217
  # Shell environment provider is opt-in: only added when a shell tool was wired.
211
218
  if shell_context_provider is not None:
@@ -329,6 +336,7 @@ def create_harness_agent(
329
336
  skills_paths: str | Path | Sequence[str | Path] | None = None,
330
337
  background_agents: Sequence[SupportsAgentRun] | None = None,
331
338
  background_agents_instructions: str | None = None,
339
+ background_agents_wait_timeout_seconds: int = DEFAULT_BACKGROUND_AGENTS_WAIT_TIMEOUT_SECONDS,
332
340
  shell_executor: ShellExecutor | None = None,
333
341
  shell_environment_provider_options: ShellEnvironmentProviderOptions | None = None,
334
342
  disable_web_search: bool = False,
@@ -478,6 +486,10 @@ def create_harness_agent(
478
486
  background_agents_instructions: Optional instruction override for the
479
487
  ``BackgroundAgentsProvider``. May include ``{background_agents}`` placeholder
480
488
  which will be replaced with the agent listing.
489
+ background_agents_wait_timeout_seconds: Maximum seconds the background-agent wait
490
+ tool blocks for a task to complete. Must be a positive integer.
491
+ Defaults to 300 seconds. On expiry, the tool returns normally and leaves the
492
+ background tasks running. Only used when ``background_agents`` is provided.
481
493
  shell_executor: Optional shell tool that enables shell command execution. When
482
494
  provided, the shell tool and a ``ShellEnvironmentProvider`` are automatically
483
495
  added (provided the client supports shell tools; otherwise a warning is logged
@@ -525,7 +537,8 @@ def create_harness_agent(
525
537
  Raises:
526
538
  ValueError: If max_context_window_tokens is provided and <= 0, or
527
539
  max_output_tokens is provided and <= 0, or max_output_tokens >=
528
- max_context_window_tokens when both are provided.
540
+ max_context_window_tokens when both are provided, or
541
+ background_agents_wait_timeout_seconds is invalid when background agents are provided.
529
542
  """
530
543
  if max_context_window_tokens is not None and max_context_window_tokens <= 0:
531
544
  raise ValueError("max_context_window_tokens must be positive.")
@@ -601,6 +614,7 @@ def create_harness_agent(
601
614
  skills_paths=skills_paths,
602
615
  background_agents=background_agents,
603
616
  background_agents_instructions=background_agents_instructions,
617
+ background_agents_wait_timeout_seconds=background_agents_wait_timeout_seconds,
604
618
  shell_context_provider=shell_provider,
605
619
  extra_context_providers=context_providers,
606
620
  )
@@ -14,6 +14,7 @@ from .._sessions import ContextProvider, HistoryProvider
14
14
  from .._skills import SkillsProvider
15
15
  from .._tools import ToolTypes
16
16
  from .._types import ChatOptions
17
+ from ._background_agents import DEFAULT_BACKGROUND_AGENTS_WAIT_TIMEOUT_SECONDS
17
18
  from ._file_access import AgentFileStore
18
19
  from ._loop import DEFAULT_MAX_ITERATIONS, NextMessageCallable, ShouldContinueCallable
19
20
  from ._mode import AgentModeProvider
@@ -77,6 +78,7 @@ def create_harness_agent(
77
78
  skills_paths: str | Path | Sequence[str | Path] | None = None,
78
79
  background_agents: Sequence[SupportsAgentRun] | None = None,
79
80
  background_agents_instructions: str | None = None,
81
+ background_agents_wait_timeout_seconds: int = DEFAULT_BACKGROUND_AGENTS_WAIT_TIMEOUT_SECONDS,
80
82
  shell_executor: _ShellExecutorLike | None = None,
81
83
  shell_environment_provider_options: _ShellEnvironmentProviderOptionsLike | None = None,
82
84
  disable_web_search: bool = False,