fix(librarian): quiet healthy-search coverage notes, tag clearing, strict result pairing
Phase A review minors: - Coverage note: source_status (when present) is now used exclusively; the source_counts-absence fallback only considers the optional legs the request explicitly enabled (web/documents/volatile). library-desk computes source_counts from the final top-N fused results only, so absence of the always-on vector/graph legs is normal ranking behavior - the old heuristic warned on virtually every healthy search - update_wiki_page: the empty-list tags sentinel (leave unchanged) made clearing all tags impossible; pass exactly ["__CLEAR__"] to send an empty tag list, documented in the docstring for the local model - Text-delegation parallel fallback: zip(..., strict=True) with an explicit count-mismatch guard so results can never be silently attributed to the wrong agent Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
This commit is contained in:
@@ -55,8 +55,13 @@ def _coverage_note(
|
||||
enabled source leg contributed nothing, so outages stay visible to
|
||||
the model and the user instead of silently narrowing results.
|
||||
|
||||
Prefers the additive source_status/degraded contract when present;
|
||||
falls back to inferring silent legs from source_counts.
|
||||
When the additive source_status/degraded contract is present it is
|
||||
authoritative and used EXCLUSIVELY - no count heuristics. Without
|
||||
it, absence from source_counts is only inferred for the optional
|
||||
legs this request explicitly enabled (web/documents/volatile);
|
||||
the always-on wiki legs (vector/graph) are never inferred, because
|
||||
source_counts only tallies the sources of the final top-N fused
|
||||
results, so their absence is normal ranking behavior, not an outage.
|
||||
"""
|
||||
if response.source_status:
|
||||
failed = sorted(
|
||||
@@ -80,7 +85,9 @@ def _coverage_note(
|
||||
# Older library-desk without per-source reporting - nothing to infer
|
||||
return ""
|
||||
|
||||
expected = {"vector", "graph"}
|
||||
# Only legs the request explicitly enabled; never vector/graph (their
|
||||
# absence from the top-N counts is healthy, see docstring)
|
||||
expected = set()
|
||||
if include_web:
|
||||
expected.add("web")
|
||||
if include_documents:
|
||||
@@ -88,8 +95,8 @@ def _coverage_note(
|
||||
if include_volatile:
|
||||
expected.add("volatile")
|
||||
|
||||
# Normalize count keys to leg names (document/documents, wiki/vector)
|
||||
aliases = {"document": "documents", "wiki": "vector"}
|
||||
# Normalize count keys to leg names (document/documents)
|
||||
aliases = {"document": "documents"}
|
||||
reported = {aliases.get(key, key) for key in response.source_counts}
|
||||
missing = sorted(expected - reported)
|
||||
if missing:
|
||||
@@ -791,6 +798,9 @@ async def read_urls_batch(
|
||||
# Wiki Write Operations
|
||||
# ============================================================================
|
||||
|
||||
CLEAR_TAGS_SENTINEL = "__CLEAR__"
|
||||
|
||||
|
||||
async def update_wiki_page(
|
||||
page_id: int,
|
||||
content: str = "",
|
||||
@@ -817,7 +827,9 @@ async def update_wiki_page(
|
||||
page_id: ID of the page to update (get from search_wiki results)
|
||||
content: New markdown content (empty = leave unchanged)
|
||||
title: New title (empty = leave unchanged)
|
||||
tags: New tag list, replaces existing tags (empty = leave unchanged)
|
||||
tags: New tag list, replaces existing tags (empty = leave unchanged).
|
||||
To remove ALL tags from a page, pass exactly ["__CLEAR__"]
|
||||
(an empty list means "leave unchanged", not "clear")
|
||||
description: New description (empty = leave unchanged)
|
||||
|
||||
Returns:
|
||||
@@ -826,15 +838,20 @@ async def update_wiki_page(
|
||||
Examples:
|
||||
update_wiki_page(42, content="# Updated Content\\n\\nNew information here")
|
||||
update_wiki_page(42, tags=["projects", "devops"]) # Add to dossiers
|
||||
update_wiki_page(42, tags=["__CLEAR__"]) # Remove all tags
|
||||
update_wiki_page(42, description="Updated description")
|
||||
"""
|
||||
# Empty list = leave unchanged; the explicit clear sentinel sends an
|
||||
# empty tag list to the service, which replaces (clears) all tags.
|
||||
clear_tags = tags == [CLEAR_TAGS_SENTINEL]
|
||||
|
||||
try:
|
||||
async with LibraryDeskClient() as client:
|
||||
page = await client.update_wiki_page(
|
||||
page_id=page_id,
|
||||
content=content if content else None,
|
||||
title=title if title else None,
|
||||
tags=tags if tags else None,
|
||||
tags=[] if clear_tags else (tags if tags else None),
|
||||
description=description if description else None,
|
||||
)
|
||||
|
||||
@@ -844,7 +861,9 @@ async def update_wiki_page(
|
||||
updated_fields.append("content")
|
||||
if title:
|
||||
updated_fields.append("title")
|
||||
if tags:
|
||||
if clear_tags:
|
||||
updated_fields.append("tags (cleared)")
|
||||
elif tags:
|
||||
updated_fields.append("tags")
|
||||
if description:
|
||||
updated_fields.append("description")
|
||||
|
||||
@@ -178,9 +178,24 @@ async def _handle_text_delegation(
|
||||
]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# asyncio.gather returns exactly one item per task; a length
|
||||
# mismatch would mean results are attributed to the wrong
|
||||
# agent, so fail loudly instead of mispairing silently.
|
||||
if len(results) != len(matches):
|
||||
logger.error(
|
||||
"delegation_result_count_mismatch",
|
||||
expected=len(matches),
|
||||
got=len(results),
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
return (
|
||||
"I apologize, sir. I was unable to complete the "
|
||||
"requested delegations."
|
||||
)
|
||||
|
||||
# Combine results (failures carry curated user-safe sentences)
|
||||
summaries = []
|
||||
for (agent, task), item in zip(matches, results, strict=False):
|
||||
for (agent, task), item in zip(matches, results, strict=True):
|
||||
agent_name = agent.lower()
|
||||
if isinstance(item, BaseException):
|
||||
logger.error(
|
||||
|
||||
Reference in New Issue
Block a user