agentbyte 0.27.0__tar.gz → 0.28.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.27.0 → agentbyte-0.28.0}/CHANGELOG.md +18 -0
  2. {agentbyte-0.27.0 → agentbyte-0.28.0}/PKG-INFO +2 -2
  3. {agentbyte-0.27.0 → agentbyte-0.28.0}/README.md +1 -1
  4. agentbyte-0.28.0/src/agentbyte/__about__.py +2 -0
  5. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/__init__.py +2 -0
  6. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/agent.py +10 -6
  7. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/types.py +31 -1
  8. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/chat.py +6 -0
  9. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai/chat.py +6 -0
  10. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/retry_policy.py +82 -11
  11. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/__init__.py +2 -2
  12. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/base.py +1 -1
  13. agentbyte-0.28.0/tests/agents/test_agent_error_response.py +99 -0
  14. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_retry_observability.py +34 -1
  15. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_retryable_error_substrings.py +99 -0
  16. agentbyte-0.27.0/tests/middleware/test_duplicate_tool_result.py → agentbyte-0.28.0/tests/middleware/test_deduplicate_tool_result.py +14 -14
  17. agentbyte-0.27.0/src/agentbyte/__about__.py +0 -2
  18. {agentbyte-0.27.0 → agentbyte-0.28.0}/.gitignore +0 -0
  19. {agentbyte-0.27.0 → agentbyte-0.28.0}/LICENSE +0 -0
  20. {agentbyte-0.27.0 → agentbyte-0.28.0}/pyproject.toml +0 -0
  21. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/__init__.py +0 -0
  22. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/agent_as_tool.py +0 -0
  23. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/base.py +0 -0
  24. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/embedding_agent.py +0 -0
  25. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/cancellation_token.py +0 -0
  26. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/catalog.py +0 -0
  27. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/cli/__init__.py +0 -0
  28. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/cli/main.py +0 -0
  29. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/component.py +0 -0
  30. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context.py +0 -0
  31. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context_providers/__init__.py +0 -0
  32. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context_providers/base.py +0 -0
  33. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context_providers/skill_tools.py +0 -0
  34. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context_providers/skills.py +0 -0
  35. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/__init__.py +0 -0
  36. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/base.py +0 -0
  37. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/config.py +0 -0
  38. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/importer.py +0 -0
  39. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/json.py +0 -0
  40. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/loader.py +0 -0
  41. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/publish.py +0 -0
  42. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/publish_config.py +0 -0
  43. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/publishers.py +0 -0
  44. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/sources.py +0 -0
  45. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/sqlite.py +0 -0
  46. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/sqlite_db.py +0 -0
  47. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/write_config.py +0 -0
  48. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/writers.py +0 -0
  49. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/entity.py +0 -0
  50. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/__init__.py +0 -0
  51. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/base.py +0 -0
  52. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/__init__.py +0 -0
  53. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/decorator.py +0 -0
  54. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/keyword.py +0 -0
  55. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/local.py +0 -0
  56. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/tool.py +0 -0
  57. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/types.py +0 -0
  58. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/comparison.py +0 -0
  59. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/eval_dataset.py +0 -0
  60. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/__init__.py +0 -0
  61. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/base.py +0 -0
  62. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/composite.py +0 -0
  63. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/llm.py +0 -0
  64. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/pairwise.py +0 -0
  65. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/reference.py +0 -0
  66. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/pairwise.py +0 -0
  67. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/report.py +0 -0
  68. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/runner.py +0 -0
  69. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/splitting.py +0 -0
  70. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/__init__.py +0 -0
  71. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/agent.py +0 -0
  72. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/model.py +0 -0
  73. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/multi_turn.py +0 -0
  74. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/orchestrator.py +0 -0
  75. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/types.py +0 -0
  76. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/__init__.py +0 -0
  77. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/_retry_observability.py +0 -0
  78. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/auth.py +0 -0
  79. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/__init__.py +0 -0
  80. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/auth.py +0 -0
  81. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/embedding.py +0 -0
  82. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/settings.py +0 -0
  83. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure_openai.py +0 -0
  84. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure_openai_embedding.py +0 -0
  85. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/base.py +0 -0
  86. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/embeddings_base.py +0 -0
  87. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai/__init__.py +0 -0
  88. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai/embedding.py +0 -0
  89. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai/settings.py +0 -0
  90. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai_embedding.py +0 -0
  91. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/pricing.py +0 -0
  92. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/settings.py +0 -0
  93. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/types.py +0 -0
  94. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/logger.py +0 -0
  95. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/memory/__init__.py +0 -0
  96. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/memory/base.py +0 -0
  97. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/messages.py +0 -0
  98. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/otel.py +0 -0
  99. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/retry.py +0 -0
  100. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/sql_usage.py +0 -0
  101. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/usage_logger.py +0 -0
  102. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/notebook.py +0 -0
  103. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/__init__.py +0 -0
  104. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/base.py +0 -0
  105. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/config.py +0 -0
  106. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/gepa.py +0 -0
  107. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/mipro.py +0 -0
  108. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/pareto.py +0 -0
  109. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/reflective.py +0 -0
  110. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/spec.py +0 -0
  111. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/trace.py +0 -0
  112. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/__init__.py +0 -0
  113. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/ai.py +0 -0
  114. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/base.py +0 -0
  115. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/handoff.py +0 -0
  116. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/plan.py +0 -0
  117. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/policies.py +0 -0
  118. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/round_robin.py +0 -0
  119. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/__init__.py +0 -0
  120. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/agents.py +0 -0
  121. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/clients.py +0 -0
  122. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instruction_registry.py +0 -0
  123. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/orchestrator.yaml +0 -0
  124. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/query_rewriter.yaml +0 -0
  125. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/researcher.yaml +0 -0
  126. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/reviewer.yaml +0 -0
  127. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/writer.yaml +0 -0
  128. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/orchestration.py +0 -0
  129. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/contracts-analyst/SKILL.md +0 -0
  130. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/hr-analyst/SKILL.md +0 -0
  131. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/hr-analyst/resources/departments.md +0 -0
  132. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/hr-analyst/resources/employees.md +0 -0
  133. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/hr-analyst/resources/payroll.md +0 -0
  134. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/streaming.py +0 -0
  135. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/workflow.py +0 -0
  136. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/session_store.py +0 -0
  137. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/__init__.py +0 -0
  138. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/base.py +0 -0
  139. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/resources.py +0 -0
  140. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/scripts.py +0 -0
  141. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/sources.py +0 -0
  142. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/validation.py +0 -0
  143. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/__init__.py +0 -0
  144. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/base.py +0 -0
  145. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/cancellation.py +0 -0
  146. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/composite.py +0 -0
  147. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/consecutive_agent.py +0 -0
  148. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/external.py +0 -0
  149. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/function_call.py +0 -0
  150. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/handoff.py +0 -0
  151. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/max_message.py +0 -0
  152. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/predicate.py +0 -0
  153. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/source.py +0 -0
  154. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/text_mention.py +0 -0
  155. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/timeout.py +0 -0
  156. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/token_usage.py +0 -0
  157. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/__init__.py +0 -0
  158. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/base.py +0 -0
  159. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/coding_tools.py +0 -0
  160. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/core_tools.py +0 -0
  161. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/decorator.py +0 -0
  162. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/memory_tool.py +0 -0
  163. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/research_tools.py +0 -0
  164. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/types.py +0 -0
  165. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/__init__.py +0 -0
  166. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/discovery.py +0 -0
  167. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/execution.py +0 -0
  168. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/models.py +0 -0
  169. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/registry.py +0 -0
  170. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/server.py +0 -0
  171. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/session_store.py +0 -0
  172. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/sessions.py +0 -0
  173. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/ui/assets/index-BF3DwXaF.js +0 -0
  174. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/ui/assets/index-ar5tOeqt.css +0 -0
  175. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/ui/index.html +0 -0
  176. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/ui/vite.svg +0 -0
  177. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/__init__.py +0 -0
  178. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/agent.py +0 -0
  179. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/__init__.py +0 -0
  180. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/_structure_hash.py +0 -0
  181. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/checkpoint.py +0 -0
  182. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/models.py +0 -0
  183. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/runner.py +0 -0
  184. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/workflow.py +0 -0
  185. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/defaults.py +0 -0
  186. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/loader.py +0 -0
  187. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/schema.py +0 -0
  188. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/schema_utils.py +0 -0
  189. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/__init__.py +0 -0
  190. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/agentbyte_agent.py +0 -0
  191. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/echo.py +0 -0
  192. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/function.py +0 -0
  193. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/http.py +0 -0
  194. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/step.py +0 -0
  195. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/subworkflow.py +0 -0
  196. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/transform.py +0 -0
  197. {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/visualizer.py +0 -0
  198. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_as_tool.py +0 -0
  199. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_basic.py +0 -0
  200. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_context_providers.py +0 -0
  201. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_event_types.py +0 -0
  202. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_memory_integration.py +0 -0
  203. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_middleware_integration.py +0 -0
  204. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_response_accessors.py +0 -0
  205. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_retry_middleware.py +0 -0
  206. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_stream_events.py +0 -0
  207. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_embedding_agent.py +0 -0
  208. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_tool_approval.py +0 -0
  209. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/cli/test_registry_check.py +0 -0
  210. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/context_providers/__init__.py +0 -0
  211. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/context_providers/test_skill_tools.py +0 -0
  212. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/context_providers/test_skills_provider.py +0 -0
  213. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/dataset/test_loader.py +0 -0
  214. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/dataset/test_multi_table.py +0 -0
  215. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/dataset/test_publish.py +0 -0
  216. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/dataset/test_sqlite_db.py +0 -0
  217. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_eval_dataset.py +0 -0
  218. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_multi_turn.py +0 -0
  219. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_pairwise.py +0 -0
  220. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_phase1_runner_and_targets.py +0 -0
  221. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_phase2_checks_and_reports.py +0 -0
  222. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_splitting.py +0 -0
  223. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_types_and_judges.py +0 -0
  224. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_azure_client.py +0 -0
  225. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_azure_embedding_client.py +0 -0
  226. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_llm_types.py +0 -0
  227. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_openai_client.py +0 -0
  228. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_openai_embedding_client.py +0 -0
  229. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_pricing.py +0 -0
  230. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_retry_policy_api.py +0 -0
  231. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/memory/test_memory.py +0 -0
  232. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_middleware_chain.py +0 -0
  233. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_otel.py +0 -0
  234. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_retry_middleware.py +0 -0
  235. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_sql_usage.py +0 -0
  236. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_usage_logger.py +0 -0
  237. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/__init__.py +0 -0
  238. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_base.py +0 -0
  239. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_base_integration.py +0 -0
  240. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_config.py +0 -0
  241. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_gepa.py +0 -0
  242. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_mipro.py +0 -0
  243. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_pareto.py +0 -0
  244. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_reflective.py +0 -0
  245. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_spec.py +0 -0
  246. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_trace.py +0 -0
  247. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_ai_orchestrator.py +0 -0
  248. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_base_orchestrator.py +0 -0
  249. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_handoff_orchestrator.py +0 -0
  250. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_plan_orchestrator.py +0 -0
  251. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_round_robin.py +0 -0
  252. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_agents.py +0 -0
  253. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_clients.py +0 -0
  254. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_instruction_registry.py +0 -0
  255. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_orchestration.py +0 -0
  256. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_streaming.py +0 -0
  257. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_workflow.py +0 -0
  258. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/__init__.py +0 -0
  259. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/test_base.py +0 -0
  260. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/test_resources.py +0 -0
  261. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/test_scripts.py +0 -0
  262. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/test_sources.py +0 -0
  263. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_base.py +0 -0
  264. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_cancellation.py +0 -0
  265. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_composite.py +0 -0
  266. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_consecutive_agent.py +0 -0
  267. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_external.py +0 -0
  268. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_function_call.py +0 -0
  269. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_handoff.py +0 -0
  270. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_max_message.py +0 -0
  271. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_predicate.py +0 -0
  272. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_source.py +0 -0
  273. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_text_mention.py +0 -0
  274. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_timeout.py +0 -0
  275. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_token_usage.py +0 -0
  276. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_cancellation_token.py +0 -0
  277. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_context.py +0 -0
  278. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_logger.py +0 -0
  279. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_messages.py +0 -0
  280. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_package_api.py +0 -0
  281. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_session_store.py +0 -0
  282. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_types.py +0 -0
  283. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_vanilla_chunker.py +0 -0
  284. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/tools/test_coding_tools.py +0 -0
  285. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/tools/test_memory_tool.py +0 -0
  286. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/tools/test_research_tools.py +0 -0
  287. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/tools/test_tools.py +0 -0
  288. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/__init__.py +0 -0
  289. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/helpers.py +0 -0
  290. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_execution.py +0 -0
  291. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_package_api.py +0 -0
  292. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_registry.py +0 -0
  293. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_server.py +0 -0
  294. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_sessions.py +0 -0
  295. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_checkpoint.py +0 -0
  296. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_subworkflow_step.py +0 -0
  297. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_agent.py +0 -0
  298. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_class.py +0 -0
  299. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_models.py +0 -0
  300. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_runner.py +0 -0
  301. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_schema.py +0 -0
  302. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_steps.py +0 -0
  303. {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_visualizer.py +0 -0
@@ -4,6 +4,24 @@ 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.28.0] - 2026-09-15
8
+
9
+ ### Changed
10
+
11
+ - **Breaking:** A failed agent run no longer injects the raw `Error: ...` text as an assistant message into the run context. The error is surfaced on the new `AgentResponse.error` (`AgentErrorDetail`) field and via `final_content`; the conversation history stays clean so the error is not replayed to the model on later turns of the session. Consumers that read the error from `response.context.messages` must switch to `response.error`.
12
+
13
+ ### Added
14
+
15
+ - `RetryPolicy.max_retries_on_opt_in` (default `None`): a separate, lower retry cap for errors matched via `retryable_error_substrings`, so a usually-terminal marker (e.g. `invalid_prompt`) can fail fast while transient errors keep the full `max_retries`.
16
+ - Retry marker matching now also inspects structured provider fields (`code`, `body.error.code`) on the error and its cause, not just `str(error)`, so a provider rewording its message text cannot silently disable a marker.
17
+ - Retry exhaustion now emits a structured `logger.warning` summary and attaches the attempt history to the raised exception as `agentbyte_retry_history`.
18
+
19
+ ## [0.27.1] - 2026-09-13
20
+
21
+ ### Changed
22
+
23
+ - **Breaking:** Renamed `DuplicateToolResultMiddleware` (introduced in 0.27.0) to `DeduplicatingToolResultMiddleware`, an action-oriented name consistent with `DeduplicatingSkillsSource`. The old name is removed with no alias — update imports from `agentbyte.middleware`.
24
+
7
25
  ## [0.27.0] - 2026-09-12
8
26
 
9
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentbyte
3
- Version: 0.27.0
3
+ Version: 0.28.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.27.0**
89
+ Current release: **0.28.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.27.0**
5
+ Current release: **0.28.0**
6
6
 
7
7
  ## Building an Agent
8
8
 
@@ -0,0 +1,2 @@
1
+ __version__ = "0.28.0"
2
+ VERSION = __version__
@@ -13,6 +13,7 @@ from .base import (
13
13
  )
14
14
  from .embedding_agent import EmbeddingAgent
15
15
  from .types import (
16
+ AgentErrorDetail,
16
17
  AgentEvent,
17
18
  AgentResponse,
18
19
  BaseEvent,
@@ -58,4 +59,5 @@ __all__ = [
58
59
  "FatalErrorEvent",
59
60
  "EmbeddingBatchProgressEvent",
60
61
  "AgentResponse",
62
+ "AgentErrorDetail",
61
63
  ]
@@ -28,6 +28,7 @@ from agentbyte.tools import ApprovalMode, BaseTool
28
28
 
29
29
  from .base import AgentConfigurationError, AgentExecutionError, BaseAgent
30
30
  from .types import (
31
+ AgentErrorDetail,
31
32
  AgentEvent,
32
33
  AgentResponse,
33
34
  BaseEvent,
@@ -403,6 +404,7 @@ class Agent(BaseAgent):
403
404
  memory_operations = 0
404
405
  cost_estimate = 0.0
405
406
  finish_reason = "stop"
407
+ run_error: Optional[AgentErrorDetail] = None
406
408
 
407
409
  effective_stream_tokens = stream_tokens
408
410
  if stream_tokens and self.middleware_chain.middlewares:
@@ -694,12 +696,13 @@ class Agent(BaseAgent):
694
696
  )
695
697
  except Exception as exc:
696
698
  finish_reason = "error"
697
- error_message = AssistantMessage(
698
- content=f"Error: {exc}",
699
- source=self.name,
700
- )
701
- working_context.add_message(error_message)
702
- yield error_message
699
+ run_error = AgentErrorDetail(message=str(exc), error_type=type(exc).__name__)
700
+ # Deliberately NOT added to working_context.messages: a raw provider
701
+ # error persisted as an assistant turn is replayed to the model as
702
+ # real history on every later turn of the session. It is surfaced via
703
+ # AgentResponse.error (and the ErrorEvent below) instead, leaving the
704
+ # conversation history clean.
705
+ yield AssistantMessage(content=f"Error: {exc}", source=self.name)
703
706
  if verbose:
704
707
  yield ErrorEvent(
705
708
  source=self.name,
@@ -725,6 +728,7 @@ class Agent(BaseAgent):
725
728
  source=self.name,
726
729
  finish_reason=finish_reason,
727
730
  usage=usage,
731
+ error=run_error,
728
732
  )
729
733
 
730
734
  if verbose:
@@ -174,6 +174,24 @@ class EmbeddingBatchProgressEvent(BaseEvent):
174
174
  total_texts: int = Field(..., description="Total number of texts in this run")
175
175
 
176
176
 
177
+ class AgentErrorDetail(BaseModel):
178
+ """Structured detail for a run that ended with ``finish_reason="error"``.
179
+
180
+ A data record, not an exception — distinct from the ``AgentError`` exception
181
+ hierarchy in ``agentbyte.agents.base``. Carried on :class:`AgentResponse`
182
+ instead of being written into the conversation as an assistant turn: a raw
183
+ provider error string persisted as an assistant message would be replayed to
184
+ the model as genuine history on every subsequent turn of the session.
185
+ Surfacing it here keeps the message list clean while still letting callers
186
+ render or classify the failure.
187
+ """
188
+
189
+ model_config = ConfigDict(frozen=True)
190
+
191
+ message: str = Field(..., description="Developer-facing error text (the raw exception string)")
192
+ error_type: str = Field(..., description="Exception class name, e.g. 'InvalidRequestError'")
193
+
194
+
177
195
  class AgentResponse(BaseModel):
178
196
  """Final response payload for a completed agent run."""
179
197
 
@@ -186,6 +204,10 @@ class AgentResponse(BaseModel):
186
204
  description="Why the agent stopped: stop, approval_needed, max_iterations, max_iterations_finalized, tool_executed, error, cancelled",
187
205
  )
188
206
  usage: Usage = Field(default_factory=Usage, description="Aggregated usage")
207
+ error: Optional[AgentErrorDetail] = Field(
208
+ default=None,
209
+ description="Set when finish_reason='error'; the failure detail, kept out of the message history",
210
+ )
189
211
  timestamp: datetime = Field(
190
212
  default_factory=datetime.now,
191
213
  description="When the response was created",
@@ -208,7 +230,14 @@ class AgentResponse(BaseModel):
208
230
 
209
231
  @property
210
232
  def final_content(self) -> str:
211
- """Final message content if available."""
233
+ """Final message content if available.
234
+
235
+ When the run failed, the error text is not in ``context.messages`` (it is
236
+ deliberately kept out of the replayed history), so it is surfaced from
237
+ ``error`` here for display and logging.
238
+ """
239
+ if self.error is not None:
240
+ return f"Error: {self.error.message}"
212
241
  if not self.context.messages:
213
242
  return ""
214
243
  return self.context.messages[-1].content
@@ -315,4 +344,5 @@ __all__ = [
315
344
  "FatalErrorEvent",
316
345
  "EmbeddingBatchProgressEvent",
317
346
  "AgentResponse",
347
+ "AgentErrorDetail",
318
348
  ]
@@ -389,6 +389,12 @@ class AzureOpenAIChatCompletionClient(
389
389
  }
390
390
  return result
391
391
  except Exception as e:
392
+ if retry_history:
393
+ logger.warning(
394
+ "LLM call failed after retries for model=%s: %s",
395
+ self.model,
396
+ build_retry_summary(retry_history),
397
+ )
392
398
  self._handle_error(e)
393
399
 
394
400
  async def _create_internal(
@@ -156,6 +156,12 @@ class OpenAIChatCompletionClient(
156
156
  }
157
157
  return result
158
158
  except Exception as e:
159
+ if retry_history:
160
+ logger.warning(
161
+ "LLM call failed after retries for model=%s: %s",
162
+ self.model,
163
+ build_retry_summary(retry_history),
164
+ )
159
165
  self._handle_error(e)
160
166
 
161
167
  async def _create_internal(
@@ -29,6 +29,16 @@ class RetryPolicy(BaseModel):
29
29
  generic word, or unrelated failures will only be slowed before they surface.
30
30
  """
31
31
 
32
+ max_retries_on_opt_in: Optional[int] = None
33
+ """Retry cap applied only to errors matched via ``retryable_error_substrings``.
34
+
35
+ ``None`` (default) means opted-in errors use ``max_retries`` like everything
36
+ else — unchanged behaviour. Set a lower value to fail fast on markers that are
37
+ usually terminal (for example, retry ``invalid_prompt`` at most once) while
38
+ still giving genuinely transient errors (rate limits) the full ``max_retries``.
39
+ The effective cap is ``min(max_retries, max_retries_on_opt_in)``.
40
+ """
41
+
32
42
  @field_validator("max_retries")
33
43
  @classmethod
34
44
  def _non_negative_retries(cls, v: int) -> int:
@@ -36,6 +46,13 @@ class RetryPolicy(BaseModel):
36
46
  raise ValueError("max_retries must be >= 0")
37
47
  return v
38
48
 
49
+ @field_validator("max_retries_on_opt_in")
50
+ @classmethod
51
+ def _non_negative_opt_in_retries(cls, v: Optional[int]) -> Optional[int]:
52
+ if v is not None and v < 0:
53
+ raise ValueError("max_retries_on_opt_in must be >= 0")
54
+ return v
55
+
39
56
  @field_validator("initial_retry_delay", "max_retry_delay")
40
57
  @classmethod
41
58
  def _no_negative_delays(cls, v: float) -> float:
@@ -59,18 +76,52 @@ class RetryMixin:
59
76
 
60
77
  retry_policy: RetryPolicy
61
78
 
79
+ def _error_marker_haystack(self, error: BaseException) -> str:
80
+ """Lowercased text to match markers against: the rendered string plus
81
+ structured provider fields (``code``, ``body['error']['code']``) from the
82
+ error and its ``__cause__``. Superset of ``str(error)``, so this never
83
+ matches *less* than before."""
84
+ parts: list[str] = [str(error)]
85
+ seen: set[int] = set()
86
+ for obj in (error, getattr(error, "__cause__", None)):
87
+ if obj is None or id(obj) in seen:
88
+ continue
89
+ seen.add(id(obj))
90
+ code = getattr(obj, "code", None)
91
+ if code:
92
+ parts.append(str(code))
93
+ body = getattr(obj, "body", None)
94
+ if isinstance(body, dict):
95
+ inner = body.get("error")
96
+ if isinstance(inner, dict) and inner.get("code"):
97
+ parts.append(str(inner["code"]))
98
+ return " ".join(parts).lower()
99
+
62
100
  def _error_opted_into_retry(self, error: Exception) -> bool:
63
- """Return True if the caller marked this error's text as retryable.
101
+ """Return True if the caller marked this error as retryable.
64
102
 
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.
103
+ Matches ``retry_policy.retryable_error_substrings`` (case-insensitive)
104
+ against the rendered error string AND structured provider fields
105
+ (``code``, ``body.error.code``) of the error and its cause. This only ever
106
+ widens what is retried; it never suppresses an error.
68
107
  """
69
108
  markers = getattr(self.retry_policy, "retryable_error_substrings", None) or []
70
109
  if not markers:
71
110
  return False
72
- text = str(error).lower()
73
- return any(marker.lower() in text for marker in markers)
111
+ haystack = self._error_marker_haystack(error)
112
+ return any(marker.lower() in haystack for marker in markers)
113
+
114
+ def _effective_max_retries(self, error: Optional[Exception]) -> int:
115
+ """Retry cap for the current failure.
116
+
117
+ Opted-in (marker-matched) errors honour ``max_retries_on_opt_in`` when it
118
+ is set; everything else uses ``max_retries``. Never raises.
119
+ """
120
+ cap = self.retry_policy.max_retries
121
+ opt_cap = getattr(self.retry_policy, "max_retries_on_opt_in", None)
122
+ if opt_cap is not None and error is not None and self._error_opted_into_retry(error):
123
+ return min(cap, opt_cap)
124
+ return cap
74
125
 
75
126
  async def _retry_with_backoff(
76
127
  self,
@@ -82,7 +133,7 @@ class RetryMixin:
82
133
  """Execute *func* with exponential back-off on transient errors."""
83
134
  from .base import AuthenticationError, InvalidRequestError, RateLimitError
84
135
  from .types import ModelClientError
85
- from ._retry_observability import build_retry_record
136
+ from ._retry_observability import build_retry_record, build_retry_summary
86
137
 
87
138
  last_exception: Optional[Exception] = None
88
139
  current_delay = self.retry_policy.initial_retry_delay
@@ -113,10 +164,30 @@ class RetryMixin:
113
164
  raise
114
165
 
115
166
  # Reached only for a retryable error.
116
- if attempt >= self.retry_policy.max_retries:
167
+ effective_max = self._effective_max_retries(last_exception)
168
+ if attempt >= effective_max:
169
+ if _retry_history is not None:
170
+ _retry_history.append(
171
+ build_retry_record(
172
+ attempt + 1, current_delay, type(last_exception), "exhausted"
173
+ )
174
+ )
175
+ summary = build_retry_summary(_retry_history)
176
+ else:
177
+ summary = {}
117
178
  logger.warning(
118
- f"Max retries ({self.retry_policy.max_retries}) reached for {func.__name__}"
179
+ "Retries exhausted for %s after %d attempt(s): %s",
180
+ func.__name__,
181
+ attempt + 1,
182
+ summary,
119
183
  )
184
+ # Attach the history so a caller that catches the exception (or a
185
+ # provider adapter that re-wraps it) can still report what was
186
+ # tried. Best-effort: never let observability raise.
187
+ try:
188
+ setattr(last_exception, "agentbyte_retry_history", list(_retry_history or []))
189
+ except Exception: # noqa: BLE001 - attribute may be read-only on some exc types
190
+ pass
120
191
  raise last_exception
121
192
  if _retry_history is not None:
122
193
  _retry_history.append(
@@ -160,7 +231,7 @@ class RetryMixin:
160
231
  # already reached the caller (the response is half-said).
161
232
  if (
162
233
  saw_chunk
163
- or attempt >= self.retry_policy.max_retries
234
+ or attempt >= self._effective_max_retries(e)
164
235
  or not self._error_opted_into_retry(e)
165
236
  ):
166
237
  raise
@@ -184,7 +255,7 @@ class RetryMixin:
184
255
  except Exception as e:
185
256
  if (
186
257
  saw_chunk
187
- or attempt >= self.retry_policy.max_retries
258
+ or attempt >= self._effective_max_retries(e)
188
259
  or not self._error_opted_into_retry(e)
189
260
  ):
190
261
  raise
@@ -6,7 +6,7 @@ from .base import (
6
6
  ApprovalMiddleware,
7
7
  BaseMiddleware,
8
8
  ContextCompactionMiddleware,
9
- DuplicateToolResultMiddleware,
9
+ DeduplicatingToolResultMiddleware,
10
10
  GuardrailMiddleware,
11
11
  LoggingMiddleware,
12
12
  MetricsMiddleware,
@@ -41,7 +41,7 @@ __all__ = [
41
41
  "MiddlewareChain",
42
42
  "ApprovalMiddleware",
43
43
  "ContextCompactionMiddleware",
44
- "DuplicateToolResultMiddleware",
44
+ "DeduplicatingToolResultMiddleware",
45
45
  "LoggingMiddleware",
46
46
  "PIIRedactionMiddleware",
47
47
  "GuardrailMiddleware",
@@ -1054,7 +1054,7 @@ class ContextCompactionMiddleware(BaseMiddleware):
1054
1054
  return None
1055
1055
 
1056
1056
 
1057
- class DuplicateToolResultMiddleware(BaseMiddleware):
1057
+ class DeduplicatingToolResultMiddleware(BaseMiddleware):
1058
1058
  """Collapse repeated identical tool results in the model-call payload.
1059
1059
 
1060
1060
  A tool result stays in the conversation for the life of a session and is
@@ -0,0 +1,99 @@
1
+ """A failed agent run surfaces the error on AgentResponse.error, not in history.
2
+
3
+ Spec 0013 §3: the raw provider error must no longer be persisted into
4
+ ``context.messages`` as a synthetic assistant turn (which would be replayed to
5
+ the model on every later turn of the session). It is carried on
6
+ ``AgentResponse.error`` (an ``AgentErrorDetail``) and reflected by
7
+ ``final_content`` instead.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ import pytest
15
+
16
+ from agentbyte.agents import Agent, AgentErrorDetail
17
+ from agentbyte.llm.base import (
18
+ BaseChatCompletionClient,
19
+ BaseChatCompletionClientConfig,
20
+ InvalidRequestError,
21
+ )
22
+ from agentbyte.llm.types import ChatCompletionResult
23
+ from agentbyte.messages import Message
24
+
25
+
26
+ class _RaisingClientConfig(BaseChatCompletionClientConfig):
27
+ pass
28
+
29
+
30
+ class _RaisingModelClient(BaseChatCompletionClient):
31
+ """Model client whose ``create()`` always fails with a marked error."""
32
+
33
+ def __init__(self, error: Exception) -> None:
34
+ super().__init__(model="fake", client=object())
35
+ self._error = error
36
+
37
+ async def create(
38
+ self,
39
+ messages: List[Message],
40
+ tools: Optional[List[Dict[str, Any]]] = None,
41
+ output_format: Optional[type] = None,
42
+ **kwargs: Any,
43
+ ) -> ChatCompletionResult:
44
+ raise self._error
45
+
46
+ async def create_stream(self, *args: Any, **kwargs: Any): # pragma: no cover - unused
47
+ raise self._error
48
+ yield # pragma: no cover - generator marker
49
+
50
+ def _to_config(self) -> BaseChatCompletionClientConfig:
51
+ return _RaisingClientConfig(model=self.model, config=self.config)
52
+
53
+ @classmethod
54
+ def _from_config(cls, config: BaseChatCompletionClientConfig) -> "_RaisingModelClient":
55
+ return cls(error=RuntimeError("boom"))
56
+
57
+
58
+ def _agent() -> Agent:
59
+ client = _RaisingModelClient(
60
+ InvalidRequestError("Azure request invalid: invalid_prompt flagged")
61
+ )
62
+ return Agent(
63
+ name="assistant",
64
+ description="desc",
65
+ instructions="help",
66
+ model_client=client,
67
+ )
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_error_surfaced_on_response_not_in_history() -> None:
72
+ agent = _agent()
73
+
74
+ resp = await agent.run("hi")
75
+
76
+ assert resp.finish_reason == "error"
77
+ assert resp.error is not None
78
+ assert isinstance(resp.error, AgentErrorDetail)
79
+ assert resp.error.error_type == "InvalidRequestError"
80
+ assert "invalid_prompt" in resp.error.message
81
+ assert resp.final_content == f"Error: {resp.error.message}"
82
+
83
+ # The fix: the raw error is NOT in replayed history.
84
+ assert all("invalid_prompt" not in (m.content or "") for m in resp.context.messages)
85
+
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_second_run_on_same_context_does_not_resurrect_error() -> None:
89
+ agent = _agent()
90
+
91
+ resp = await agent.run("hi")
92
+ resp2 = await agent.run("again", context=resp.context)
93
+
94
+ assert resp2.finish_reason == "error"
95
+ # No earlier turn carries the error text back into the prompt.
96
+ assert all(
97
+ "invalid_prompt" not in (m.content or "")
98
+ for m in resp2.context.messages[:-1]
99
+ )
@@ -19,9 +19,10 @@ import pytest
19
19
 
20
20
  from agentbyte.llm.azure.chat import AzureOpenAIChatCompletionClient
21
21
  from agentbyte.llm.azure.embedding import AzureOpenAIEmbeddingClient
22
- from agentbyte.llm.base import RateLimitError
22
+ from agentbyte.llm.base import InvalidRequestError, RateLimitError
23
23
  from agentbyte.llm.openai.chat import OpenAIChatCompletionClient
24
24
  from agentbyte.llm.openai.embedding import OpenAIEmbeddingClient
25
+ from agentbyte.llm.retry_policy import RetryMixin, RetryPolicy
25
26
  from agentbyte.messages import UserMessage
26
27
 
27
28
 
@@ -72,6 +73,38 @@ def _fake_openai_client(chat_response=None, embedding_response=None) -> Any:
72
73
  # Helper: build a client that raises RateLimitError N times then succeeds
73
74
  # ---------------------------------------------------------------------------
74
75
 
76
+ class _Harness(RetryMixin):
77
+ """Minimal RetryMixin host with a configurable retry policy."""
78
+
79
+ def __init__(self, policy: RetryPolicy) -> None:
80
+ self.retry_policy = policy
81
+
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_exhaustion_records_history_and_attaches_to_exception():
85
+ """When opted-in retries are exhausted, the last record is 'exhausted' and the
86
+ history is attached to the raised exception as ``agentbyte_retry_history``."""
87
+ policy = RetryPolicy(
88
+ max_retries=2,
89
+ initial_retry_delay=0.0,
90
+ max_retry_delay=0.0,
91
+ retryable_error_substrings=["invalid_prompt"],
92
+ )
93
+ harness = _Harness(policy)
94
+ history: list = []
95
+
96
+ async def always_fail() -> str:
97
+ raise InvalidRequestError("invalid_prompt")
98
+
99
+ with pytest.raises(InvalidRequestError) as exc_info:
100
+ await harness._retry_with_backoff(always_fail, _retry_history=history)
101
+
102
+ assert history[-1]["outcome"] == "exhausted"
103
+ attached = getattr(exc_info.value, "agentbyte_retry_history", None)
104
+ assert attached is not None
105
+ assert attached[-1]["outcome"] == "exhausted"
106
+
107
+
75
108
  def _flaky_create(success_response: Any, fail_times: int):
76
109
  """Return an AsyncMock that raises RateLimitError fail_times then returns success."""
77
110
  call_count = 0
@@ -166,6 +166,105 @@ async def test_rate_limit_still_transient_without_opt_in() -> None:
166
166
  assert calls["n"] == 2
167
167
 
168
168
 
169
+ @pytest.mark.asyncio
170
+ async def test_opt_in_cap_lowers_retries_for_matched_error() -> None:
171
+ policy = _fast_policy(
172
+ max_retries=3,
173
+ max_retries_on_opt_in=1,
174
+ retryable_error_substrings=["invalid_prompt"],
175
+ )
176
+ harness = _Harness(policy)
177
+ calls = {"n": 0}
178
+
179
+ async def always_fail() -> str:
180
+ calls["n"] += 1
181
+ raise InvalidRequestError("invalid_prompt")
182
+
183
+ with pytest.raises(InvalidRequestError):
184
+ await harness._retry_with_backoff(always_fail)
185
+ # 1 initial + 1 opt-in retry.
186
+ assert calls["n"] == 2
187
+
188
+
189
+ @pytest.mark.asyncio
190
+ async def test_transient_error_keeps_full_cap_under_opt_in_cap() -> None:
191
+ policy = _fast_policy(
192
+ max_retries=3,
193
+ max_retries_on_opt_in=1,
194
+ retryable_error_substrings=["invalid_prompt"],
195
+ )
196
+ harness = _Harness(policy)
197
+ calls = {"n": 0}
198
+
199
+ async def always_fail() -> str:
200
+ calls["n"] += 1
201
+ raise RateLimitError("slow down")
202
+
203
+ with pytest.raises(RateLimitError):
204
+ await harness._retry_with_backoff(always_fail)
205
+ # Rate limit is not opted-in, so the opt-in cap must not shrink it: 1 + 3.
206
+ assert calls["n"] == 4
207
+
208
+
209
+ @pytest.mark.asyncio
210
+ async def test_structured_code_matches_without_marker_in_message() -> None:
211
+ policy = _fast_policy(max_retries=2, retryable_error_substrings=["invalid_prompt"])
212
+ harness = _Harness(policy)
213
+ calls = {"n": 0}
214
+
215
+ class CodedError(Exception):
216
+ def __init__(self, message: str, code: str) -> None:
217
+ super().__init__(message)
218
+ self.code = code
219
+
220
+ async def flaky() -> str:
221
+ calls["n"] += 1
222
+ if calls["n"] < 2:
223
+ raise CodedError("Error code: 400", code="invalid_prompt")
224
+ return "ok"
225
+
226
+ assert await harness._retry_with_backoff(flaky) == "ok"
227
+ assert calls["n"] == 2
228
+
229
+
230
+ @pytest.mark.asyncio
231
+ async def test_structured_body_error_code_matches() -> None:
232
+ policy = _fast_policy(max_retries=2, retryable_error_substrings=["invalid_prompt"])
233
+ harness = _Harness(policy)
234
+ calls = {"n": 0}
235
+
236
+ class BodyError(Exception):
237
+ def __init__(self, message: str, body: dict) -> None:
238
+ super().__init__(message)
239
+ self.body = body
240
+
241
+ async def flaky() -> str:
242
+ calls["n"] += 1
243
+ if calls["n"] < 2:
244
+ raise BodyError("Error code: 400", body={"error": {"code": "invalid_prompt"}})
245
+ return "ok"
246
+
247
+ assert await harness._retry_with_backoff(flaky) == "ok"
248
+ assert calls["n"] == 2
249
+
250
+
251
+ @pytest.mark.asyncio
252
+ async def test_default_opt_in_cap_none_is_unchanged() -> None:
253
+ policy = _fast_policy(max_retries=2, retryable_error_substrings=["invalid_prompt"])
254
+ harness = _Harness(policy)
255
+ assert policy.max_retries_on_opt_in is None
256
+ calls = {"n": 0}
257
+
258
+ async def always_fail() -> str:
259
+ calls["n"] += 1
260
+ raise InvalidRequestError("invalid_prompt")
261
+
262
+ with pytest.raises(InvalidRequestError):
263
+ await harness._retry_with_backoff(always_fail)
264
+ # Unchanged: 1 initial + max_retries.
265
+ assert calls["n"] == 3
266
+
267
+
169
268
  @pytest.mark.asyncio
170
269
  async def test_stream_retries_opted_in_error_before_any_chunk() -> None:
171
270
  policy = _fast_policy(max_retries=3, retryable_error_substrings=["invalid_prompt"])