agent-framework-core 1.16.0__tar.gz → 1.17.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.16.0 → agent_framework_core-1.17.0}/PKG-INFO +1 -3
  2. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_agent_hooks.py +92 -16
  3. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_agents.py +19 -19
  4. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_clients.py +5 -1
  5. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_compaction.py +183 -34
  6. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_agent.py +4 -6
  7. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_agent.pyi +1 -1
  8. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_mcp.py +83 -21
  9. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_middleware.py +184 -26
  10. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_tools.py +140 -22
  11. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_types.py +65 -3
  12. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_agent.py +69 -18
  13. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_agent_executor.py +61 -28
  14. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_executor.py +4 -0
  15. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_request_info_mixin.py +369 -369
  16. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_runner_context.py +38 -0
  17. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_workflow.py +95 -7
  18. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_workflow_context.py +5 -0
  19. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_workflow_executor.py +12 -1
  20. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/observability.py +4 -4
  21. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/security.py +34 -19
  22. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/pyproject.toml +1 -6
  23. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/LICENSE +0 -0
  24. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/README.md +0 -0
  25. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/__init__.py +0 -0
  26. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/__init__.pyi +0 -0
  27. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_docstrings.py +0 -0
  28. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_evaluation.py +0 -0
  29. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_feature_stage.py +0 -0
  30. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_filesystem.py +0 -0
  31. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/__init__.py +0 -0
  32. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_background_agents.py +0 -0
  33. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_file_access.py +0 -0
  34. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_file_memory.py +0 -0
  35. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_loop.py +0 -0
  36. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_memory.py +0 -0
  37. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_mode.py +0 -0
  38. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_todo.py +0 -0
  39. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_harness/_tool_approval.py +0 -0
  40. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_serialization.py +0 -0
  41. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_sessions.py +0 -0
  42. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_settings.py +0 -0
  43. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_skills.py +0 -0
  44. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_telemetry.py +0 -0
  45. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/__init__.py +0 -0
  46. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_agent_utils.py +0 -0
  47. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_checkpoint.py +0 -0
  48. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_checkpoint_encoding.py +0 -0
  49. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_const.py +0 -0
  50. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_conversation_history.py +0 -0
  51. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_edge.py +0 -0
  52. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_edge_runner.py +0 -0
  53. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_events.py +0 -0
  54. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_function_executor.py +0 -0
  55. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_functional.py +0 -0
  56. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_message_utils.py +0 -0
  57. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_model_utils.py +0 -0
  58. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_runner.py +0 -0
  59. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_state.py +0 -0
  60. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_typing_utils.py +0 -0
  61. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_validation.py +0 -0
  62. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_viz.py +0 -0
  63. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/_workflows/_workflow_builder.py +0 -0
  64. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/a2a/__init__.py +0 -0
  65. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/a2a/__init__.pyi +0 -0
  66. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/ag_ui/__init__.py +0 -0
  67. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/ag_ui/__init__.pyi +0 -0
  68. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/amazon/__init__.py +0 -0
  69. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/amazon/__init__.pyi +0 -0
  70. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/anthropic/__init__.py +0 -0
  71. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/anthropic/__init__.pyi +0 -0
  72. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/azure/__init__.py +0 -0
  73. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/azure/__init__.pyi +0 -0
  74. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/chatkit/__init__.py +0 -0
  75. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/chatkit/__init__.pyi +0 -0
  76. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/declarative/__init__.py +0 -0
  77. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/declarative/__init__.pyi +0 -0
  78. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/devui/__init__.py +0 -0
  79. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/devui/__init__.pyi +0 -0
  80. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/exceptions.py +0 -0
  81. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/foundry/__init__.py +0 -0
  82. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/foundry/__init__.pyi +0 -0
  83. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/gemini/__init__.py +0 -0
  84. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/gemini/__init__.pyi +0 -0
  85. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/github/__init__.py +0 -0
  86. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/github/__init__.pyi +0 -0
  87. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/google/__init__.py +0 -0
  88. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/google/__init__.pyi +0 -0
  89. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/hyperlight/__init__.py +0 -0
  90. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/lab/__init__.py +0 -0
  91. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/mem0/__init__.py +0 -0
  92. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/mem0/__init__.pyi +0 -0
  93. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/microsoft/__init__.py +0 -0
  94. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/microsoft/__init__.pyi +0 -0
  95. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/mistral/__init__.py +0 -0
  96. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/mistral/__init__.pyi +0 -0
  97. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/monty/__init__.py +0 -0
  98. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/monty/__init__.pyi +0 -0
  99. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/ollama/__init__.py +0 -0
  100. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/ollama/__init__.pyi +0 -0
  101. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/openai/__init__.py +0 -0
  102. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/openai/__init__.pyi +0 -0
  103. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/orchestrations/__init__.py +0 -0
  104. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/orchestrations/__init__.pyi +0 -0
  105. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/py.typed +0 -0
  106. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/redis/__init__.py +0 -0
  107. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/redis/__init__.pyi +0 -0
  108. {agent_framework_core-1.16.0 → agent_framework_core-1.17.0}/agent_framework/tools/__init__.py +0 -0
  109. {agent_framework_core-1.16.0 → agent_framework_core-1.17.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.16.0
3
+ Version: 1.17.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
@@ -21,7 +21,6 @@ Requires-Dist: typing-extensions>=4.15.0,<5
21
21
  Requires-Dist: pydantic>=2,<3
22
22
  Requires-Dist: python-dotenv>=1,<2
23
23
  Requires-Dist: opentelemetry-api>=1.39.0,<2
24
- Requires-Dist: agent-hooks-sdk>=0.1.0a4,<0.2 ; extra == "agent-hooks"
25
24
  Requires-Dist: mcp>=1.24.0,<2 ; extra == "all"
26
25
  Requires-Dist: agent-framework-a2a ; extra == "all"
27
26
  Requires-Dist: agent-framework-ag-ui ; extra == "all"
@@ -57,7 +56,6 @@ Project-URL: homepage, https://aka.ms/agent-framework
57
56
  Project-URL: issues, https://github.com/microsoft/agent-framework/issues
58
57
  Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
59
58
  Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
60
- Provides-Extra: agent-hooks
61
59
  Provides-Extra: all
62
60
 
63
61
  # Get Started with Microsoft Agent Framework
@@ -93,7 +93,7 @@ per-run points and the host owns the session boundaries.
93
93
 
94
94
  The ``agent-hooks-sdk`` dependency is optional: importing this module (and the lazy root
95
95
  exports) works without it, and the factories raise a descriptive ``ModuleNotFoundError``
96
- when the SDK is missing. Install it via ``pip install agent-framework-core[agent-hooks]``.
96
+ when the SDK is missing. Install it via ``pip install agent-hooks-sdk``.
97
97
  """
98
98
 
99
99
  from __future__ import annotations
@@ -162,7 +162,7 @@ _DEFAULT_TIMEOUT = 5.0
162
162
 
163
163
  _SDK_MISSING_MESSAGE = (
164
164
  "The agent-hooks middleware requires the optional `agent-hooks-sdk` package. "
165
- "Please install `agent-framework-core[agent-hooks]` (or `agent-hooks-sdk`)."
165
+ "Please install it with `pip install agent-hooks-sdk`."
166
166
  )
167
167
 
168
168
  _TRIO_REQUIRED_MESSAGE = (
@@ -191,7 +191,7 @@ def _require_sdk() -> None:
191
191
  """Import the SDK surface this module uses at runtime, with a helpful install hint.
192
192
 
193
193
  Only a genuinely missing ``agent_hooks`` package is translated into the
194
- install-the-extra message; anything else (a broken installation, an incompatible
194
+ SDK installation message; anything else (a broken installation, an incompatible
195
195
  SDK version missing symbols, a failing transitive import) propagates unchanged so
196
196
  real breakage is not masked as a missing extra.
197
197
  """
@@ -862,23 +862,99 @@ def _agent_updates_from_response(response: AgentResponse[Any]) -> list[AgentResp
862
862
  return updates
863
863
 
864
864
 
865
- def _tool_names(context: AgentContext) -> list[str]:
866
- """Project the registered tool names for ``agent_startup`` (spec ``tools_registered``)."""
867
- from ._tools import _get_tool_name, normalize_tools # type: ignore[reportPrivateUsage]
865
+ def _normalized_tools(tools: Any, *, point: str) -> list[Any]:
866
+ """Normalize a tools value for projection; empty (with a warning) when it cannot be.
867
+
868
+ Everything — including the emptiness check inside ``normalize_tools`` — runs under
869
+ the guard, so a tools container whose ``__bool__``/``__len__``/``__iter__`` raises
870
+ degrades to omitting the projection instead of aborting the emission mid-run.
871
+ """
872
+ from ._tools import normalize_tools
868
873
 
869
- tools: Any = context.tools if context.tools is not None else getattr(context.agent, "tools", None)
870
- if tools is None:
874
+ try:
875
+ return list(normalize_tools(tools))
876
+ except Exception:
877
+ logger.warning("agent-hooks could not normalize the tools for the %s projection.", point)
871
878
  return []
879
+
880
+
881
+ def _projected_tool_name(item: Any) -> str:
882
+ """Project a tool's display name; hosted-tool mappings fall back to their ``type``."""
883
+ from ._tools import _get_tool_name # type: ignore[reportPrivateUsage]
884
+
885
+ fallback = type(item).__name__
886
+ name = _get_tool_name(item)
887
+ if name:
888
+ return name
889
+ if isinstance(item, Mapping):
890
+ # Hosted-tool mappings carry top-level fields (no nested "function"), e.g.
891
+ # {"type": "web_search"} or {"type": "web_search_20250305", "name": "web_search"}.
892
+ mapping = cast("Mapping[str, Any]", item)
893
+ for key in ("name", "type"):
894
+ value = mapping.get(key)
895
+ if isinstance(value, str) and value:
896
+ return value
897
+ return fallback
898
+
899
+
900
+ def _tool_description(item: Any) -> str | None:
901
+ """Extract a tool description from a tool object or dict tool definition."""
902
+ if isinstance(item, Mapping):
903
+ mapping = cast("Mapping[str, Any]", item)
904
+ function = mapping.get("function")
905
+ # Function-tool dicts nest the description; hosted-tool mappings keep it top-level.
906
+ description = (
907
+ cast("Mapping[str, Any]", function).get("description")
908
+ if isinstance(function, Mapping)
909
+ else mapping.get("description")
910
+ )
911
+ return description if isinstance(description, str) else None
912
+ description = getattr(item, "description", None)
913
+ return description if isinstance(description, str) else None
914
+
915
+
916
+ def _tool_names(context: AgentContext) -> list[str]:
917
+ """Project the run-start tool names for ``agent_startup`` (spec ``tools_registered``).
918
+
919
+ The framework owns the run-start resolution — ``AgentContext._resolve_run_start_tools``
920
+ returns the agent-declared tools plus this invocation's run-level tools (the named
921
+ ``tools`` parameter takes precedence over a ``tools`` entry in the options mapping),
922
+ already normalized — so this helper is projection only. This is deliberately the
923
+ run-start snapshot: tools registered dynamically during the run (for example by
924
+ context providers during run preparation, or by MCP servers whose functions expand
925
+ at connect time) cannot be known at ``agent_startup`` time; they surface in each
926
+ ``pre_model_call`` emission's ``tools`` projection (the completed per-call set) and
927
+ are bracketed by ``pre_tool_call``/``post_tool_call`` like any other tool when
928
+ invoked. An unresolvable set degrades to a warning and an empty snapshot rather
929
+ than aborting the emission.
930
+ """
872
931
  try:
873
- normalized = normalize_tools(tools)
932
+ resolved = context._resolve_run_start_tools() # pyright: ignore[reportPrivateUsage]
874
933
  except Exception:
875
- logger.warning("agent-hooks could not normalize the run's tools for the agent_startup projection.")
934
+ logger.warning("agent-hooks could not resolve the run-start tools for the agent_startup projection.")
876
935
  return []
877
- names: list[str] = []
878
- for item in normalized:
879
- name = _get_tool_name(item)
880
- names.append(name if name else type(item).__name__)
881
- return names
936
+ return [_projected_tool_name(item) for item in resolved]
937
+
938
+
939
+ def _pre_model_call_tools(options: Mapping[str, Any]) -> list[dict[str, Any]] | None:
940
+ """Project the per-call effective tool set for ``pre_model_call`` (spec ``tools``).
941
+
942
+ This is the completed set for this model call — the run-start tools plus anything
943
+ registered during the run (context-provider tools, connected MCP-server functions,
944
+ progressive tool exposure) — whereas ``agent_startup``'s ``tools_registered`` is the
945
+ run-start snapshot. Entries carry ``{"name", "description"?}`` (description omitted
946
+ when the tool has none). Returns ``None`` — the spec's optional field is omitted —
947
+ when the call offers no tools or the set cannot be projected, so an unprojectable
948
+ set is never misreported as "no tools".
949
+ """
950
+ projected: list[dict[str, Any]] = []
951
+ for item in _normalized_tools(options.get("tools"), point="pre_model_call"):
952
+ entry: dict[str, Any] = {"name": _projected_tool_name(item)}
953
+ description = _tool_description(item)
954
+ if description:
955
+ entry["description"] = description
956
+ projected.append(entry)
957
+ return projected or None
882
958
 
883
959
 
884
960
  def _is_host_error(record: InterceptionRecord) -> bool:
@@ -1269,7 +1345,7 @@ class _AgentHooksChatMiddleware(_AgentHooksMiddlewareBase, ChatMiddleware):
1269
1345
  model_id = str(options.get("model") or type(context.client).__name__)
1270
1346
  before = _ModelRequestCodec.to_wire(context.messages)
1271
1347
  outcome: EmitOutcome = await state.emitter.emit(
1272
- state.builder.pre_model_call(model_id=model_id, messages=before)
1348
+ state.builder.pre_model_call(model_id=model_id, messages=before, tools=_pre_model_call_tools(options))
1273
1349
  )
1274
1350
  transformed_messages = _ModelRequestCodec.write_back(context.messages, before, outcome.target)
1275
1351
  if transformed_messages is not None:
@@ -31,6 +31,8 @@ from ._middleware import (
31
31
  FunctionInvocationContext,
32
32
  MiddlewareTypes,
33
33
  _as_middleware_list, # pyright: ignore[reportPrivateUsage]
34
+ _copy_middleware_sequence, # pyright: ignore[reportPrivateUsage]
35
+ _select_run_level_tools, # pyright: ignore[reportPrivateUsage]
34
36
  categorize_middleware,
35
37
  )
36
38
  from ._serialization import SerializationMixin
@@ -439,7 +441,7 @@ class BaseAgent(SerializationMixin):
439
441
  name: str | None = None,
440
442
  description: str | None = None,
441
443
  context_providers: Sequence[ContextProvider] | None = None,
442
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
444
+ middleware: Sequence[MiddlewareTypes] | None = None,
443
445
  additional_properties: MutableMapping[str, Any] | None = None,
444
446
  ) -> None:
445
447
  """Initialize a BaseAgent instance.
@@ -450,10 +452,8 @@ class BaseAgent(SerializationMixin):
450
452
  name: The name of the agent, can be None.
451
453
  description: The description of the agent.
452
454
  context_providers: Context providers to include during agent invocation.
453
- middleware: List of middleware, or a single middleware object (including a
454
- ``MiddlewareBundle``) which is treated as a one-element list. The
455
- constructor copies the sequence; assign to or mutate the
456
- ``middleware`` attribute for post-construction changes.
455
+ middleware: List of middleware. The constructor copies the sequence; assign
456
+ to or mutate the ``middleware`` attribute for post-construction changes.
457
457
  additional_properties: Additional properties set on the agent.
458
458
  """
459
459
  if id is None:
@@ -462,11 +462,8 @@ class BaseAgent(SerializationMixin):
462
462
  self.name = name
463
463
  self.description = description
464
464
  self.context_providers: list[ContextProvider] = list(context_providers or [])
465
- # Canonicalize storage: the bare-source rule (a single middleware object or a
466
- # MiddlewareBundle is one element) is owned by _as_middleware_list; storing a
467
- # normalized list keeps the declared attribute type honest.
468
465
  self.middleware: list[MiddlewareTypes] | None = (
469
- _as_middleware_list(middleware) if middleware is not None else None
466
+ _copy_middleware_sequence(middleware) if middleware is not None else None
470
467
  )
471
468
  self.additional_properties: dict[str, Any] = cast(dict[str, Any], additional_properties or {})
472
469
 
@@ -819,7 +816,7 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]):
819
816
  tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
