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.
- {agentbyte-0.27.0 → agentbyte-0.28.0}/CHANGELOG.md +18 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/PKG-INFO +2 -2
- {agentbyte-0.27.0 → agentbyte-0.28.0}/README.md +1 -1
- agentbyte-0.28.0/src/agentbyte/__about__.py +2 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/__init__.py +2 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/agent.py +10 -6
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/types.py +31 -1
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/chat.py +6 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai/chat.py +6 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/retry_policy.py +82 -11
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/__init__.py +2 -2
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/base.py +1 -1
- agentbyte-0.28.0/tests/agents/test_agent_error_response.py +99 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_retry_observability.py +34 -1
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_retryable_error_substrings.py +99 -0
- agentbyte-0.27.0/tests/middleware/test_duplicate_tool_result.py → agentbyte-0.28.0/tests/middleware/test_deduplicate_tool_result.py +14 -14
- agentbyte-0.27.0/src/agentbyte/__about__.py +0 -2
- {agentbyte-0.27.0 → agentbyte-0.28.0}/.gitignore +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/LICENSE +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/pyproject.toml +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/agent_as_tool.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/agents/embedding_agent.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/cancellation_token.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/catalog.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/cli/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/cli/main.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/component.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context_providers/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context_providers/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context_providers/skill_tools.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/context_providers/skills.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/config.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/importer.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/json.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/loader.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/publish.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/publish_config.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/publishers.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/sources.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/sqlite.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/sqlite_db.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/write_config.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/dataset/writers.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/entity.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/decorator.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/keyword.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/local.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/tool.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/checks/types.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/comparison.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/eval_dataset.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/composite.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/llm.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/pairwise.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/judges/reference.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/pairwise.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/report.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/runner.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/splitting.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/agent.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/model.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/multi_turn.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/targets/orchestrator.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/eval/types.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/_retry_observability.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/auth.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/auth.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/embedding.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure/settings.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure_openai.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/azure_openai_embedding.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/embeddings_base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai/embedding.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai/settings.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/openai_embedding.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/pricing.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/settings.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/llm/types.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/logger.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/memory/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/memory/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/messages.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/otel.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/retry.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/sql_usage.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/middleware/usage_logger.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/notebook.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/config.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/gepa.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/mipro.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/pareto.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/reflective.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/spec.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/optim/trace.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/ai.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/handoff.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/plan.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/policies.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/orchestration/round_robin.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/agents.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/clients.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instruction_registry.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/orchestrator.yaml +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/query_rewriter.yaml +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/researcher.yaml +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/reviewer.yaml +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/instructions/writer.yaml +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/orchestration.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/contracts-analyst/SKILL.md +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/hr-analyst/SKILL.md +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/hr-analyst/resources/departments.md +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/hr-analyst/resources/employees.md +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/skills/hr-analyst/resources/payroll.md +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/streaming.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/presets/workflow.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/session_store.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/resources.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/scripts.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/sources.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/skills/validation.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/cancellation.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/composite.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/consecutive_agent.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/external.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/function_call.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/handoff.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/max_message.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/predicate.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/source.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/text_mention.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/timeout.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/termination/token_usage.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/coding_tools.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/core_tools.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/decorator.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/memory_tool.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/tools/research_tools.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/types.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/discovery.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/execution.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/models.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/registry.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/server.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/session_store.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/sessions.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/ui/assets/index-BF3DwXaF.js +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/ui/assets/index-ar5tOeqt.css +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/ui/index.html +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/webui/ui/vite.svg +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/agent.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/_structure_hash.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/checkpoint.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/models.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/runner.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/core/workflow.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/defaults.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/loader.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/schema.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/schema_utils.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/agentbyte_agent.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/echo.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/function.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/http.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/step.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/subworkflow.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/steps/transform.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/src/agentbyte/workflow/visualizer.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_as_tool.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_basic.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_context_providers.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_event_types.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_memory_integration.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_middleware_integration.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_response_accessors.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_retry_middleware.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_agent_stream_events.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_embedding_agent.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/agents/test_tool_approval.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/cli/test_registry_check.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/context_providers/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/context_providers/test_skill_tools.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/context_providers/test_skills_provider.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/dataset/test_loader.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/dataset/test_multi_table.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/dataset/test_publish.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/dataset/test_sqlite_db.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_eval_dataset.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_multi_turn.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_pairwise.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_phase1_runner_and_targets.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_phase2_checks_and_reports.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_splitting.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/eval/test_types_and_judges.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_azure_client.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_azure_embedding_client.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_llm_types.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_openai_client.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_openai_embedding_client.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_pricing.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/llm/test_retry_policy_api.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/memory/test_memory.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_middleware_chain.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_otel.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_retry_middleware.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_sql_usage.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/middleware/test_usage_logger.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_base_integration.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_config.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_gepa.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_mipro.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_pareto.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_reflective.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_spec.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/optim/test_trace.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_ai_orchestrator.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_base_orchestrator.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_handoff_orchestrator.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_plan_orchestrator.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/orchestration/test_round_robin.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_agents.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_clients.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_instruction_registry.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_orchestration.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_streaming.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/presets/test_workflow.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/test_base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/test_resources.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/test_scripts.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/skills/test_sources.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_base.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_cancellation.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_composite.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_consecutive_agent.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_external.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_function_call.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_handoff.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_max_message.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_predicate.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_source.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_text_mention.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_timeout.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/termination/test_token_usage.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_cancellation_token.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_context.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_logger.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_messages.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_package_api.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_session_store.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_types.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/test_vanilla_chunker.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/tools/test_coding_tools.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/tools/test_memory_tool.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/tools/test_research_tools.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/tools/test_tools.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/__init__.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/helpers.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_execution.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_package_api.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_registry.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_server.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/webui/test_sessions.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_checkpoint.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_subworkflow_step.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_agent.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_class.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_models.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_runner.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_schema.py +0 -0
- {agentbyte-0.27.0 → agentbyte-0.28.0}/tests/workflow/test_workflow_steps.py +0 -0
- {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.
|
|
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.
|
|
89
|
+
Current release: **0.28.0**
|
|
90
90
|
|
|
91
91
|
## Building an Agent
|
|
92
92
|
|
|
@@ -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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
|
101
|
+
"""Return True if the caller marked this error as retryable.
|
|
64
102
|
|
|
65
|
-
Matches ``
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
return any(marker.lower() in
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
|
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"])
|