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
+23 -7
View File
@@ -7,7 +7,7 @@ the library-desk API, offering:
- Wiki and document management
- Semantic search and knowledge graph exploration
"""
from typing import Any, Optional
from typing import Any
from pydantic_ai import Agent
@@ -27,7 +27,7 @@ from src.agents.librarian.tools import (
smart_create_wiki_page,
update_wiki_page,
)
from src.core.config import config
from src.agents.protocol import AgentError
from src.core.logging_config import get_logger
logger = get_logger(__name__)
@@ -138,7 +138,7 @@ If a tool fails or you cannot access a data source:
"""
# Lazy initialization to avoid connection issues during imports
_librarian_agent: Optional[Agent[None, str]] = None
_librarian_agent: Agent[None, str] | None = None
def _create_librarian_agent() -> Agent[None, str]:
@@ -204,7 +204,7 @@ def get_librarian_agent() -> Agent[None, str]:
async def run_librarian(
task: str,
context: str = "",
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
) -> str:
"""
Execute a research task with The Librarian.
@@ -220,6 +220,10 @@ async def run_librarian(
Returns:
Research results and findings
Raises:
AgentError: If the research task fails. Exception detail is
logged here; callers map the failure to a user-safe message.
Example:
result = await run_librarian(
task="Find information about Docker networking",
@@ -255,19 +259,23 @@ async def run_librarian(
return result.output
except Exception as e:
# Full detail stays in the logs; callers receive a structured
# failure instead of error text masquerading as research output.
logger.error(
"librarian_task_error",
task=task[:50],
error=str(e),
exc_info=True,
)
return f"The Librarian encountered an error: {str(e)}"
raise AgentError(
"Research task failed", agent_name="librarian"
) from e
async def run_librarian_stream(
task: str,
context: str = "",
message_history: Optional[list[Any]] = None,
message_history: list[Any] | None = None,
):
"""
Execute a research task with streaming output.
@@ -282,6 +290,10 @@ async def run_librarian_stream(
Yields:
str: Text deltas from the response
Raises:
AgentError: If the research task fails. Exception detail is
logged here; callers map the failure to a user-safe message.
Example:
async for delta in run_librarian_stream("Find Docker docs"):
print(delta, end="", flush=True)
@@ -309,10 +321,14 @@ async def run_librarian_stream(
logger.info("librarian_stream_completed", task=task[:50])
except Exception as e:
# Full detail stays in the logs; raise instead of yielding error
# text into the stream as if it were research output.
logger.error(
"librarian_stream_error",
task=task[:50],
error=str(e),
exc_info=True,
)
yield f"\n\nThe Librarian encountered an error: {str(e)}"
raise AgentError(
"Research task failed", agent_name="librarian"
) from e