- delegate_to_* now receives a trimmed conversation history (last ~6 turns, 500 chars/turn) as context on both live direct-delegation paths (streaming and steward non-streaming), via new build_delegation_context helper - _stream_direct_delegation restructured as an async generator: the butler 'start' think message streams BEFORE the expert runs and the success/error message right after it finishes, instead of all messages arriving after the research completed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
186 lines
6.5 KiB
Python
186 lines
6.5 KiB
Python
"""
|
|
Tests for real-time think message streaming and context plumbing in
|
|
the direct delegation paths.
|
|
|
|
_stream_direct_delegation must be an async generator that yields the
|
|
"start" think message BEFORE the expert runs (so 'Allow me to consult
|
|
the archives, sir.' streams while research is in flight), and both
|
|
direct delegation paths must pass trimmed conversation history as
|
|
expert context.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.agents.delegation import DelegationResult
|
|
from src.responses.streaming import (
|
|
ReasoningSummaryDelta,
|
|
ReasoningSummaryDone,
|
|
StreamingCoordinator,
|
|
)
|
|
|
|
HISTORY = [
|
|
{"role": "user", "content": "Tell me about my homelab wiki"},
|
|
{"role": "assistant", "content": "It documents your services, sir."},
|
|
]
|
|
|
|
|
|
def _librarian_result(output: str = "Findings.") -> DelegationResult:
|
|
return DelegationResult(
|
|
expert_name="librarian",
|
|
task="task",
|
|
success=True,
|
|
output=output,
|
|
)
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestStreamDirectDelegation:
|
|
"""Real-time streaming behavior of _stream_direct_delegation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_think_streams_before_research_runs(self):
|
|
coordinator = StreamingCoordinator()
|
|
tracker = AsyncMock()
|
|
results: dict = {}
|
|
|
|
with patch(
|
|
"src.agents.delegation.delegate_to_librarian",
|
|
new_callable=AsyncMock,
|
|
return_value=_librarian_result(),
|
|
) as mock_delegate:
|
|
gen = coordinator._stream_direct_delegation(
|
|
user_message="Search for Docker info",
|
|
recommendation=SimpleNamespace(
|
|
recommended_capabilities=["librarian"]
|
|
),
|
|
tracker=tracker,
|
|
conversation_id="conv_1",
|
|
conversation_history=HISTORY,
|
|
results=results,
|
|
)
|
|
|
|
# First event: the start think message, BEFORE any research
|
|
first = await gen.__anext__()
|
|
assert isinstance(first, ReasoningSummaryDelta)
|
|
assert first.delta.strip() != ""
|
|
assert mock_delegate.await_count == 0, (
|
|
"start think message must stream before the expert runs"
|
|
)
|
|
|
|
second = await gen.__anext__()
|
|
assert isinstance(second, ReasoningSummaryDone)
|
|
assert mock_delegate.await_count == 0
|
|
|
|
# Third event: completion message - research has now run
|
|
third = await gen.__anext__()
|
|
assert isinstance(third, ReasoningSummaryDelta)
|
|
assert mock_delegate.await_count == 1
|
|
|
|
remaining = [event async for event in gen]
|
|
assert any(isinstance(e, ReasoningSummaryDone) for e in remaining)
|
|
|
|
# Results dict is populated for Phase 2 synthesis
|
|
assert results["expert_results"] == {"librarian": "Findings."}
|
|
assert results["tools_called"] == ["delegate_to_librarian"]
|
|
assert len(results["think_messages"]) == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_conversation_history_passed_as_context(self):
|
|
coordinator = StreamingCoordinator()
|
|
tracker = AsyncMock()
|
|
|
|
with patch(
|
|
"src.agents.delegation.delegate_to_librarian",
|
|
new_callable=AsyncMock,
|
|
return_value=_librarian_result(),
|
|
) as mock_delegate:
|
|
events = [
|
|
event
|
|
async for event in coordinator._stream_direct_delegation(
|
|
user_message="And what services does it list?",
|
|
recommendation=SimpleNamespace(
|
|
recommended_capabilities=["librarian"]
|
|
),
|
|
tracker=tracker,
|
|
conversation_id="conv_1",
|
|
conversation_history=HISTORY,
|
|
results={},
|
|
)
|
|
]
|
|
|
|
assert events, "generator must yield think events"
|
|
context = mock_delegate.await_args.kwargs["context"]
|
|
assert "Tell me about my homelab wiki" in context
|
|
assert "It documents your services, sir." in context
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_failed_delegation_streams_error_think(self):
|
|
coordinator = StreamingCoordinator()
|
|
tracker = AsyncMock()
|
|
results: dict = {}
|
|
|
|
failed = DelegationResult(
|
|
expert_name="librarian",
|
|
task="task",
|
|
success=False,
|
|
output="I'm afraid the archives proved difficult to access.",
|
|
error="The Librarian was unable to complete the task.",
|
|
)
|
|
|
|
with patch(
|
|
"src.agents.delegation.delegate_to_librarian",
|
|
new_callable=AsyncMock,
|
|
return_value=failed,
|
|
):
|
|
events = [
|
|
event
|
|
async for event in coordinator._stream_direct_delegation(
|
|
user_message="Search for Docker info",
|
|
recommendation=SimpleNamespace(
|
|
recommended_capabilities=["librarian"]
|
|
),
|
|
tracker=tracker,
|
|
conversation_id="conv_1",
|
|
results=results,
|
|
)
|
|
]
|
|
|
|
assert results["tools_called"] == []
|
|
# Expert result carries the curated user-safe sentence
|
|
assert "archives" in results["expert_results"]["librarian"]
|
|
deltas = [e.delta for e in events if isinstance(e, ReasoningSummaryDelta)]
|
|
assert len(deltas) == 2 # start + error think messages
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestServiceDelegationContext:
|
|
"""The non-streaming direct delegation path passes trimmed history."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_direct_delegation_with_results_passes_context(self):
|
|
from src.responses.service import _direct_delegation_with_results
|
|
|
|
tracker = AsyncMock()
|
|
|
|
with patch(
|
|
"src.agents.delegation.delegate_to_librarian",
|
|
new_callable=AsyncMock,
|
|
return_value=_librarian_result(),
|
|
) as mock_delegate:
|
|
results = await _direct_delegation_with_results(
|
|
user_message="And what services does it list?",
|
|
recommendation=SimpleNamespace(
|
|
recommended_capabilities=["librarian"]
|
|
),
|
|
tracker=tracker,
|
|
conversation_id="conv_1",
|
|
conversation_history=HISTORY,
|
|
)
|
|
|
|
context = mock_delegate.await_args.kwargs["context"]
|
|
assert "Tell me about my homelab wiki" in context
|
|
assert results["expert_results"]["librarian"] == "Findings."
|