feat(backend): every generation call carries a session name
The release audit swept every agent.run and raw chat call: the two streaming orchestrate paths never carried a binding (they predate the v2.6.0 pinning), and the biographer and housekeeper ran bare. Through the wrapper a session-less call takes idle slots only and 503s once four sessions are resident — so the streaming paths join tatlock-orchestrate, and the experts share tatlock-experts at rank 35: between the librarian and the phases, the two least-used consumers trade the spare slot by rank instead of anyone hitting an empty pool. Rank mutation shown to fail its test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -191,9 +191,12 @@ async def run_biographer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from src.anthropic.model_selector import with_phase_binding
|
||||||
|
|
||||||
result = await agent.run(
|
result = await agent.run(
|
||||||
prompt,
|
prompt,
|
||||||
message_history=message_history,
|
message_history=message_history,
|
||||||
|
model_settings=with_phase_binding(None, "experts"),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -206,12 +206,12 @@ async def run_housekeeper(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Temperature 0.1 for slight exploration (skipped on Claude backend)
|
# 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(
|
result = await agent.run(
|
||||||
prompt,
|
prompt,
|
||||||
message_history=message_history,
|
message_history=message_history,
|
||||||
model_settings=get_sampling_settings(0.1),
|
model_settings=with_phase_binding(get_sampling_settings(0.1), "experts"),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -404,10 +404,13 @@ class TatlockAgent(AgentInterface):
|
|||||||
# with async context managers inside generators
|
# with async context managers inside generators
|
||||||
# The StreamingCoordinator will handle word-by-word streaming
|
# The StreamingCoordinator will handle word-by-word streaming
|
||||||
# Pass message_history to maintain conversation context and tracker for tool logging
|
# 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(
|
result = await self.agent.run(
|
||||||
user_message,
|
user_message,
|
||||||
message_history=message_history if message_history else None,
|
message_history=message_history if message_history else None,
|
||||||
deps=tracker,
|
deps=tracker,
|
||||||
|
model_settings=with_phase_binding(None, "orchestrate"),
|
||||||
)
|
)
|
||||||
final_text = result.output
|
final_text = result.output
|
||||||
|
|
||||||
@@ -625,10 +628,13 @@ class TatlockAgent(AgentInterface):
|
|||||||
# Use run() instead of run_stream() to avoid Ollama 400 bug
|
# Use run() instead of run_stream() to avoid Ollama 400 bug
|
||||||
# with streaming + tool calls (PydanticAI issues #1292, #2256)
|
# with streaming + tool calls (PydanticAI issues #1292, #2256)
|
||||||
# We yield the final response in chunks to maintain streaming interface
|
# 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(
|
result = await scoped_agent.run(
|
||||||
enriched_message,
|
enriched_message,
|
||||||
message_history=pydantic_history if pydantic_history else None,
|
message_history=pydantic_history if pydantic_history else None,
|
||||||
deps=tool_tracker,
|
deps=tool_tracker,
|
||||||
|
model_settings=with_phase_binding(None, "orchestrate"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Stream the final response in chunks to maintain UX
|
# Stream the final response in chunks to maintain UX
|
||||||
|
|||||||
@@ -313,6 +313,12 @@ _PHASE_BINDINGS: dict[str, tuple[int | None, str, int]] = {
|
|||||||
"steward": (0, "tatlock-steward", 40),
|
"steward": (0, "tatlock-steward", 40),
|
||||||
"orchestrate": (1, "tatlock-orchestrate", 40),
|
"orchestrate": (1, "tatlock-orchestrate", 40),
|
||||||
"synthesize": (2, "tatlock-synthesize", 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),
|
"librarian": (None, "librarian", 30),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,11 +121,12 @@ class TestPhaseBinding:
|
|||||||
monkeypatch.setattr(model_selector, "_local_flavor", "boilerroom")
|
monkeypatch.setattr(model_selector, "_local_flavor", "boilerroom")
|
||||||
ranks = {
|
ranks = {
|
||||||
phase: model_selector.phase_extra_body(phase)["eviction_order"]
|
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["steward"] == ranks["orchestrate"] == ranks["synthesize"] == 40
|
||||||
|
assert ranks["experts"] == 35
|
||||||
assert ranks["librarian"] == 30
|
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
|
assert ranks["librarian"] > 20 # webber's rank stays below
|
||||||
|
|
||||||
def test_wrapper_session_names_are_stable(self, local_first, monkeypatch):
|
def test_wrapper_session_names_are_stable(self, local_first, monkeypatch):
|
||||||
@@ -134,12 +135,13 @@ class TestPhaseBinding:
|
|||||||
monkeypatch.setattr(model_selector, "_local_flavor", "boilerroom")
|
monkeypatch.setattr(model_selector, "_local_flavor", "boilerroom")
|
||||||
names = {
|
names = {
|
||||||
phase: model_selector.phase_extra_body(phase)["session"]
|
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 == {
|
assert names == {
|
||||||
"steward": "tatlock-steward",
|
"steward": "tatlock-steward",
|
||||||
"orchestrate": "tatlock-orchestrate",
|
"orchestrate": "tatlock-orchestrate",
|
||||||
"synthesize": "tatlock-synthesize",
|
"synthesize": "tatlock-synthesize",
|
||||||
|
"experts": "tatlock-experts",
|
||||||
"librarian": "librarian",
|
"librarian": "librarian",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user