820
817
  default_options: OptionsCoT | None = None,
821
818
  context_providers: Sequence[ContextProvider] | None = None,
822
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
819
+ middleware: Sequence[MiddlewareTypes] | None = None,
823
820
  require_per_service_call_history_persistence: bool = False,
824
821
  compaction_strategy: CompactionStrategy | None = None,
825
822
  tokenizer: TokenizerProtocol | None = None,
@@ -838,8 +835,6 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]):
838
835
  description: A brief description of the agent's purpose.
839
836
  context_providers: Context providers to include during agent invocation.
840
837
  middleware: List of middleware to intercept agent and function invocations.
841
- A single middleware object (including a ``MiddlewareBundle``) is
842
- treated as a one-element list.
843
838
  require_per_service_call_history_persistence: When True (and a HistoryProvider is
844
839
  present), the provider always persists history via per-service-call middleware,
845
840
  regardless of whether the client stores history server-side. If the client does
@@ -1365,8 +1360,13 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]):
1365
1360
  opts = dict(options) if options else {}
1366
1361
  existing_additional_args: dict[str, Any] = opts.pop("additional_function_arguments", None) or {}
1367
1362
 
1368
- # Get tools from options or named parameter (named param takes precedence)
1369
- tools_ = tools if tools is not None else opts.pop("tools", None)
1363
+ # Run-level tools: the named parameter takes precedence over an options entry
1364
+ # (_select_run_level_tools is the framework's single statement of that rule,
1365
+ # shared with the middleware layer's run-start resolution). The options entry
1366
+ # is consumed either way, so a losing options["tools"] can never ride the
1367
+ # remaining options into the request and silently override the resolved list.
1368
+ tools_ = _select_run_level_tools(tools, opts)
1369
+ opts.pop("tools", None)
1370
1370
 
