feat(agents): enforce one librarian timeout budget
- add LIBRARIAN_TIMEOUT config (default 180s) and enforce it with asyncio.wait_for inside delegate_to_librarian, covering the live paths (steward direct delegation and SSE streaming) that had no cap - timeouts fail honestly: success=False with a curated butler sentence, detail in logs - set an explicit timeout on TatlockOllamaProvider's AsyncOpenAI client from OLLAMA_TIMEOUT instead of the SDK default (~600s per LLM call) - remove the contradictory unused 60s default from AgentRequest.timeout_seconds; coordination falls back to the configured budget Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -193,6 +193,30 @@ class TestDelegateToLibrarian:
|
||||
assert "Connection refused" not in result.error
|
||||
assert "internal" not in result.error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delegate_to_librarian_timeout(self, monkeypatch):
|
||||
"""Delegation is capped by LIBRARIAN_TIMEOUT and fails honestly."""
|
||||
import asyncio
|
||||
|
||||
from src.core.config import config
|
||||
|
||||
async def slow_run(task, context=""):
|
||||
await asyncio.sleep(5)
|
||||
return "too late"
|
||||
|
||||
monkeypatch.setattr(config, "LIBRARIAN_TIMEOUT", 0.05)
|
||||
|
||||
with patch(
|
||||
"src.agents.librarian.agent.run_librarian",
|
||||
new=slow_run,
|
||||
):
|
||||
result = await delegate_to_librarian(task="Search for information")
|
||||
|
||||
assert result.success is False
|
||||
assert "longer than expected" in result.output
|
||||
assert result.error is not None
|
||||
assert "time budget" in result.error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delegate_to_librarian_preserves_task(self):
|
||||
"""Test delegation result preserves original task."""
|
||||
|
||||
@@ -27,7 +27,8 @@ class TestAgentRequest:
|
||||
|
||||
assert request.task == "Find information about Docker"
|
||||
assert request.context == ""
|
||||
assert request.timeout_seconds == 60
|
||||
# No hardcoded default - None defers to the configured budget
|
||||
assert request.timeout_seconds is None
|
||||
|
||||
def test_request_with_context(self):
|
||||
"""Test request with additional context."""
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Tests for TatlockOllamaProvider configuration.
|
||||
|
||||
The AsyncOpenAI client must carry an explicit timeout from
|
||||
config.OLLAMA_TIMEOUT instead of the SDK default (~600s), so a stuck
|
||||
LLM call cannot consume the whole delegation budget.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.config import config
|
||||
from src.ollama.provider import TatlockOllamaProvider, _sanitize_messages
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestProviderTimeout:
|
||||
"""Timeout configuration on the underlying AsyncOpenAI client."""
|
||||
|
||||
def test_openai_client_timeout_from_config(self):
|
||||
provider = TatlockOllamaProvider(base_url="http://localhost:11434/v1")
|
||||
|
||||
assert provider._openai_client.timeout == float(config.OLLAMA_TIMEOUT)
|
||||
|
||||
def test_timeout_is_not_sdk_default(self):
|
||||
provider = TatlockOllamaProvider(base_url="http://localhost:11434/v1")
|
||||
|
||||
# The OpenAI SDK defaults to 600s; the configured cap must win
|
||||
assert provider._openai_client.timeout < 600
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMessageSanitization:
|
||||
"""Null content sanitization for Ollama compatibility."""
|
||||
|
||||
def test_null_content_with_tool_calls_becomes_empty_string(self):
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{"id": "call_1", "type": "function"}],
|
||||
}
|
||||
]
|
||||
|
||||
sanitized = _sanitize_messages(messages)
|
||||
|
||||
assert sanitized[0]["content"] == ""
|
||||
|
||||
def test_regular_messages_unchanged(self):
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Good day, sir."},
|
||||
]
|
||||
|
||||
assert _sanitize_messages(messages) == messages
|
||||
Reference in New Issue
Block a user