diff --git a/src/agents/biographer/agent.py b/src/agents/biographer/agent.py index 6fe7e7c..ca2c49a 100644 --- a/src/agents/biographer/agent.py +++ b/src/agents/biographer/agent.py @@ -191,9 +191,12 @@ async def run_biographer( ) try: + from src.anthropic.model_selector import with_phase_binding + result = await agent.run( prompt, message_history=message_history, + model_settings=with_phase_binding(None, "experts"), ) logger.info( diff --git a/src/agents/housekeeper/agent.py b/src/agents/housekeeper/agent.py index d509e9b..0ab392c 100644 --- a/src/agents/housekeeper/agent.py +++ b/src/agents/housekeeper/agent.py @@ -206,12 +206,12 @@ async def run_housekeeper( try: # Temperature 0.1 for slight exploration (skipped on Claude backend) - from src.anthropic.model_selector import get_sampling_settings + from src.anthropic.model_selector import get_sampling_settings, with_phase_binding result = await agent.run( prompt, message_history=message_history, - model_settings=get_sampling_settings(0.1), + model_settings=with_phase_binding(get_sampling_settings(0.1), "experts"), ) logger.info( diff --git a/src/agents/tatlock.py b/src/agents/tatlock.py index 2e4c605..9d80333 100644 --- a/src/agents/tatlock.py +++ b/src/agents/tatlock.py @@ -404,10 +404,13 @@ class TatlockAgent(AgentInterface): # with async context managers inside generators # The StreamingCoordinator will handle word-by-word streaming # Pass message_history to maintain conversation context and tracker for tool logging + from src.anthropic.model_selector import with_phase_binding + result = await self.agent.run( user_message, message_history=message_history if message_history else None, deps=tracker, + model_settings=with_phase_binding(None, "orchestrate"), ) final_text = result.output @@ -625,10 +628,13 @@ class TatlockAgent(AgentInterface): # Use run() instead of run_stream() to avoid Ollama 400 bug # with streaming + tool calls (PydanticAI issues #1292, #2256) # We yield the final response in chunks to maintain streaming interface + from src.anthropic.model_selector import with_phase_binding + result = await scoped_agent.run( enriched_message, message_history=pydantic_history if pydantic_history else None, deps=tool_tracker, + model_settings=with_phase_binding(None, "orchestrate"), ) # Stream the final response in chunks to maintain UX diff --git a/src/anthropic/model_selector.py b/src/anthropic/model_selector.py index 567d471..8634054 100644 --- a/src/anthropic/model_selector.py +++ b/src/anthropic/model_selector.py @@ -313,6 +313,12 @@ _PHASE_BINDINGS: dict[str, tuple[int | None, str, int]] = { "steward": (0, "tatlock-steward", 40), "orchestrate": (1, "tatlock-orchestrate", 40), "synthesize": (2, "tatlock-synthesize", 40), + # Experts (biographer, housekeeper) share one session between the + # librarian and the phases: the two least-used consumers trade the + # spare slot by rank instead of anyone hitting an empty pool — a + # session-less call 503s once four sessions are resident (D-4's + # idle-only rule), so every generation call here must carry a name. + "experts": (None, "tatlock-experts", 35), "librarian": (None, "librarian", 30), } diff --git a/tests/anthropic/test_model_selector.py b/tests/anthropic/test_model_selector.py index 8aec232..5b2b39f 100644 --- a/tests/anthropic/test_model_selector.py +++ b/tests/anthropic/test_model_selector.py @@ -121,11 +121,12 @@ class TestPhaseBinding: monkeypatch.setattr(model_selector, "_local_flavor", "boilerroom") ranks = { phase: model_selector.phase_extra_body(phase)["eviction_order"] - for phase in ("steward", "orchestrate", "synthesize", "librarian") + for phase in ("steward", "orchestrate", "synthesize", "experts", "librarian") } assert ranks["steward"] == ranks["orchestrate"] == ranks["synthesize"] == 40 + assert ranks["experts"] == 35 assert ranks["librarian"] == 30 - assert ranks["librarian"] < ranks["steward"] + assert ranks["librarian"] < ranks["experts"] < ranks["steward"] assert ranks["librarian"] > 20 # webber's rank stays below def test_wrapper_session_names_are_stable(self, local_first, monkeypatch): @@ -134,12 +135,13 @@ class TestPhaseBinding: monkeypatch.setattr(model_selector, "_local_flavor", "boilerroom") names = { phase: model_selector.phase_extra_body(phase)["session"] - for phase in ("steward", "orchestrate", "synthesize", "librarian") + for phase in ("steward", "orchestrate", "synthesize", "experts", "librarian") } assert names == { "steward": "tatlock-steward", "orchestrate": "tatlock-orchestrate", "synthesize": "tatlock-synthesize", + "experts": "tatlock-experts", "librarian": "librarian", }