mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-10 18:22:20 +02:00
* refactor(model-routing): centralize explicit foreground fallback policy Make foreground fallback an explicit per-user, availability-only policy shared by streaming Chat, non-stream Chat, and Agent runs. Preserve strict defaults, owner/model and credential boundaries, pinned Agent routes, and truthful per-round provenance/accounting. Carry provider-reported model identifiers through native streaming adapters, non-stream responses, and caches, and keep legacy default_model_fallbacks as tombstoned raw storage that generic settings APIs and agent tools cannot expose or mutate. * fix(agent-loop): restore rebase-dropped qwen routing, workspace prompt, and temperature clamp * fix(model-routing): thread selected endpoint identity, fix cost classification and fallback eligibility * fix(chat): restore stream helpers and harden run stop lifecycle * fix(model-routing): let numeric provider codes win over symbolic rate-limit statuses * fix(agent-loop): apply qwen temperature and notes-tool clamps per fallback candidate * fix(chat): honor queued stop across resend and reload canonical terminal on EOF * fix(chat): track stop queue and cleanup ownership by per-send generation * fix(agent-loop): preserve requested temperature for non-qwen fallback candidates * fix(chat): reserve send ownership before any await and scope stop to the current send * fix(chat): clear the previous run identity at send reservation --------- Co-authored-by: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Co-authored-by: StressTestor <212606152+StressTestor@users.noreply.github.com>
336 lines
11 KiB
Python
336 lines
11 KiB
Python
"""Executable regression coverage for behavior lost in PR #6020's rebase."""
|
|
|
|
import asyncio
|
|
import json
|
|
|
|
import src.agent_loop as agent_loop
|
|
|
|
|
|
ODY_QWEN = "odysseus-qwen3-4b"
|
|
NOTES_TOOLS = {
|
|
"manage_notes",
|
|
"manage_calendar",
|
|
"manage_tasks",
|
|
"ask_user",
|
|
"update_plan",
|
|
}
|
|
|
|
|
|
def _collect(generator):
|
|
async def _run():
|
|
return [chunk async for chunk in generator]
|
|
|
|
return asyncio.run(_run())
|
|
|
|
|
|
def _events(chunks):
|
|
return [
|
|
json.loads(chunk[6:])
|
|
for chunk in chunks
|
|
if chunk.startswith("data: ") and not chunk.startswith("data: [DONE]")
|
|
]
|
|
|
|
|
|
def _install_route_probe(monkeypatch):
|
|
prompt_calls = []
|
|
stream_calls = []
|
|
|
|
monkeypatch.setattr(agent_loop, "get_setting", lambda key, default=None: default)
|
|
monkeypatch.setattr(agent_loop, "get_mcp_manager", lambda: None)
|
|
monkeypatch.setattr(agent_loop, "estimate_tokens", lambda *args, **kwargs: 10)
|
|
monkeypatch.setattr(agent_loop, "blocked_tools_for_owner", lambda owner: set())
|
|
monkeypatch.setattr(
|
|
agent_loop,
|
|
"_agent_route_tool_mode",
|
|
lambda *args, **kwargs: (True, False, False),
|
|
)
|
|
|
|
def fake_build(
|
|
messages,
|
|
model,
|
|
_active_document,
|
|
_mcp_mgr,
|
|
disabled_tools=None,
|
|
**kwargs,
|
|
):
|
|
prompt_calls.append(
|
|
{
|
|
"model": model,
|
|
"relevant_tools": set(kwargs.get("relevant_tools") or set()),
|
|
"disabled_tools": set(disabled_tools or set()),
|
|
"workspace": kwargs.get("workspace"),
|
|
}
|
|
)
|
|
return (list(messages), [])
|
|
|
|
async def fake_stream(_candidates, _messages, **kwargs):
|
|
stream_calls.append(kwargs)
|
|
yield 'data: {"delta": "ok"}\n\n'
|
|
yield "data: [DONE]\n\n"
|
|
|
|
monkeypatch.setattr(agent_loop, "_build_system_prompt", fake_build)
|
|
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", fake_stream)
|
|
return prompt_calls, stream_calls
|
|
|
|
|
|
def _run_probe(messages, *, relevant_tools, **kwargs):
|
|
return _collect(
|
|
agent_loop.stream_agent_loop(
|
|
"https://api.example/v1",
|
|
kwargs.pop("model", ODY_QWEN),
|
|
messages,
|
|
max_rounds=1,
|
|
relevant_tools=set(relevant_tools),
|
|
_is_teacher_run=True,
|
|
**kwargs,
|
|
)
|
|
)
|
|
|
|
|
|
def test_odysseus_notes_mode_clamps_and_reenables_all_personal_managers(monkeypatch):
|
|
prompt_calls, _ = _install_route_probe(monkeypatch)
|
|
|
|
_run_probe(
|
|
[{"role": "user", "content": "Add buy milk to my notes."}],
|
|
relevant_tools={"bash", "manage_notes", "manage_calendar", "manage_tasks"},
|
|
disabled_tools={"manage_notes", "manage_calendar", "manage_tasks"},
|
|
)
|
|
|
|
route = prompt_calls[0]
|
|
assert route["relevant_tools"] == NOTES_TOOLS
|
|
assert route["disabled_tools"].isdisjoint(
|
|
{"manage_notes", "manage_calendar", "manage_tasks"}
|
|
)
|
|
|
|
|
|
def test_odysseus_general_mode_disables_every_tool(monkeypatch):
|
|
from src.tool_policy import known_tool_names
|
|
|
|
prompt_calls, _ = _install_route_probe(monkeypatch)
|
|
|
|
_run_probe(
|
|
[{"role": "user", "content": "Explain the CAP theorem."}],
|
|
relevant_tools={"bash", "manage_notes", "ask_user"},
|
|
)
|
|
|
|
route = prompt_calls[0]
|
|
assert route["relevant_tools"] == set()
|
|
assert known_tool_names() <= route["disabled_tools"]
|
|
|
|
|
|
def test_odysseus_calendar_intent_uses_notes_mode(monkeypatch):
|
|
prompt_calls, _ = _install_route_probe(monkeypatch)
|
|
|
|
_run_probe(
|
|
[{"role": "user", "content": "Add lunch tomorrow to my calendar."}],
|
|
relevant_tools={"manage_notes", "manage_calendar", "manage_tasks", "bash"},
|
|
)
|
|
|
|
assert prompt_calls[0]["relevant_tools"] == NOTES_TOOLS
|
|
|
|
|
|
def test_odysseus_calendar_followup_keeps_notes_mode(monkeypatch):
|
|
prompt_calls, _ = _install_route_probe(monkeypatch)
|
|
messages = [
|
|
{"role": "user", "content": "Add lunch tomorrow to my calendar."},
|
|
{
|
|
"role": "assistant",
|
|
"content": "Done.",
|
|
"metadata": {
|
|
"tool_events": [
|
|
{
|
|
"tool": "manage_calendar",
|
|
"command": '{"action":"create_event","summary":"Lunch"}',
|
|
"output": "Created event evt-123 at noon.",
|
|
}
|
|
]
|
|
},
|
|
},
|
|
{"role": "user", "content": "Move it to 3pm."},
|
|
]
|
|
|
|
_run_probe(
|
|
messages,
|
|
relevant_tools={"manage_notes", "manage_calendar", "manage_tasks", "bash"},
|
|
)
|
|
|
|
assert prompt_calls[0]["relevant_tools"] == NOTES_TOOLS
|
|
|
|
|
|
def test_agent_route_passes_workspace_to_system_prompt(monkeypatch):
|
|
prompt_calls, _ = _install_route_probe(monkeypatch)
|
|
|
|
_run_probe(
|
|
[{"role": "user", "content": "Fix the failing test in this project."}],
|
|
model="gpt-4o",
|
|
relevant_tools={"bash", "read_file", "apply_patch"},
|
|
workspace="/tmp/example-repo",
|
|
)
|
|
|
|
assert prompt_calls[0]["workspace"] == "/tmp/example-repo"
|
|
|
|
|
|
def test_odysseus_qwen_temperature_is_capped_for_agent_requests(monkeypatch):
|
|
_, stream_calls = _install_route_probe(monkeypatch)
|
|
|
|
_run_probe(
|
|
[{"role": "user", "content": "Add buy milk to my notes."}],
|
|
relevant_tools={"manage_notes"},
|
|
temperature=1.2,
|
|
)
|
|
|
|
assert stream_calls[0]["temperature"] == 0.2
|
|
|
|
|
|
def test_qwen_fallback_candidate_gets_capped_temperature(monkeypatch):
|
|
"""A non-qwen primary must not leak its temperature into a qwen fallback."""
|
|
|
|
_, stream_calls = _install_route_probe(monkeypatch)
|
|
|
|
_run_probe(
|
|
[{"role": "user", "content": "Explain the CAP theorem."}],
|
|
model="gpt-4o",
|
|
relevant_tools={"bash"},
|
|
temperature=1.2,
|
|
fallbacks=[("https://qwen.example/v1", ODY_QWEN, {})],
|
|
)
|
|
|
|
assert stream_calls[0]["temperature"] == 1.2
|
|
factory = stream_calls[0]["candidate_request_factory"]
|
|
request = asyncio.run(factory(1, "https://qwen.example/v1", ODY_QWEN, {}))
|
|
assert request["kwargs"]["temperature"] == 0.2
|
|
|
|
|
|
def test_non_qwen_fallback_keeps_requested_temperature(monkeypatch):
|
|
"""A qwen primary's 0.2 cap must not leak into a non-qwen fallback."""
|
|
|
|
_, stream_calls = _install_route_probe(monkeypatch)
|
|
|
|
_run_probe(
|
|
[{"role": "user", "content": "Add buy milk to my notes."}],
|
|
relevant_tools={"manage_notes"},
|
|
temperature=1.2,
|
|
fallbacks=[("https://backup.example/v1", "gpt-4o", {})],
|
|
)
|
|
|
|
assert stream_calls[0]["temperature"] == 0.2
|
|
factory = stream_calls[0]["candidate_request_factory"]
|
|
request = asyncio.run(factory(1, "https://backup.example/v1", "gpt-4o", {}))
|
|
assert request["kwargs"]["temperature"] == 1.2
|
|
|
|
|
|
def test_qwen_notes_fallback_reenables_personal_managers(monkeypatch):
|
|
"""The answering candidate's notes mode must unblock the managers for
|
|
execution, not just enable them in its own route schemas."""
|
|
|
|
_install_route_probe(monkeypatch)
|
|
stream_round = 0
|
|
resolve_round = 0
|
|
seen_exec = {}
|
|
|
|
async def fake_stream(_candidates, _messages, **kwargs):
|
|
nonlocal stream_round
|
|
stream_round += 1
|
|
if stream_round == 1:
|
|
yield (
|
|
"data: "
|
|
+ json.dumps(
|
|
{
|
|
"type": "fallback",
|
|
"answered_by": ODY_QWEN,
|
|
"candidate_index": 1,
|
|
}
|
|
)
|
|
+ "\n\n"
|
|
)
|
|
yield 'data: {"delta": "Adding the note."}\n\n'
|
|
else:
|
|
yield 'data: {"delta": "Done."}\n\n'
|
|
yield "data: [DONE]\n\n"
|
|
|
|
def fake_resolve(*args, **kwargs):
|
|
nonlocal resolve_round
|
|
resolve_round += 1
|
|
if resolve_round == 1:
|
|
return ([agent_loop.ToolBlock("manage_notes", "{}")], False, [])
|
|
return ([], False, [])
|
|
|
|
async def fake_execute(block, *args, **kwargs):
|
|
# Execution is the consumer daybreak's probe showed rejecting the
|
|
# managers: it receives the shared disabled_tools set, not the
|
|
# answering route's own tool state.
|
|
seen_exec["disabled_tools"] = set(kwargs.get("disabled_tools") or [])
|
|
return ("manage_notes: saved", {"output": "noted", "exit_code": 0})
|
|
|
|
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", fake_stream)
|
|
monkeypatch.setattr(agent_loop, "_resolve_tool_blocks", fake_resolve)
|
|
monkeypatch.setattr(agent_loop, "execute_tool_block", fake_execute)
|
|
|
|
_collect(
|
|
agent_loop.stream_agent_loop(
|
|
"https://api.example/v1",
|
|
"gpt-4o",
|
|
[{"role": "user", "content": "Add buy milk to my notes."}],
|
|
max_rounds=2,
|
|
relevant_tools={"manage_notes", "manage_calendar", "manage_tasks", "bash"},
|
|
disabled_tools={"manage_notes", "manage_calendar", "manage_tasks"},
|
|
fallbacks=[("https://qwen.example/v1", ODY_QWEN, {})],
|
|
_is_teacher_run=True,
|
|
)
|
|
)
|
|
|
|
assert seen_exec["disabled_tools"].isdisjoint(
|
|
{"manage_notes", "manage_calendar", "manage_tasks"}
|
|
)
|
|
|
|
|
|
def test_persisted_mcp_tool_event_keeps_description_and_resolved_name(monkeypatch):
|
|
_install_route_probe(monkeypatch)
|
|
stream_round = 0
|
|
resolve_round = 0
|
|
|
|
async def fake_stream(_candidates, _messages, **kwargs):
|
|
nonlocal stream_round
|
|
stream_round += 1
|
|
if stream_round == 1:
|
|
yield 'data: {"delta": "Calling calendar."}\n\n'
|
|
else:
|
|
yield 'data: {"delta": "Finished."}\n\n'
|
|
yield "data: [DONE]\n\n"
|
|
|
|
def fake_resolve(*args, **kwargs):
|
|
nonlocal resolve_round
|
|
resolve_round += 1
|
|
if resolve_round == 1:
|
|
return ([agent_loop.ToolBlock("mcp", "{}")], False, [])
|
|
return ([], False, [])
|
|
|
|
async def fake_execute(block, *args, **kwargs):
|
|
assert block.tool_type == "mcp"
|
|
return (
|
|
"mcp__calendar__create_event: created team sync",
|
|
{"output": "Created event evt-456.", "exit_code": 0},
|
|
)
|
|
|
|
monkeypatch.setattr(agent_loop, "stream_llm_with_fallback", fake_stream)
|
|
monkeypatch.setattr(agent_loop, "_resolve_tool_blocks", fake_resolve)
|
|
monkeypatch.setattr(agent_loop, "execute_tool_block", fake_execute)
|
|
|
|
chunks = _collect(
|
|
agent_loop.stream_agent_loop(
|
|
"https://api.example/v1",
|
|
"gpt-4o",
|
|
[{"role": "user", "content": "Create the team sync event."}],
|
|
max_rounds=2,
|
|
relevant_tools={"mcp"},
|
|
_is_teacher_run=True,
|
|
)
|
|
)
|
|
metrics = next(
|
|
event["data"] for event in _events(chunks) if event.get("type") == "metrics"
|
|
)
|
|
persisted = metrics["tool_events"][0]
|
|
|
|
assert persisted["tool"] == "mcp__calendar__create_event"
|
|
assert persisted["desc"] == "mcp__calendar__create_event: created team sync"
|