agentbyte 0.26.4__tar.gz → 0.27.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 (303) hide show
  1. {agentbyte-0.26.4 → agentbyte-0.27.0}/CHANGELOG.md +12 -0
  2. {agentbyte-0.26.4 → agentbyte-0.27.0}/PKG-INFO +2 -2
  3. {agentbyte-0.26.4 → agentbyte-0.27.0}/README.md +1 -1
  4. agentbyte-0.27.0/src/agentbyte/__about__.py +2 -0
  5. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/agents/agent.py +26 -1
  6. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/agents/base.py +3 -0
  7. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/agents/types.py +1 -1
  8. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/context_providers/skills.py +3 -2
  9. agentbyte-0.27.0/src/agentbyte/llm/retry_policy.py +200 -0
  10. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/middleware/__init__.py +2 -0
  11. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/middleware/base.py +109 -0
  12. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_basic.py +69 -0
  13. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/llm/test_retry_policy_api.py +1 -0
  14. agentbyte-0.27.0/tests/llm/test_retryable_error_substrings.py +222 -0
  15. agentbyte-0.27.0/tests/middleware/test_duplicate_tool_result.py +191 -0
  16. agentbyte-0.26.4/src/agentbyte/__about__.py +0 -2
  17. agentbyte-0.26.4/src/agentbyte/llm/retry_policy.py +0 -130
  18. {agentbyte-0.26.4 → agentbyte-0.27.0}/.gitignore +0 -0
  19. {agentbyte-0.26.4 → agentbyte-0.27.0}/LICENSE +0 -0
  20. {agentbyte-0.26.4 → agentbyte-0.27.0}/pyproject.toml +0 -0
  21. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/__init__.py +0 -0
  22. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/agents/__init__.py +0 -0
  23. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/agents/agent_as_tool.py +0 -0
  24. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/agents/embedding_agent.py +0 -0
  25. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/cancellation_token.py +0 -0
  26. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/catalog.py +0 -0
  27. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/cli/__init__.py +0 -0
  28. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/cli/main.py +0 -0
  29. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/component.py +0 -0
  30. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/context.py +0 -0
  31. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/context_providers/__init__.py +0 -0
  32. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/context_providers/base.py +0 -0
  33. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/context_providers/skill_tools.py +0 -0
  34. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/__init__.py +0 -0
  35. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/base.py +0 -0
  36. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/config.py +0 -0
  37. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/importer.py +0 -0
  38. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/json.py +0 -0
  39. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/loader.py +0 -0
  40. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/publish.py +0 -0
  41. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/publish_config.py +0 -0
  42. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/publishers.py +0 -0
  43. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/sources.py +0 -0
  44. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/sqlite.py +0 -0
  45. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/sqlite_db.py +0 -0
  46. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/write_config.py +0 -0
  47. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/dataset/writers.py +0 -0
  48. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/entity.py +0 -0
  49. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/__init__.py +0 -0
  50. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/base.py +0 -0
  51. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/checks/__init__.py +0 -0
  52. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/checks/decorator.py +0 -0
  53. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/checks/keyword.py +0 -0
  54. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/checks/local.py +0 -0
  55. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/checks/tool.py +0 -0
  56. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/checks/types.py +0 -0
  57. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/comparison.py +0 -0
  58. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/eval_dataset.py +0 -0
  59. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/judges/__init__.py +0 -0
  60. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/judges/base.py +0 -0
  61. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/judges/composite.py +0 -0
  62. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/judges/llm.py +0 -0
  63. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/judges/pairwise.py +0 -0
  64. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/judges/reference.py +0 -0
  65. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/pairwise.py +0 -0
  66. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/report.py +0 -0
  67. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/runner.py +0 -0
  68. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/splitting.py +0 -0
  69. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/targets/__init__.py +0 -0
  70. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/targets/agent.py +0 -0
  71. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/targets/model.py +0 -0
  72. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/targets/multi_turn.py +0 -0
  73. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/targets/orchestrator.py +0 -0
  74. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/eval/types.py +0 -0
  75. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/__init__.py +0 -0
  76. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/_retry_observability.py +0 -0
  77. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/auth.py +0 -0
  78. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/azure/__init__.py +0 -0
  79. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/azure/auth.py +0 -0
  80. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/azure/chat.py +0 -0
  81. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/azure/embedding.py +0 -0
  82. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/azure/settings.py +0 -0
  83. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/azure_openai.py +0 -0
  84. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/azure_openai_embedding.py +0 -0
  85. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/base.py +0 -0
  86. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/embeddings_base.py +0 -0
  87. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/openai/__init__.py +0 -0
  88. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/openai/chat.py +0 -0
  89. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/openai/embedding.py +0 -0
  90. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/openai/settings.py +0 -0
  91. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/openai_embedding.py +0 -0
  92. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/pricing.py +0 -0
  93. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/settings.py +0 -0
  94. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/llm/types.py +0 -0
  95. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/logger.py +0 -0
  96. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/memory/__init__.py +0 -0
  97. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/memory/base.py +0 -0
  98. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/messages.py +0 -0
  99. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/middleware/otel.py +0 -0
  100. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/middleware/retry.py +0 -0
  101. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/middleware/sql_usage.py +0 -0
  102. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/middleware/usage_logger.py +0 -0
  103. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/notebook.py +0 -0
  104. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/__init__.py +0 -0
  105. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/base.py +0 -0
  106. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/config.py +0 -0
  107. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/gepa.py +0 -0
  108. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/mipro.py +0 -0
  109. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/pareto.py +0 -0
  110. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/reflective.py +0 -0
  111. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/spec.py +0 -0
  112. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/optim/trace.py +0 -0
  113. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/orchestration/__init__.py +0 -0
  114. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/orchestration/ai.py +0 -0
  115. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/orchestration/base.py +0 -0
  116. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/orchestration/handoff.py +0 -0
  117. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/orchestration/plan.py +0 -0
  118. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/orchestration/policies.py +0 -0
  119. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/orchestration/round_robin.py +0 -0
  120. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/__init__.py +0 -0
  121. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/agents.py +0 -0
  122. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/clients.py +0 -0
  123. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/instruction_registry.py +0 -0
  124. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/instructions/orchestrator.yaml +0 -0
  125. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/instructions/query_rewriter.yaml +0 -0
  126. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/instructions/researcher.yaml +0 -0
  127. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/instructions/reviewer.yaml +0 -0
  128. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/instructions/writer.yaml +0 -0
  129. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/orchestration.py +0 -0
  130. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/skills/contracts-analyst/SKILL.md +0 -0
  131. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/skills/hr-analyst/SKILL.md +0 -0
  132. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/skills/hr-analyst/resources/departments.md +0 -0
  133. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/skills/hr-analyst/resources/employees.md +0 -0
  134. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/skills/hr-analyst/resources/payroll.md +0 -0
  135. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/streaming.py +0 -0
  136. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/presets/workflow.py +0 -0
  137. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/session_store.py +0 -0
  138. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/skills/__init__.py +0 -0
  139. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/skills/base.py +0 -0
  140. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/skills/resources.py +0 -0
  141. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/skills/scripts.py +0 -0
  142. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/skills/sources.py +0 -0
  143. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/skills/validation.py +0 -0
  144. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/__init__.py +0 -0
  145. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/base.py +0 -0
  146. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/cancellation.py +0 -0
  147. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/composite.py +0 -0
  148. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/consecutive_agent.py +0 -0
  149. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/external.py +0 -0
  150. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/function_call.py +0 -0
  151. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/handoff.py +0 -0
  152. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/max_message.py +0 -0
  153. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/predicate.py +0 -0
  154. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/source.py +0 -0
  155. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/text_mention.py +0 -0
  156. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/timeout.py +0 -0
  157. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/termination/token_usage.py +0 -0
  158. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/tools/__init__.py +0 -0
  159. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/tools/base.py +0 -0
  160. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/tools/coding_tools.py +0 -0
  161. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/tools/core_tools.py +0 -0
  162. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/tools/decorator.py +0 -0
  163. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/tools/memory_tool.py +0 -0
  164. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/tools/research_tools.py +0 -0
  165. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/types.py +0 -0
  166. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/__init__.py +0 -0
  167. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/discovery.py +0 -0
  168. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/execution.py +0 -0
  169. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/models.py +0 -0
  170. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/registry.py +0 -0
  171. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/server.py +0 -0
  172. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/session_store.py +0 -0
  173. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/sessions.py +0 -0
  174. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/ui/assets/index-BF3DwXaF.js +0 -0
  175. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/ui/assets/index-ar5tOeqt.css +0 -0
  176. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/ui/index.html +0 -0
  177. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/webui/ui/vite.svg +0 -0
  178. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/__init__.py +0 -0
  179. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/agent.py +0 -0
  180. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/core/__init__.py +0 -0
  181. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/core/_structure_hash.py +0 -0
  182. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/core/checkpoint.py +0 -0
  183. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/core/models.py +0 -0
  184. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/core/runner.py +0 -0
  185. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/core/workflow.py +0 -0
  186. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/defaults.py +0 -0
  187. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/loader.py +0 -0
  188. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/schema.py +0 -0
  189. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/schema_utils.py +0 -0
  190. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/steps/__init__.py +0 -0
  191. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/steps/agentbyte_agent.py +0 -0
  192. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/steps/echo.py +0 -0
  193. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/steps/function.py +0 -0
  194. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/steps/http.py +0 -0
  195. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/steps/step.py +0 -0
  196. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/steps/subworkflow.py +0 -0
  197. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/steps/transform.py +0 -0
  198. {agentbyte-0.26.4 → agentbyte-0.27.0}/src/agentbyte/workflow/visualizer.py +0 -0
  199. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_as_tool.py +0 -0
  200. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_context_providers.py +0 -0
  201. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_event_types.py +0 -0
  202. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_memory_integration.py +0 -0
  203. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_middleware_integration.py +0 -0
  204. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_response_accessors.py +0 -0
  205. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_retry_middleware.py +0 -0
  206. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_agent_stream_events.py +0 -0
  207. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_embedding_agent.py +0 -0
  208. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/agents/test_tool_approval.py +0 -0
  209. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/cli/test_registry_check.py +0 -0
  210. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/context_providers/__init__.py +0 -0
  211. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/context_providers/test_skill_tools.py +0 -0
  212. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/context_providers/test_skills_provider.py +0 -0
  213. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/dataset/test_loader.py +0 -0
  214. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/dataset/test_multi_table.py +0 -0
  215. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/dataset/test_publish.py +0 -0
  216. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/dataset/test_sqlite_db.py +0 -0
  217. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/eval/test_eval_dataset.py +0 -0
  218. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/eval/test_multi_turn.py +0 -0
  219. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/eval/test_pairwise.py +0 -0
  220. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/eval/test_phase1_runner_and_targets.py +0 -0
  221. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/eval/test_phase2_checks_and_reports.py +0 -0
  222. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/eval/test_splitting.py +0 -0
  223. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/eval/test_types_and_judges.py +0 -0
  224. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/llm/test_azure_client.py +0 -0
  225. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/llm/test_azure_embedding_client.py +0 -0
  226. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/llm/test_llm_types.py +0 -0
  227. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/llm/test_openai_client.py +0 -0
  228. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/llm/test_openai_embedding_client.py +0 -0
  229. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/llm/test_pricing.py +0 -0
  230. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/llm/test_retry_observability.py +0 -0
  231. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/memory/test_memory.py +0 -0
  232. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/middleware/test_middleware_chain.py +0 -0
  233. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/middleware/test_otel.py +0 -0
  234. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/middleware/test_retry_middleware.py +0 -0
  235. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/middleware/test_sql_usage.py +0 -0
  236. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/middleware/test_usage_logger.py +0 -0
  237. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/__init__.py +0 -0
  238. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_base.py +0 -0
  239. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_base_integration.py +0 -0
  240. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_config.py +0 -0
  241. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_gepa.py +0 -0
  242. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_mipro.py +0 -0
  243. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_pareto.py +0 -0
  244. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_reflective.py +0 -0
  245. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_spec.py +0 -0
  246. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/optim/test_trace.py +0 -0
  247. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/orchestration/test_ai_orchestrator.py +0 -0
  248. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/orchestration/test_base_orchestrator.py +0 -0
  249. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/orchestration/test_handoff_orchestrator.py +0 -0
  250. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/orchestration/test_plan_orchestrator.py +0 -0
  251. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/orchestration/test_round_robin.py +0 -0
  252. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/presets/test_agents.py +0 -0
  253. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/presets/test_clients.py +0 -0
  254. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/presets/test_instruction_registry.py +0 -0
  255. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/presets/test_orchestration.py +0 -0
  256. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/presets/test_streaming.py +0 -0
  257. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/presets/test_workflow.py +0 -0
  258. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/skills/__init__.py +0 -0
  259. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/skills/test_base.py +0 -0
  260. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/skills/test_resources.py +0 -0
  261. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/skills/test_scripts.py +0 -0
  262. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/skills/test_sources.py +0 -0
  263. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_base.py +0 -0
  264. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_cancellation.py +0 -0
  265. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_composite.py +0 -0
  266. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_consecutive_agent.py +0 -0
  267. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_external.py +0 -0
  268. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_function_call.py +0 -0
  269. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_handoff.py +0 -0
  270. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_max_message.py +0 -0
  271. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_predicate.py +0 -0
  272. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_source.py +0 -0
  273. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_text_mention.py +0 -0
  274. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_timeout.py +0 -0
  275. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/termination/test_token_usage.py +0 -0
  276. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/test_cancellation_token.py +0 -0
  277. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/test_context.py +0 -0
  278. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/test_logger.py +0 -0
  279. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/test_messages.py +0 -0
  280. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/test_package_api.py +0 -0
  281. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/test_session_store.py +0 -0
  282. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/test_types.py +0 -0
  283. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/test_vanilla_chunker.py +0 -0
  284. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/tools/test_coding_tools.py +0 -0
  285. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/tools/test_memory_tool.py +0 -0
  286. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/tools/test_research_tools.py +0 -0
  287. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/tools/test_tools.py +0 -0
  288. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/webui/__init__.py +0 -0
  289. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/webui/helpers.py +0 -0
  290. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/webui/test_execution.py +0 -0
  291. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/webui/test_package_api.py +0 -0
  292. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/webui/test_registry.py +0 -0
  293. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/webui/test_server.py +0 -0
  294. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/webui/test_sessions.py +0 -0
  295. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_checkpoint.py +0 -0
  296. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_subworkflow_step.py +0 -0
  297. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_workflow_agent.py +0 -0
  298. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_workflow_class.py +0 -0
  299. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_workflow_models.py +0 -0
  300. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_workflow_runner.py +0 -0
  301. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_workflow_schema.py +0 -0
  302. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_workflow_steps.py +0 -0
  303. {agentbyte-0.26.4 → agentbyte-0.27.0}/tests/workflow/test_workflow_visualizer.py +0 -0