1371
1371
  input_messages = normalize_messages(messages)
1372
1372
 
@@ -1808,7 +1808,7 @@ class Agent(
1808
1808
  *,
1809
1809
  stream: Literal[False] = ...,
1810
1810
  session: AgentSession | None = None,
1811
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1811
+ middleware: Sequence[MiddlewareTypes] | None = None,
1812
1812
  tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
1813
1813
  options: ChatOptions[ResponseModelBoundT],
1814
1814
  compaction_strategy: CompactionStrategy | None = None,
@@ -1824,7 +1824,7 @@ class Agent(
1824
1824
  *,
1825
1825
  stream: Literal[False] = ...,
1826
1826
  session: AgentSession | None = None,
1827
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1827
+ middleware: Sequence[MiddlewareTypes] | None = None,
1828
1828
  tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
1829
1829
  options: OptionsCoT | ChatOptions[None] | None = None,
1830
1830
  compaction_strategy: CompactionStrategy | None = None,
@@ -1840,7 +1840,7 @@ class Agent(
1840
1840
  *,
1841
1841
  stream: Literal[True],
1842
1842
  session: AgentSession | None = None,
1843
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1843
+ middleware: Sequence[MiddlewareTypes] | None = None,
1844
1844
  tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
1845
1845
  options: OptionsCoT | ChatOptions[Any] | None = None,
1846
1846
  compaction_strategy: CompactionStrategy | None = None,
@@ -1855,7 +1855,7 @@ class Agent(
1855
1855
  *,
1856
1856
  stream: bool = False,
1857
1857
  session: AgentSession | None = None,
1858
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1858
+ middleware: Sequence[MiddlewareTypes] | None = None,
1859
1859
  tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
1860
1860
  options: OptionsCoT | ChatOptions[Any] | None = None,
1861
1861
  compaction_strategy: CompactionStrategy | None = None,
@@ -1893,7 +1893,7 @@ class Agent(
1893
1893
  tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
1894
1894
  default_options: OptionsCoT | None = None,
1895
1895
  context_providers: Sequence[ContextProvider] | None = None,
1896
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
1896
+ middleware: Sequence[MiddlewareTypes] | None = None,
1897
1897
  require_per_service_call_history_persistence: bool = False,
1898
1898
  compaction_strategy: CompactionStrategy | None = None,
1899
1899
  tokenizer: TokenizerProtocol | None = None,
@@ -96,6 +96,10 @@ class SupportsChatGetResponse(Protocol[OptionsContraT]):
96
96
  Protocols use structural subtyping (duck typing). Classes don't need
97
97
  to explicitly inherit from this protocol to be considered compatible.
98
98
 
99
+ Implementing this protocol does not by itself guarantee that an instance
100
+ can be reused across concurrent calls. Check the concrete client's
101
+ concurrency contract before sharing it.
102
+
99
103
  Examples:
100
104
  .. code-block:: python
101
105
 
@@ -578,7 +582,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
578
582
  tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None = None,
579
583
  default_options: OptionsCoT | Mapping[str, Any] | None = None,
580
584
  context_providers: Sequence[Any] | None = None,
581
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
585
+ middleware: Sequence[MiddlewareTypes] | None = None,
582
586
  require_per_service_call_history_persistence: bool = False,
583
587
  compaction_strategy: CompactionStrategy | None = None,
584
588
  tokenizer: TokenizerProtocol | None = None,
@@ -375,6 +375,76 @@ def _set_group_summarized_by_summary_id(message: Message, summary_id: str) -> No
375
375
  annotation[SUMMARIZED_BY_SUMMARY_ID_KEY] = summary_id
376
376
 
377
377
 
378
+ def _reconcile_compaction_summaries( # pyright: ignore[reportUnusedFunction]
379
+ source_messages: list[Message],
380
+ working_messages: Sequence[Message],
381
+ previous_message_ids: set[int],
382
+ ) -> None:
383
+ """Reconcile summaries of source messages without persisting unrelated rewrites."""
384
+ source_message_ids = {message.message_id for message in source_messages if message.message_id}
385
+ candidates: list[tuple[Message, set[str]]] = []
386
+ for message in working_messages:
387
+ if id(message) in previous_message_ids:
388
+ continue
389
+
390
+ annotation = _read_group_annotation_raw(message)
391
+ if annotation is None:
392
+ continue
393
+ summarized_message_ids: Any = annotation.get(SUMMARY_OF_MESSAGE_IDS_KEY)
394
+ if (
395
+ message.message_id
396
+ and isinstance(summarized_message_ids, list)
397
+ and summarized_message_ids
398
+ and all(isinstance(value, str) for value in cast("list[Any]", summarized_message_ids))
399
+ ):
400
+ candidates.append((message, set(cast("list[str]", summarized_message_ids))))
401
+
402
+ dependencies = {message.message_id: summary_ids for message, summary_ids in candidates if message.message_id}
403
+ supported_ids = set(source_message_ids)
404
+ pending_ids = set(dependencies)
405
+ while pending_ids:
406
+ newly_supported = {summary_id for summary_id in pending_ids if dependencies[summary_id].issubset(supported_ids)}
407
+ if not newly_supported:
408
+ break
409
+ supported_ids.update(newly_supported)
410
+ pending_ids.difference_update(newly_supported)
411
+
412
+ accepted_ids = set(dependencies).difference(pending_ids)
413
+ for message in [*source_messages, *(candidate for candidate, _ in candidates)]:
414
+ annotation = _read_group_annotation_raw(message)
415
+ if annotation is None:
416
+ continue
417
+ summary_id = annotation.get(SUMMARIZED_BY_SUMMARY_ID_KEY)
418
+ if not isinstance(summary_id, str) or summary_id in accepted_ids or summary_id in source_message_ids:
419
+ continue
420
+ annotation.pop(SUMMARIZED_BY_SUMMARY_ID_KEY, None)
421
+ message.additional_properties.pop(EXCLUDED_KEY, None)
422
+ message.additional_properties.pop(EXCLUDE_REASON_KEY, None)
423
+
424
+ def source_dependencies(summary_id: str) -> set[str]:
425
+ expanded: set[str] = set()
426
+ for dependency_id in dependencies[summary_id]:
427
+ if dependency_id in source_message_ids:
428
+ expanded.add(dependency_id)
429
+ elif dependency_id in accepted_ids:
430
+ expanded.update(source_dependencies(dependency_id))
431
+ return expanded
432
+
433
+ for message, _ in candidates:
434
+ if message.message_id not in accepted_ids:
435
+ continue
436
+ summarized_source_ids = source_dependencies(message.message_id)
437
+ insertion_index = min(
438
+ (
439
+ index
440
+ for index, source_message in enumerate(source_messages)
441
+ if source_message.message_id in summarized_source_ids
442
+ ),
443
+ default=len(source_messages),
444
+ )
445
+ source_messages.insert(insertion_index, message)
446
+
447
+
378
448
  def _write_group_annotation(
379
449
  message: Message,
380
450
  *,
@@ -802,8 +872,8 @@ class TruncationStrategy:
802
872
  - token count when ``tokenizer`` is provided
803
873
  - included message count when ``tokenizer`` is not provided
804
874
  Compaction triggers when the metric exceeds ``max_n`` and trims toward
805
- ``compact_to``. The minimum retained group is never excluded, so the
806
- result may remain above ``compact_to`` when that group alone exceeds it.
875
+ ``compact_to``. Protected groups are never excluded, so the result may
876
+ remain above ``compact_to`` when those groups alone exceed it.
807
877
  """
808
878
 
809
879
  def __init__(
@@ -813,6 +883,7 @@ class TruncationStrategy:
813
883
  compact_to: int,
814
884
  tokenizer: TokenizerProtocol | None = None,
815
885
  preserve_system: bool = True,
886
+ preserve_first_user_group: bool = False,
816
887
  ) -> None:
817
888
  """Create a truncation strategy.
818
889
 
@@ -824,6 +895,8 @@ class TruncationStrategy:
824
895
  tokenizer: Optional tokenizer used for token-based truncation.
825
896
  preserve_system: When True, system groups remain included and only
826
897
  non-system groups are eligible for exclusion.
898
+ preserve_first_user_group: When True, the earliest user group
899
+ remains included along with the minimum retained group.
827
900
  """
828
901
  if max_n <= 0:
829
902
  raise ValueError("max_n must be greater than 0.")
@@ -835,6 +908,7 @@ class TruncationStrategy:
835
908
  self.compact_to = compact_to
836
909
  self.tokenizer = tokenizer
837
910
  self.preserve_system = preserve_system
911
+ self.preserve_first_user_group = preserve_first_user_group
838
912
 
839
913
  async def __call__(self, messages: list[Message]) -> bool:
840
914
  ordered_group_ids = _ordered_group_ids_from_annotations(messages)
@@ -850,6 +924,13 @@ class TruncationStrategy:
850
924
  protected_ids: set[str] = set()
851
925
  if self.preserve_system:
852
926
  protected_ids = {group_id for group_id in ordered_group_ids if kinds.get(group_id) == "system"}
927
+ if self.preserve_first_user_group:
928
+ first_user_group_id = next(
929
+ (group_id for group_id in ordered_group_ids if kinds.get(group_id) == "user"),
930
+ None,
931
+ )
932
+ if first_user_group_id is not None:
933
+ protected_ids.add(first_user_group_id)
853
934
  protected_ids.update(_minimum_retained_group_ids(messages, ordered_group_ids, kinds))
854
935
 
855
936
  changed = False
@@ -1472,6 +1553,50 @@ class TokenBudgetComposedStrategy:
1472
1553
  return changed
1473
1554
 
1474
1555
 
1556
+ async def _run_compaction_strategy(
1557
+ messages: list[Message],
1558
+ *,
1559
+ strategy: CompactionStrategy,
1560
+ tokenizer: TokenizerProtocol | None = None,
1561
+ phase: str,
1562
+ ) -> bool:
1563
+ """Apply a strategy and log aggregate metrics when it changes context."""
1564
+ resolved_tokenizer = tokenizer
1565
+ if resolved_tokenizer is None:
1566
+ strategy_tokenizer = getattr(strategy, "tokenizer", None)
1567
+ if isinstance(strategy_tokenizer, TokenizerProtocol):
1568
+ resolved_tokenizer = strategy_tokenizer
1569
+
1570
+ annotate_message_groups(messages)
1571
+ if resolved_tokenizer is not None:
1572
+ annotate_token_counts(messages, tokenizer=resolved_tokenizer)
1573
+ before_message_count = _count_included_messages(messages)
1574
+ before_token_count = included_token_count(messages) if resolved_tokenizer is not None else None
1575
+
1576
+ changed = await strategy(messages)
1577
+ if not changed:
1578
+ return False
1579
+
1580
+ annotate_message_groups(messages)
1581
+ if resolved_tokenizer is not None:
1582
+ annotate_token_counts(messages, tokenizer=resolved_tokenizer)
1583
+ after_message_count = _count_included_messages(messages)
1584
+ after_token_count = included_token_count(messages) if resolved_tokenizer is not None else None
1585
+ strategy_name = getattr(strategy, "__name__", type(strategy).__name__)
1586
+ logger.info(
1587
+ "Compaction applied",
1588
+ extra={
1589
+ "compaction_phase": phase,
1590
+ "compaction_strategy": strategy_name,
1591
+ "compaction_included_messages_before": before_message_count,
1592
+ "compaction_included_messages_after": after_message_count,
1593
+ "compaction_included_tokens_before": before_token_count,
1594
+ "compaction_included_tokens_after": after_token_count,
1595
+ },
1596
+ )
1597
+ return True
1598
+
1599
+
1475
1600
  async def apply_compaction(
1476
1601
  messages: list[Message],
1477
1602
  *,
@@ -1481,10 +1606,12 @@ async def apply_compaction(
1481
1606
  """Apply configured compaction and return projected model-input messages."""
1482
1607
  if strategy is None:
1483
1608
  return messages
1484
- annotate_message_groups(messages)
1485
- if tokenizer is not None:
1486
- annotate_token_counts(messages, tokenizer=tokenizer)
1487
- await strategy(messages)
1609
+ await _run_compaction_strategy(
1610
+ messages,
1611
+ strategy=strategy,
1612
+ tokenizer=tokenizer,
1613
+ phase="in_run",
1614
+ )
1488
1615
  return project_included_messages(messages)
1489
1616
 
1490
1617
 
@@ -1579,10 +1706,12 @@ class CompactionProvider(ContextProvider):
1579
1706
  if not all_messages:
1580
1707
  return
1581
1708
 
1582
- annotate_message_groups(all_messages)
1583
- if self.tokenizer is not None:
1584
- annotate_token_counts(all_messages, tokenizer=self.tokenizer)
1585
- await self.before_strategy(all_messages)
1709
+ await _run_compaction_strategy(
1710
+ all_messages,
1711
+ strategy=self.before_strategy,
1712
+ tokenizer=self.tokenizer,
1713
+ phase="before_run",
1714
+ )
1586
1715
 
1587
1716
  projected = project_included_messages(all_messages)
1588
1717
  projected_set = {id(m) for m in projected}
@@ -1611,10 +1740,12 @@ class CompactionProvider(ContextProvider):
1611
1740
  return
1612
1741
  stored_messages: list[Message] = raw_messages # type: ignore[assignment]
1613
1742
 
1614
- annotate_message_groups(stored_messages)
1615
- if self.tokenizer is not None:
1616
- annotate_token_counts(stored_messages, tokenizer=self.tokenizer)
1617
- await self.after_strategy(stored_messages)
1743
+ await _run_compaction_strategy(
1744
+ stored_messages,
1745
+ strategy=self.after_strategy,
1746
+ tokenizer=self.tokenizer,
1747
+ phase="after_run",
1748
+ )
1618
1749
 
1619
1750
  # Keep all messages (including excluded) in storage so annotations are
1620
1751
  # preserved. The history provider's ``skip_excluded`` flag controls
@@ -1632,9 +1763,10 @@ class ContextWindowCompactionStrategy:
1632
1763
  2. **Truncation** — removes oldest non-system groups when included tokens
1633
1764
  exceed ``truncation_threshold`` of the input budget.
1634
1765
 
1635
- The class uses two independent :class:`TokenBudgetComposedStrategy`
1636
- instances — one per phase — so each fires only when its own threshold
1637
- is exceeded.
1766
+ Each phase checks its threshold explicitly. Token counts are refreshed
1767
+ after tool-result eviction before deciding whether destructive truncation
1768
+ is necessary. If protected groups still exceed the input budget after
1769
+ truncation, the strategy preserves them and emits a structured warning.
1638
1770
 
1639
1771
  Examples:
1640
1772
  .. code-block:: python
@@ -1663,6 +1795,7 @@ class ContextWindowCompactionStrategy:
1663
1795
  tool_eviction_threshold: float = DEFAULT_TOOL_EVICTION_THRESHOLD,
1664
1796
  truncation_threshold: float = DEFAULT_TRUNCATION_THRESHOLD,
1665
1797
  keep_last_tool_call_groups: int = 4,
1798
+ preserve_first_user_group: bool = False,
1666
1799
  ) -> None:
1667
1800
  """Create a context-window compaction strategy.
1668
1801
 
@@ -1681,6 +1814,8 @@ class ContextWindowCompactionStrategy:
1681
1814
  keep_last_tool_call_groups: Number of most recent tool-call groups
1682
1815
  to retain verbatim during tool eviction. Older groups are
1683
1816
  collapsed into summaries. Defaults to 4.
1817
+ preserve_first_user_group: Whether destructive truncation preserves
1818
+ the earliest user group. Defaults to False.
1684
1819
 
1685
1820
  Raises:
1686
1821
  ValueError: If thresholds are out of range or inconsistent.
@@ -1706,24 +1841,18 @@ class ContextWindowCompactionStrategy:
1706
1841
  self.input_budget_tokens = input_budget
1707
1842
  self.tool_eviction_threshold = tool_eviction_threshold
1708
1843
  self.truncation_threshold = truncation_threshold
1844
+ self.tokenizer = resolved_tokenizer
1845
+ self._tool_eviction_tokens = tool_eviction_tokens
1846
+ self._truncation_tokens = truncation_tokens
1709
1847
 
1710
- self._tool_eviction = TokenBudgetComposedStrategy(
1711
- token_budget=tool_eviction_tokens,
1712
- tokenizer=resolved_tokenizer,
1713
- strategies=[
1714
- ToolResultCompactionStrategy(keep_last_tool_call_groups=keep_last_tool_call_groups),
1715
- ],
1848
+ self._tool_eviction = ToolResultCompactionStrategy(
1849
+ keep_last_tool_call_groups=keep_last_tool_call_groups,
1716
1850
  )
1717
- self._truncation = TokenBudgetComposedStrategy(
1718
- token_budget=truncation_tokens,
1851
+ self._truncation = TruncationStrategy(
1852
+ max_n=truncation_tokens,
1853
+ compact_to=tool_eviction_tokens,
1719
1854
  tokenizer=resolved_tokenizer,
1720
- strategies=[
1721
- TruncationStrategy(
1722
- max_n=truncation_tokens,
1723
- compact_to=tool_eviction_tokens,
1724
- tokenizer=resolved_tokenizer,
1725
- ),
1726
- ],
1855
+ preserve_first_user_group=preserve_first_user_group,
1727
1856
  )
1728
1857
 
1729
1858
  async def __call__(self, messages: list[Message]) -> bool:
@@ -1732,8 +1861,28 @@ class ContextWindowCompactionStrategy:
1732
1861
  Returns:
1733
1862
  True if compaction changed message inclusion; otherwise False.
1734
1863
  """
1735
- changed = await self._tool_eviction(messages)
1736
- return (await self._truncation(messages)) or changed
1864
+ annotate_message_groups(messages)
1865
+ annotate_token_counts(messages, tokenizer=self.tokenizer)
1866
+
1867
+ changed = False
1868
+ if included_token_count(messages) > self._tool_eviction_tokens:
1869
+ changed = await self._tool_eviction(messages)
1870
+ annotate_message_groups(messages)
1871
+ annotate_token_counts(messages, tokenizer=self.tokenizer)
1872
+
1873
+ if included_token_count(messages) > self._truncation_tokens:
1874
+ changed = (await self._truncation(messages)) or changed
1875
+ remaining_tokens = included_token_count(messages)
1876
+ if remaining_tokens > self.input_budget_tokens:
1877
+ logger.warning(
1878
+ "Compaction could not fit protected messages within the input budget",
1879
+ extra={
1880
+ "compaction_strategy": type(self).__name__,
1881
+ "compaction_included_tokens_after": remaining_tokens,
1882
+ "compaction_input_budget_tokens": self.input_budget_tokens,
1883
+ },
1884
+ )
1885
+ return changed
1737
1886
 
1738
1887
 
1739
1888
  __all__ = [
@@ -20,6 +20,7 @@ from .._agents import Agent, SupportsAgentRun
20
20
  from .._clients import SupportsShellTool, SupportsWebSearchTool
21
21
  from .._compaction import CompactionProvider, ContextWindowCompactionStrategy
22
22
  from .._feature_stage import ExperimentalFeature, warn_experimental_feature
23
+ from .._middleware import _copy_middleware_sequence # pyright: ignore[reportPrivateUsage]
23
24
  from .._sessions import ContextProvider, HistoryProvider, InMemoryHistoryProvider, MessageInjectionMiddleware
24
25
  from .._skills import SkillsProvider
25
26
  from .._telemetry import FeatureIndex, mark_feature_used
@@ -347,7 +348,7 @@ def create_harness_agent(
347
348
  loop_max_iterations: int | None = DEFAULT_MAX_ITERATIONS,
348
349
  otel_provider_name: str | None = None,
349
350
  context_providers: Sequence[ContextProvider] | None = None,
350
- middleware: MiddlewareTypes | Sequence[MiddlewareTypes] | None = None,
351
+ middleware: Sequence[MiddlewareTypes] | None = None,
351
352
  default_options: Mapping[str, Any] | None = None,
352
353
  ) -> Agent[OptionsCoT]:
353
354
  """Create a pre-configured agent with batteries included.
@@ -669,11 +670,8 @@ def create_harness_agent(
669
670
  # Message injection is always on. It is a no-op when no messages are queued for the session,
670
671
  # so there is no opt-out.
671
672
  assembled_middleware.append(MessageInjectionMiddleware())
672
- # Bare-source normalization (a single middleware object or a MiddlewareBundle is
673
- # one element) is owned by _as_middleware_list.
674
- from .._middleware import _as_middleware_list # pyright: ignore[reportPrivateUsage]
675
-
676
- assembled_middleware.extend(_as_middleware_list(middleware))
673
+ if middleware is not None:
674
+ assembled_middleware.extend(_copy_middleware_sequence(middleware))
677
675
 
678
676
  agent = Agent(
679
677
  client,