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