fix(agents): make librarian failures structured and user-safe

- run_librarian / run_librarian_stream raise AgentError instead of
  returning/yielding error text as normal output; detail stays in logs
- delegate_to_* wrappers now put a curated butler-toned sentence in
  DelegationResult.output on failure and never expose str(e), so
  streaming's error branch is reachable and honest
- _execute_single_delegation propagates success; direct delegation only
  records delegate_to_* as called when the expert actually succeeded
- librarian tools return user-safe messages instead of
  'Error searching: {e}' strings that leaked internal URLs into
  synthesis; coordination stream errors are curated as well
- ruff cleanups (TYPE_CHECKING forward refs, B904, unused locals) in
  the touched files to keep them lint-clean

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 10:12:30 +02:00
co-authored by Claude Fable 5
parent 59f5b54ac9
commit f853db8ccc
10 changed files with 277 additions and 140 deletions
+17 -9
View File
@@ -4,18 +4,19 @@ Tests for delegation infrastructure.
Tests the DelegationTask dataclass and delegation wrapper functions
that implement the agent-as-tool pattern.
"""
from unittest.mock import AsyncMock, patch
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from src.agents.delegation import (
ActionType,
DelegationTask,
DelegationResult,
HOUSEHOLD_THINK_MESSAGES,
STREAMING_DELEGATION_WRAPPERS,
ActionType,
DelegationResult,
DelegationTask,
_detect_action_type,
delegate_to_librarian,
get_think_message,
_detect_action_type,
)
@@ -170,11 +171,11 @@ class TestDelegateToLibrarian:
@pytest.mark.asyncio
async def test_delegate_to_librarian_handles_error(self):
"""Test delegation handles Librarian errors gracefully."""
"""Test delegation maps Librarian errors to a user-safe result."""
with patch(
"src.agents.librarian.agent.run_librarian",
new_callable=AsyncMock,
side_effect=Exception("Connection refused"),
side_effect=Exception("Connection refused to http://internal:8089"),
):
result = await delegate_to_librarian(
task="Search for information",
@@ -182,8 +183,15 @@ class TestDelegateToLibrarian:
assert isinstance(result, DelegationResult)
assert result.success is False
assert result.output == ""
assert result.error == "Connection refused"
# Output carries a curated butler-toned sentence
assert result.output == get_think_message(
"librarian", "Search for information", "error"
)
# Exception detail stays in logs only - never in the result
assert "Connection refused" not in result.output
assert result.error is not None
assert "Connection refused" not in result.error
assert "internal" not in result.error
@pytest.mark.asyncio
async def test_delegate_to_librarian_preserves_task(self):