diff --git a/solstone/convey/provider_readiness.py b/solstone/convey/provider_readiness.py index 8b3807efb..6b61476c3 100644 --- a/solstone/convey/provider_readiness.py +++ b/solstone/convey/provider_readiness.py @@ -10,6 +10,11 @@ from solstone.think.providers.local_endpoint import ( LOCAL_ENDPOINT_CONTRACT_COPY, LOCAL_ENDPOINT_UNREACHABLE_COPY, ) +from solstone.think.responsiveness import ( + NON_RESPONSIVE_READINESS_DETAIL, + NON_RESPONSIVE_READINESS_SUMMARY, + NON_RESPONSIVE_REASON_CODE, +) @dataclass(frozen=True) @@ -354,6 +359,12 @@ _ENTRIES: dict[str, _Entry] = { detail="Try again or choose another provider.", recovery_action=None, ), + NON_RESPONSIVE_REASON_CODE: _Entry( + klass="generic", + summary=NON_RESPONSIVE_READINESS_SUMMARY, + detail=NON_RESPONSIVE_READINESS_DETAIL, + recovery_action=_THINKING_ACTION, + ), "token_budget_exceeded": _Entry( klass="generic", summary="this run reached its resource budget before finishing", diff --git a/solstone/convey/static/chat_reasons.js b/solstone/convey/static/chat_reasons.js index d3966eb43..3ed38496f 100644 --- a/solstone/convey/static/chat_reasons.js +++ b/solstone/convey/static/chat_reasons.js @@ -166,6 +166,10 @@ "template": "I didn't get a response", "action": null }, + "non_responsive": { + "template": "the thinking engine isn't answering requests", + "action": {"label": "Open Thinking", "href": "/app/thinking/#main"} + }, "token_budget_exceeded": { "template": "this run reached its resource budget before finishing", "action": null diff --git a/solstone/think/cogitate_policy.py b/solstone/think/cogitate_policy.py index 767ad5faf..1cb253430 100644 --- a/solstone/think/cogitate_policy.py +++ b/solstone/think/cogitate_policy.py @@ -16,6 +16,7 @@ from solstone.think.cogitate_contract import ( COGITATE_READ_TOOL_NAMES, capabilities_for_access_tier, ) +from solstone.think.responsiveness import NON_RESPONSIVE_REASON_CODE MAX_TURNS = 60 DEFAULT_RUN_COST_CAP_USD = 1.00 @@ -44,6 +45,7 @@ DETERMINISTIC_FAILURE_REASON_CODES = frozenset( "max_turns_exhausted", "model_not_found", "no_output", + NON_RESPONSIVE_REASON_CODE, "provider_request_rejected", "schema_invalid", "token_budget_exceeded", @@ -65,6 +67,7 @@ DETERMINISTIC_FAILURE_CAPS: dict[str, int] = { "max_turns_exhausted": 2, "model_not_found": 1, "no_output": 2, + NON_RESPONSIVE_REASON_CODE: 2, "provider_request_rejected": 1, "schema_invalid": 3, "token_budget_exceeded": 2, diff --git a/tests/test_chat_reasons.py b/tests/test_chat_reasons.py index bd94efbfd..e16c7e430 100644 --- a/tests/test_chat_reasons.py +++ b/tests/test_chat_reasons.py @@ -50,6 +50,7 @@ EXPECTED_CODES = { "incomplete_text_length", "max_turns_exhausted", "no_output", + "non_responsive", "token_budget_exceeded", "wall_clock_exceeded", "unknown", diff --git a/tests/test_cogitate_policy.py b/tests/test_cogitate_policy.py index d70e5f4da..eda93a823 100644 --- a/tests/test_cogitate_policy.py +++ b/tests/test_cogitate_policy.py @@ -8,6 +8,7 @@ from pathlib import Path import pytest from solstone.think import cogitate_policy +from solstone.think.responsiveness import NON_RESPONSIVE_REASON_CODE def _policy( @@ -54,6 +55,11 @@ def test_failure_capped_provider_request_rejected_is_one(): assert cogitate_policy.failure_capped("provider_request_rejected", 1) is True +def test_failure_capped_non_responsive_cap_is_two(): + assert cogitate_policy.failure_capped(NON_RESPONSIVE_REASON_CODE, 1) is False + assert cogitate_policy.failure_capped(NON_RESPONSIVE_REASON_CODE, 2) is True + + def test_deterministic_failure_caps_cover_reason_codes_exactly(): assert ( set(cogitate_policy.DETERMINISTIC_FAILURE_CAPS) diff --git a/tests/test_provider_readiness_presenter.py b/tests/test_provider_readiness_presenter.py index 4894ad210..09c547141 100644 --- a/tests/test_provider_readiness_presenter.py +++ b/tests/test_provider_readiness_presenter.py @@ -16,6 +16,11 @@ from solstone.convey.provider_readiness import ( from solstone.think.providers import shared from solstone.think.providers.local import ContextBudgetExceeded, LocalCapacityExhausted from solstone.think.providers.local_admission import LocalAdmissionTimeout +from solstone.think.responsiveness import ( + NON_RESPONSIVE_READINESS_DETAIL, + NON_RESPONSIVE_READINESS_SUMMARY, + NON_RESPONSIVE_REASON_CODE, +) def local_provider_error_codes() -> set[str]: @@ -69,6 +74,23 @@ def test_explicit_extra_codes_are_mapped(): assert "no_output" in mapped +def test_non_responsive_readiness_copy_is_generic_actionable(): + view = present_for_reason( + NON_RESPONSIVE_REASON_CODE, + provider="google", + model="test-model", + status="blocked", + ) + + assert view.summary == NON_RESPONSIVE_READINESS_SUMMARY + assert view.detail == NON_RESPONSIVE_READINESS_DETAIL + assert view.recovery_action is not None + assert view.recovery_action.label == "Open Thinking" + assert view.recovery_action.target == "/app/thinking/#main" + assert is_blocking_reason(NON_RESPONSIVE_REASON_CODE) is False + assert backlog_reason_category(NON_RESPONSIVE_REASON_CODE) == "generic" + + def test_local_runtime_exception_codes_are_registered(): mapped = mapped_reason_codes() for code in (