Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
334f0c6
fix: add safe tool execution fallback policy
Aug 15, 2026
7e1e7dd
fix: harden HTTP tool failure classification
Aug 15, 2026
4e6512c
fix: address tool fallback review findings
Aug 15, 2026
c796661
docs: specify fail-closed HTTP and SSE contract
seonghobae Aug 15, 2026
fbe50d3
fix: address follow-up tool fallback review
github-actions[bot] Aug 16, 2026
4407046
fix: bound tool fallback retries
seonghobae Aug 20, 2026
6cc4466
docs: remove plan whitespace
seonghobae Aug 20, 2026
2351cab
fix(tools): honor suppressed exception contexts
seonghobae Aug 20, 2026
ac7d546
fix(security): require object policy for external admin reads
seonghobae Aug 20, 2026
9794cff
fix(security): pass principal token to resource policy
seonghobae Aug 20, 2026
b762782
test(tool): preserve structured metadata through wrappers
seonghobae Aug 20, 2026
b706952
fix(tools): preserve structured failures through wrappers
seonghobae Aug 20, 2026
f60adbc
docs: record external admin resource policy
seonghobae Aug 20, 2026
5bbec44
refactor: keep tool failure PR focused
seonghobae Aug 20, 2026
e1d4170
test: repair fallback regression checks
seonghobae Aug 20, 2026
1329dc7
fix: contain raw provider failures
seonghobae Aug 21, 2026
1ccb868
fix: sever fail closed tool error causes
seonghobae Aug 21, 2026
e258875
fix: harden tool failure retry boundaries
seonghobae Aug 21, 2026
cc806cd
fix: stop malformed provider failover
seonghobae Aug 21, 2026
276ed4f
fix: preserve terminal provider tool failures
seonghobae Aug 21, 2026
d7b4f68
fix: preserve provider tool-stop failures (#811)
seonghobae Aug 21, 2026
035d08a
fix: count safe retries in circuit health
seonghobae Aug 21, 2026
b24b3a3
refactor: remove unreachable provider error branch
seonghobae Aug 21, 2026
c05ab7e
fix: remove stale fallback error state
seonghobae Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Changelog

All notable changes to this project are documented in this file.

## [Unreleased]

### Added

- Structured tool failure categories, stable fallback actions, and public adapter exceptions.
- Secret-free `tool_fallback_decision` audit events.
- Exact regression coverage for the Strix `Tool execute_command not found in agent strix` failure.

### Changed

- Agent invocation now retries explicitly idempotent transient tool failures with bounded exponential backoff within a per-agent budget.
- A shared four-attempt ceiling now bounds the configured same-agent tool retry budget.
- Fail-closed tool decisions now have dedicated JSON and SSE error contracts, and preserve the observed failure kind in secret-free audit evidence.
- Missing or unavailable tools move to the next eligible agent instead of terminating the workflow immediately.

### Security

- Ambiguous non-idempotent outcomes, invalid arguments, permission denial, and policy denial fail closed.
- Fallback errors and audit events do not copy provider exception text, tool arguments, outputs, or credentials; fail-closed exceptions also sever the original cause chain so later traceback logging cannot recover them.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ python tests/test_admin_contract.py
python tests/test_conventions.py
python tests/test_api_contract.py
python tests/test_security_hardening.py
python tests/test_tool_execution_fallback.py
python tests/test_repository_security_metadata.py
python tests/test_product_planning_contract.py
python tests/test_plugin_driven_artifacts.py
Expand Down
17 changes: 17 additions & 0 deletions contextual_orchestrator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@
from .kv_config import InMemoryConfigStore, get_config_store
from .orchestrator import ModelAgent, TaskOrchestrator, WorkflowStep, load_agents
from .token_counting import HeuristicTokenCounter, build_token_counter
from .tool_fallback import (
MAX_TOOL_RETRY_ATTEMPTS,
ToolExecutionError,
ToolFallbackAction,
ToolFallbackStoppedError,
ToolFailureDecision,
ToolFailureKind,
classify_tool_failure,
)

__all__ = [
"ModelAgent",
Expand All @@ -48,6 +57,14 @@
"get_credential",
"register_credential",
"NotConfigured",
"MAX_TOOL_RETRY_ATTEMPTS",
# tool fallback
"ToolExecutionError",
"ToolFallbackAction",
"ToolFallbackStoppedError",
"ToolFailureDecision",
"ToolFailureKind",
"classify_tool_failure",
# cost review
"ATTRIBUTION_DIMENSIONS",
"AttributionDimensions",
Expand Down
20 changes: 17 additions & 3 deletions contextual_orchestrator/model_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
DISCOVERY_TIMEOUT_SECONDS = 15.0


def _provider_discovery_error_code(exc: Exception) -> str:
"""Map provider failures to stable codes without retaining provider response text."""
if isinstance(exc, urllib.error.HTTPError):
return f"http_status_{exc.code}"
if isinstance(exc, TimeoutError):
return "timeout"
if isinstance(exc, urllib.error.URLError):
return "transport_error"
if isinstance(exc, ValueError):
return "invalid_response"
return "provider_error"


@dataclass(frozen=True)
class ProviderModelSource:
"""Where and how to discover one provider's models."""
Expand Down Expand Up @@ -99,9 +112,10 @@ class DiscoveredModel:
class ProviderDiscoveryError(RuntimeError):
"""Raised when a provider's model list could not be fetched (network/auth failure)."""

def __init__(self, provider_name: str, detail: str) -> None:
def __init__(self, provider_name: str, error_code: str) -> None:
self.provider_name = provider_name
super().__init__(f"model discovery failed for provider {provider_name!r}: {detail}")
self.error_code = error_code
super().__init__(f"model discovery failed for provider {provider_name!r}: {error_code}")


def _fetch_json(url: str, *, api_key: str, auth_scheme: str, timeout: float) -> Any:
Expand Down Expand Up @@ -191,7 +205,7 @@ def discover_provider_models(
try:
payload = _fetch_json(url, api_key=api_key, auth_scheme=source.auth_scheme, timeout=timeout)
except (urllib.error.URLError, TimeoutError, ValueError) as exc: # pragma: no cover - network path
raise ProviderDiscoveryError(source.provider_name, str(exc)) from exc
raise ProviderDiscoveryError(source.provider_name, _provider_discovery_error_code(exc)) from None
if source.style == "bytez":
return _parse_bytez(payload, source)
return _parse_openai_compatible(payload, source)
Expand Down
Loading
Loading