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
+19 -12
View File
@@ -8,12 +8,12 @@ returns a structured result for synthesis.
This implements the agent-as-tool pattern recommended by PydanticAI:
agents call other agents via tool wrappers, keeping each agent focused.
"""
from collections.abc import AsyncGenerator
from dataclasses import dataclass, field
from enum import Enum
from typing import AsyncGenerator, Callable, Optional, Any
from src.core.logging_config import get_logger
from src.core.tracing import trace_span, SpanType
from src.core.tracing import SpanType, trace_span
logger = get_logger(__name__)
@@ -167,7 +167,7 @@ class DelegationTask:
action: str = ""
priority: int = 0
depends_on: list[str] = field(default_factory=list)
result: Optional[str] = None
result: str | None = None
task_id: str = ""
def __post_init__(self):
@@ -186,14 +186,16 @@ class DelegationResult:
expert_name: Which expert handled the task
task: Original task description
success: Whether the delegation succeeded
output: Expert's response/findings
error: Error message if failed
output: Expert's response/findings. On failure this holds a
curated, user-safe butler sentence (never exception detail)
error: Short user-safe error label if failed. Exception detail
stays in the logs only
"""
expert_name: str
task: str
success: bool
output: str
error: Optional[str] = None
error: str | None = None
async def delegate_to_librarian(
@@ -285,12 +287,15 @@ async def delegate_to_librarian(
span.metadata["success"] = False
span.details["error"] = str(e)
# Exception detail stays in the logs; the user-facing output
# is a curated butler sentence so internals never leak into
# synthesis.
return DelegationResult(
expert_name="librarian",
task=task,
success=False,
output="",
error=str(e),
output=get_think_message("librarian", task, "error"),
error="The Librarian was unable to complete the task.",
)
@@ -383,12 +388,13 @@ async def delegate_to_biographer(
span.metadata["success"] = False
span.details["error"] = str(e)
# Exception detail stays in the logs only.
return DelegationResult(
expert_name="biographer",
task=task,
success=False,
output="",
error=str(e),
output=get_think_message("biographer", task, "error"),
error="The Biographer was unable to complete the task.",
)
@@ -480,12 +486,13 @@ async def delegate_to_housekeeper(
span.metadata["success"] = False
span.details["error"] = str(e)
# Exception detail stays in the logs only.
return DelegationResult(
expert_name="housekeeper",
task=task,
success=False,
output="",
error=str(e),
output=get_think_message("housekeeper", task, "error"),
error="The Housekeeper was unable to complete the task.",
)