@@ -4,6 +4,18 @@ All notable changes to Agentbyte are documented in this file.
4
4
 
5
5
  The format follows Keep a Changelog principles and semantic versioning.
6
6
 
7
+ ## [0.27.0] - 2026-09-12
8
+
9
+ ### Added
10
+
11
+ - Agents now support `finalize_on_exhaustion` (default `True`): when the iteration budget is about to be exhausted, the last iteration is reserved for a tool-free finalization turn so the agent always returns a complete, well-formed answer instead of nothing. This turn disables tools and injects a finalization instruction; the run reports `finish_reason="max_iterations_finalized"`. Set `finalize_on_exhaustion=False` to keep the previous behavior of stopping with `finish_reason="max_iterations"`.
12
+ - New `DuplicateToolResultMiddleware` (exported from `agentbyte.middleware`) collapses byte-identical large tool results in the model-call payload: it keeps the most recent full copy and replaces earlier duplicates in place with a short placeholder, preserving `tool_call_id` pairing. Duplicates are matched on a full-content hash, and only the outgoing payload is modified — persisted session history keeps every full copy. This caps the token cost of a skill or resource that is loaded on several turns; place it before `ContextCompactionMiddleware`. Configurable via `min_dedupe_chars` (default `2000`).
13
+ - `RetryPolicy` gained `retryable_error_substrings` (default empty): an otherwise-terminal error (for example Azure's non-deterministic `invalid_prompt` 400) is retried with the standard back-off when `str(error)` contains a configured marker (case-insensitive). The field is plain JSON data, so it round-trips through component serialization. Streaming keeps its no-retry-after-first-chunk guard, and `AuthenticationError` remains terminal regardless of markers.
14
+
15
+ ### Changed
16
+
17
+ - The default skills advertisement template now instructs the model to reuse skill content already present earlier in the conversation instead of calling `load_skill` again, reducing avoidable reloads in multi-turn sessions. Callers passing a custom `instruction_template` are unaffected.
18
+
7
19
  ## [0.26.4] - 2026-09-10
8
20
 
9
21
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentbyte
3
- Version: 0.26.4
3
+ Version: 0.27.0
4
4
  Summary: A toolkit for designing multiagent systems
5
5
  Author-email: MrDataPsycho <mr.data.psycho@gmail.com>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -86,7 +86,7 @@ Description-Content-Type: text/markdown
86
86
 
87
87
  Agentbyte is an observability-first agentic AI framework for building and studying multiagent systems with a learning-first, implementation-oriented workflow.
88
88
 
89
- Current release: **0.26.4**
89
+ Current release: **0.27.0**
90
90
 
91
91
  ## Building an Agent
92
92
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Agentbyte is an observability-first agentic AI framework for building and studying multiagent systems with a learning-first, implementation-oriented workflow.
4
4
 
5
- Current release: **0.26.4**
5
+ Current release: **0.27.0**
6
6
 
7
7
  ## Building an Agent
8
8
 
@@ -0,0 +1,2 @@
1
+ __version__ = "0.27.0"
2
+ VERSION = __version__
@@ -16,6 +16,7 @@ from agentbyte.llm.types import ChatCompletionChunk
16
16
  from agentbyte.messages import (
17
17
  AssistantMessage,
18
18
  Message,
19
+ SystemMessage,
19
20
  ToolCallRequest,
20
21
  ToolMessage,
21
22
  Usage,
@@ -42,6 +43,14 @@ from .types import (
42
43
  )
43
44
 
44
45
 
46
+ _FINALIZE_INSTRUCTION = (
47
+ "You have reached the maximum number of tool-using iterations. No further "
48
+ "tool calls are available. Produce your final answer now using only the "
49
+ "information already gathered. If the analysis is incomplete, state clearly "
50
+ "what is missing, but still return a complete, well-formed response."
51
+ )
52
+
53
+
45
54
  class Agent(BaseAgent):
46
55
  """Standard single-agent execution loop with tools and cancellation."""
47
56
 
@@ -462,6 +471,20 @@ class Agent(BaseAgent):
462
471
  memory_operations += 1
463
472
  tools = self._get_tools_for_llm(extra_tools=provider_tools) if (self.tools or provider_tools) else None
464
473
 
474
+ # Reserve the final iteration for a tool-free answer so an exhausted
475
+ # budget yields a complete response instead of nothing.
476
+ is_final_iteration = (
477
+ self.finalize_on_exhaustion
478
+ and self.max_iterations > 1
479
+ and iteration == self.max_iterations - 1
480
+ and bool(tools)
481
+ )
482
+ if is_final_iteration:
483
+ tools = None
484
+ llm_messages.append(
485
+ SystemMessage(content=_FINALIZE_INSTRUCTION, source=self.name)
486
+ )
487
+
465
488
  if verbose:
466
489
  yield ModelCallEvent(
467
490
  source=self.name,
@@ -518,7 +541,7 @@ class Agent(BaseAgent):
518
541
 
519
542
  working_context.add_message(assistant_message)
520
543
  yield assistant_message
521
- finish_reason = "stop"
544
+ finish_reason = "max_iterations_finalized" if is_final_iteration else "stop"
522
545
  break
523
546
 
524
547
  async def _execute_model_call(payload: dict[str, object]):
@@ -621,6 +644,8 @@ class Agent(BaseAgent):
621
644
 
622
645
  if not assistant_message.tool_calls:
623
646
  finish_reason = completion_result.finish_reason or "stop"
647
+ if is_final_iteration:
648
+ finish_reason = "max_iterations_finalized"
624
649
  break
625
650
 
626
651
  approval_pause = False
@@ -53,6 +53,7 @@ class BaseAgent(ABC):
53
53
  memory: Optional[BaseMemory] = None,
54
54
  middlewares: Optional[List[BaseMiddleware]] = None,
55
55
  max_iterations: int = 10,
56
+ finalize_on_exhaustion: bool = True,
56
57
  output_format: Optional[Type[BaseModel]] = None,
57
58
  summarize_tool_result: bool = True,
58
59
  required_tools: Optional[List[str]] = None,
@@ -68,6 +69,7 @@ class BaseAgent(ABC):
68
69
  self.memory = memory
69
70
  self.middleware_chain = MiddlewareChain(middlewares)
70
71
  self.max_iterations = max_iterations
72
+ self.finalize_on_exhaustion = finalize_on_exhaustion
71
73
  self.output_format = output_format
72
74
  self.summarize_tool_result = summarize_tool_result
73
75
  self.required_tools = required_tools or []
@@ -217,6 +219,7 @@ class BaseAgent(ABC):
217
219
  "tools_count": len(self.tools),
218
220
  "middlewares_count": len(self.middleware_chain.middlewares),
219
221
  "max_iterations": self.max_iterations,
222
+ "finalize_on_exhaustion": self.finalize_on_exhaustion,
220
223
  }
221
224
 
222
225
  def as_tool(
@@ -183,7 +183,7 @@ class AgentResponse(BaseModel):
183
183
  source: str = Field(..., description="Agent name")
184
184
  finish_reason: str = Field(
185
185
  ...,
186
- description="Why the agent stopped: stop, approval_needed, max_iterations, error, cancelled",
186
+ description="Why the agent stopped: stop, approval_needed, max_iterations, max_iterations_finalized, tool_executed, error, cancelled",
187
187
  )
188
188
  usage: Usage = Field(default_factory=Usage, description="Aggregated usage")
189
189
  timestamp: datetime = Field(
@@ -29,9 +29,10 @@ Each skill provides specialized instructions, reference documents, and assets fo
29
29
  </available_skills>
30
30
 
31
31
  When a task aligns with a skill's domain, follow these steps in exact order:
32
- - Use `load_skill` to retrieve the skill's instructions.
32
+ - If the skill's content is already present earlier in this conversation, reuse it and do NOT call `load_skill` again for it.
33
+ - Otherwise, use `load_skill` to retrieve the skill's instructions.
33
34
  - Follow the provided guidance.
34
- {resource_instructions}{runner_instructions}Only load what is needed, when it is needed."""
35
+ {resource_instructions}{runner_instructions}Only load what is needed, when it is needed, and do not reload content already shown above."""
35
36
 
36
37
  _RESOURCE_INSTRUCTIONS = (
37
38
  "- Use `read_skill_resource` to read any referenced resources, "
@@ -0,0 +1,200 @@
1
+ """Shared retry policy and mixin for LLM clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from collections.abc import AsyncGenerator
8
+ from typing import Any, Callable, List, Optional
9
+
10
+ from pydantic import BaseModel, Field, field_validator
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class RetryPolicy(BaseModel):
16
+ """Retry configuration for LLM clients."""
17
+
18
+ max_retries: int = 3
19
+ initial_retry_delay: float = 1.0
20
+ max_retry_delay: float = 60.0
21
+ retryable_error_substrings: List[str] = Field(default_factory=list)
22
+ """Opt otherwise-terminal errors into the retry path by marker.
23
+
24
+ An error that would normally be non-transient (for example a provider's
25
+ ``BadRequestError`` mapped to :class:`InvalidRequestError`) is retried with
26
+ the standard back-off when ``str(error)`` contains any of these substrings
27
+ (case-insensitive). Empty by default, so existing behaviour is unchanged.
28
+ Use a specific marker such as an error code (``"invalid_prompt"``), not a
29
+ generic word, or unrelated failures will only be slowed before they surface.
30
+ """
31
+
32
+ @field_validator("max_retries")
33
+ @classmethod
34
+ def _non_negative_retries(cls, v: int) -> int:
35
+ if v < 0:
36
+ raise ValueError("max_retries must be >= 0")
37
+ return v
38
+
39
+ @field_validator("initial_retry_delay", "max_retry_delay")
40
+ @classmethod
41
+ def _no_negative_delays(cls, v: float) -> float:
42
+ if v < 0:
43
+ raise ValueError("delay must be non-negative")
44
+ return v
45
+
46
+ def model_post_init(self, __context: Any) -> None:
47
+ if self.max_retry_delay < self.initial_retry_delay:
48
+ raise ValueError(
49
+ "max_retry_delay must be >= initial_retry_delay"
50
+ )
51
+
52
+
53
+ class RetryMixin:
54
+ """Mixin providing unified retry logic for all LLM clients.
55
+
56
+ Clients must set ``self.retry_policy`` (a :class:`RetryPolicy` instance)
57
+ before calling these methods.
58
+ """
59
+
60
+ retry_policy: RetryPolicy
61
+
62
+ def _error_opted_into_retry(self, error: Exception) -> bool:
63
+ """Return True if the caller marked this error's text as retryable.
64
+
65
+ Matches ``str(error)`` case-insensitively against
66
+ ``retry_policy.retryable_error_substrings``. This only ever widens what is
67
+ retried; it never suppresses an error.
68
+ """
69
+ markers = getattr(self.retry_policy, "retryable_error_substrings", None) or []
70
+ if not markers:
71
+ return False
72
+ text = str(error).lower()
73
+ return any(marker.lower() in text for marker in markers)
74
+
75
+ async def _retry_with_backoff(
76
+ self,
77
+ func: Callable[..., Any],
78
+ *args: Any,
79
+ _retry_history: Optional[List] = None,
80
+ **kwargs: Any,
81
+ ) -> Any:
82
+ """Execute *func* with exponential back-off on transient errors."""
83
+ from .base import AuthenticationError, InvalidRequestError, RateLimitError
84
+ from .types import ModelClientError
85
+ from ._retry_observability import build_retry_record
86
+
87
+ last_exception: Optional[Exception] = None
88
+ current_delay = self.retry_policy.initial_retry_delay
89
+
90
+ for attempt in range(self.retry_policy.max_retries + 1):
91
+ try:
92
+ return await func(*args, **kwargs)
93
+ except AuthenticationError as e:
94
+ # Authentication never becomes valid on retry; opt-in cannot
95
+ # widen this.
96
+ logger.error(f"Non-transient error in {func.__name__}: {e}")
97
+ raise
98
+ except InvalidRequestError as e:
99
+ # Terminal by classification. Retry only when the caller opted
100
+ # this error's text into the retry path.
101
+ last_exception = e
102
+ if not self._error_opted_into_retry(e):
103
+ logger.error(f"Non-transient error in {func.__name__}: {e}")
104
+ raise
105
+ except (RateLimitError, ModelClientError) as e:
106
+ last_exception = e
107
+ except Exception as e:
108
+ # Raw provider errors reach the retry loop before adapter
109
+ # conversion. Terminal unless the caller opted the text in.
110
+ last_exception = e
111
+ if not self._error_opted_into_retry(e):
112
+ logger.error(f"Unexpected error in {func.__name__}: {e}")
113
+ raise
114
+
115
+ # Reached only for a retryable error.
116
+ if attempt >= self.retry_policy.max_retries:
117
+ logger.warning(
118
+ f"Max retries ({self.retry_policy.max_retries}) reached for {func.__name__}"
119
+ )
120
+ raise last_exception
121
+ if _retry_history is not None:
122
+ _retry_history.append(
123
+ build_retry_record(
124
+ attempt + 1, current_delay, type(last_exception), "retrying"
125
+ )
126
+ )
127
+ logger.debug(
128
+ f"Transient error in {func.__name__} "
129
+ f"(attempt {attempt + 1}/{self.retry_policy.max_retries + 1}): "
130
+ f"{last_exception}. Retrying in {current_delay:.1f}s"
131
+ )
132
+ await asyncio.sleep(current_delay)
133
+ current_delay = min(current_delay * 2, self.retry_policy.max_retry_delay)
134
+
135
+ raise last_exception if last_exception is not None else ModelClientError("Retry logic error")
136
+
137
+ async def _retry_stream_with_backoff(
138
+ self,
139
+ func: Callable[..., AsyncGenerator[Any, None]],
140
+ *args: Any,
141
+ **kwargs: Any,
142
+ ) -> AsyncGenerator[Any, None]:
143
+ """Execute a streaming *func* with exponential back-off on transient errors."""
144
+ from .base import AuthenticationError, InvalidRequestError, RateLimitError
145
+ from .types import ModelClientError
146
+
147
+ current_delay = self.retry_policy.initial_retry_delay
148
+
149
+ for attempt in range(self.retry_policy.max_retries + 1):
150
+ saw_chunk = False
151
+ try:
152
+ async for chunk in func(*args, **kwargs):
153
+ saw_chunk = True
154
+ yield chunk
155
+ return
156
+ except AuthenticationError:
157
+ raise
158
+ except InvalidRequestError as e:
159
+ # Terminal unless opted in, and never retried once a chunk has
160
+ # already reached the caller (the response is half-said).
161
+ if (
162
+ saw_chunk
163
+ or attempt >= self.retry_policy.max_retries
164
+ or not self._error_opted_into_retry(e)
165
+ ):
166
+ raise
167
+ logger.debug(
168
+ f"Retryable stream error in {func.__name__} "
169
+ f"(attempt {attempt + 1}/{self.retry_policy.max_retries + 1}): {e}. "
170
+ f"Retrying in {current_delay:.1f}s"
171
+ )
172
+ await asyncio.sleep(current_delay)
173
+ current_delay = min(current_delay * 2, self.retry_policy.max_retry_delay)
174
+ except (RateLimitError, ModelClientError) as e:
175
+ if saw_chunk or attempt >= self.retry_policy.max_retries:
176
+ raise
177
+ logger.debug(
178
+ f"Transient stream error in {func.__name__} "
179
+ f"(attempt {attempt + 1}/{self.retry_policy.max_retries + 1}): {e}. "
180
+ f"Retrying in {current_delay:.1f}s"
181
+ )
182
+ await asyncio.sleep(current_delay)
183
+ current_delay = min(current_delay * 2, self.retry_policy.max_retry_delay)
184
+ except Exception as e:
185
+ if (
186
+ saw_chunk
187
+ or attempt >= self.retry_policy.max_retries
188
+ or not self._error_opted_into_retry(e)
189
+ ):
190
+ raise
191
+ logger.debug(
192
+ f"Retryable stream error in {func.__name__} "
193
+ f"(attempt {attempt + 1}/{self.retry_policy.max_retries + 1}): {e}. "
194
+ f"Retrying in {current_delay:.1f}s"
195
+ )
196
+ await asyncio.sleep(current_delay)
197
+ current_delay = min(current_delay * 2, self.retry_policy.max_retry_delay)
198
+
199
+
200
+ __all__ = ["RetryPolicy", "RetryMixin"]
@@ -6,6 +6,7 @@ from .base import (
6
6
  ApprovalMiddleware,
7
7
  BaseMiddleware,
8
8
  ContextCompactionMiddleware,
9
+ DuplicateToolResultMiddleware,
9
10
  GuardrailMiddleware,
10
11
  LoggingMiddleware,
11
12
  MetricsMiddleware,
@@ -40,6 +41,7 @@ __all__ = [
40
41
  "MiddlewareChain",
41
42
  "ApprovalMiddleware",
42
43
  "ContextCompactionMiddleware",
44
+ "DuplicateToolResultMiddleware",
43
45
  "LoggingMiddleware",
44
46
  "PIIRedactionMiddleware",
45
47
  "GuardrailMiddleware",
@@ -1,12 +1,14 @@
1
1
  """Middleware infrastructure for cross-cutting agent concerns."""
2
2
 
3
3
  import asyncio
4
+ import copy
4
5
  import inspect
5
6
  import json
6
7
  import logging
7
8
  import re
8
9
  import time
9
10
  from abc import ABC, abstractmethod
11
+ from hashlib import sha256
10
12
  from collections import deque
11
13
  from collections.abc import AsyncGenerator, Awaitable, Callable
12
14
  from enum import Enum
@@ -1052,6 +1054,113 @@ class ContextCompactionMiddleware(BaseMiddleware):
1052
1054
  return None
1053
1055
 
1054
1056
 
1057
+ class DuplicateToolResultMiddleware(BaseMiddleware):
1058
+ """Collapse repeated identical tool results in the model-call payload.
1059
+
1060
+ A tool result stays in the conversation for the life of a session and is
1061
+ re-transmitted on every later model call. When the same large document is
1062
+ returned by a tool more than once (for example a skill or resource loaded on
1063
+ several turns), the payload carries multiple byte-identical copies, each
1064
+ billed and sent again.
1065
+
1066
+ This middleware keeps exactly one full copy of each duplicated large tool
1067
+ result -- the most recent -- and replaces the earlier copies in place with a
1068
+ short placeholder. Keeping the last occurrence leaves the surviving full copy
1069
+ in the recency tail that ``ContextCompactionMiddleware`` preserves, so
1070
+ collapsing never leaves a reference to content that later compaction removes.
1071
+
1072
+ Only the outgoing payload (``context.data["messages"]``) is modified; the
1073
+ persisted ``AgentContext.messages`` are untouched, so session history keeps
1074
+ every full copy. Recommended placement: before ``ContextCompactionMiddleware``
1075
+ so compaction operates on the already-deduplicated list.
1076
+ """
1077
+
1078
+ _PLACEHOLDER = (
1079
+ "[Duplicate tool result omitted - identical content appears later in "
1080
+ "this conversation and is still valid. Reuse it; do not request it again.]"
1081
+ )
1082
+
1083
+ def __init__(self, min_dedupe_chars: int = 2000):
1084
+ if min_dedupe_chars <= 0:
1085
+ raise ValueError("min_dedupe_chars must be greater than zero")
1086
+ self.min_dedupe_chars = min_dedupe_chars
1087
+
1088
+ def _is_dedupe_candidate(self, message: Any) -> bool:
1089
+ """A successful tool result whose body is large enough to be worth it."""
1090
+ if getattr(message, "role", None) != "tool":
1091
+ return False
1092
+ if getattr(message, "success", None) is not True:
1093
+ return False
1094
+ content = getattr(message, "content", None)
1095
+ return isinstance(content, str) and len(content) >= self.min_dedupe_chars
1096
+
1097
+ def _collapse(self, message: Any) -> Any:
1098
+ """Return a copy of *message* with its body replaced by the placeholder.
1099
+
1100
+ Never mutates *message* in place: the same object is referenced by the
1101
+ persisted context, and the placeholder must only affect the outgoing
1102
+ payload.
1103
+ """
1104
+ model_copy = getattr(message, "model_copy", None)
1105
+ if callable(model_copy):
1106
+ return model_copy(update={"content": self._PLACEHOLDER})
1107
+ clone = copy.copy(message)
1108
+ clone.content = self._PLACEHOLDER
1109
+ return clone
1110
+
1111
+ async def process_request(self, context: MiddlewareContext) -> MiddlewareContext:
1112
+ if context.operation != OperationType.MODEL_CALL:
1113
+ return context
1114
+
1115
+ payload = context.data
1116
+ if not isinstance(payload, dict):
1117
+ return context
1118
+
1119
+ messages = payload.get("messages", [])
1120
+ if not isinstance(messages, list) or len(messages) <= 1:
1121
+ return context
1122
+
1123
+ # Group candidate indices by an exact hash of the full body. A prefix or
1124
+ # fingerprint is never used, so two different bodies cannot collide.
1125
+ groups: Dict[str, List[int]] = {}
1126
+ for idx, message in enumerate(messages):
1127
+ if not self._is_dedupe_candidate(message):
1128
+ continue
1129
+ key = sha256(message.content.encode("utf-8")).hexdigest()
1130
+ groups.setdefault(key, []).append(idx)
1131
+
1132
+ # Keep the last occurrence in each duplicate group; collapse the rest.
1133
+ collapse: set[int] = set()
1134
+ duplicate_groups = 0
1135
+ for indices in groups.values():
1136
+ if len(indices) > 1:
1137
+ duplicate_groups += 1
1138
+ collapse.update(indices[:-1])
1139
+
1140
+ if not collapse:
1141
+ return context
1142
+
1143
+ new_messages = [
1144
+ self._collapse(message) if idx in collapse else message
1145
+ for idx, message in enumerate(messages)
1146
+ ]
1147
+ context.data = {**payload, "messages": new_messages}
1148
+ context.metadata["duplicate_tool_results_pruned"] = True
1149
+ context.metadata["duplicate_groups"] = duplicate_groups
1150
+ context.metadata["messages_collapsed"] = len(collapse)
1151
+ return context
1152
+
1153
+ async def process_response(self, context: MiddlewareContext, result: Any) -> Any:
1154
+ return result
1155
+
1156
+ async def process_error(
1157
+ self,
1158
+ context: MiddlewareContext,
1159
+ error: Exception,
1160
+ ) -> Optional[Any]:
1161
+ return None
1162
+
1163
+
1055
1164
  class ApprovalMiddleware(BaseMiddleware):
1056
1165
  """Middleware that emits approval events for configured tool names."""
1057
1166
 
@@ -36,6 +36,7 @@ class FakeModelClient(BaseChatCompletionClient):
36
36
  output_format: Optional[type] = None,
37
37
  **kwargs: Any,
38
38
  ) -> ChatCompletionResult:
39
+ self.last_tools = tools
39
40
  response = self._responses[self._index]
40
41
  self._index += 1
41
42
  return response
@@ -361,3 +362,71 @@ async def test_agent_run_with_streaming_tool_path() -> None:
361
362
  tool_messages = [message for message in response.messages if message.role == "tool"]
362
363
  assert len(tool_messages) == 1
363
364
  assert tool_messages[0].content == "5"
365
+
366
+
367
+ def _tool_call_result(call_id: str) -> ChatCompletionResult:
368
+ return ChatCompletionResult(
369
+ message=AssistantMessage(
370
+ content="calling tool",
371
+ source="fake",
372
+ tool_calls=[
373
+ {
374
+ "tool_name": "add",
375
+ "parameters": {"a": 1, "b": 2},
376
+ "call_id": call_id,
377
+ }
378
+ ],
379
+ ),
380
+ usage=Usage(tokens_input=5, tokens_output=2),
381
+ model="fake",
382
+ )
383
+
384
+
385
+ @pytest.mark.asyncio
386
+ async def test_agent_finalizes_on_exhaustion() -> None:
387
+ responses = [
388
+ _tool_call_result("call-1"),
389
+ _tool_call_result("call-2"),
390
+ ChatCompletionResult(
391
+ message=AssistantMessage(content="final answer", source="fake"),
392
+ usage=Usage(tokens_input=4, tokens_output=2),
393
+ model="fake",
394
+ ),
395
+ ]
396
+ client = FakeModelClient(responses)
397
+ agent = Agent(
398
+ name="assistant",
399
+ description="desc",
400
+ instructions="help",
401
+ model_client=client,
402
+ tools=[add],
403
+ max_iterations=3,
404
+ finalize_on_exhaustion=True,
405
+ )
406
+
407
+ response = await agent.run(UserMessage(content="calculate", source="user"))
408
+
409
+ assert response.finish_reason == "max_iterations_finalized"
410
+ assert response.messages[-1].content == "final answer"
411
+ # Last model call must have been made with tools disabled.
412
+ assert client.last_tools is None
413
+
414
+
415
+ @pytest.mark.asyncio
416
+ async def test_agent_max_iterations_without_finalize() -> None:
417
+ responses = [_tool_call_result("call-1"), _tool_call_result("call-2")]
418
+ client = FakeModelClient(responses)
419
+ agent = Agent(
420
+ name="assistant",
421
+ description="desc",
422
+ instructions="help",
423
+ model_client=client,
424
+ tools=[add],
425
+ max_iterations=2,
426
+ finalize_on_exhaustion=False,
427
+ )
428
+
429
+ response = await agent.run(UserMessage(content="calculate", source="user"))
430
+
431
+ assert response.finish_reason == "max_iterations"
432
+
@@ -88,6 +88,7 @@ def test_component_dump_includes_nested_retry_policy(client_cls, model) -> None:
88
88
  "max_retries": 4,
89
89
  "initial_retry_delay": 0.5,
90
90
  "max_retry_delay": 8.0,
91
+ "retryable_error_substrings": [],
91
92
  }
92
93
  assert "max_retries" not in dumped.config
93
94
  assert "initial_retry_delay" not in dumped.config