import json from pathlib import Path from src.agent_loop import ( _classify_agent_request, _contextual_link_followup_topic, _is_contextual_link_followup, _is_terse_link_request, _minimal_recent_notes_tool_context_message, _looks_like_explicit_notes_only_turn, _looks_like_explicit_web_search_request, _malformed_write_needs_body_handoff, _post_finish_inspection_should_converge, _repeated_artifact_mutation_can_finish, _minimal_native_tool_prompt, _should_use_direct_low_signal_path, ) def test_web_audio_local_media_request_is_not_public_web_search() -> None: prompt = ( "View /workspace/fixtures/Canon1.png and create output.html with " "a Play button using the Web Audio API." ) assert not _looks_like_explicit_web_search_request( prompt, local_media_turn=True, ) def test_explicit_public_web_request_remains_recognized() -> None: assert _looks_like_explicit_web_search_request( "Search the web for the latest official release notes." ) def test_verified_identical_artifact_rewrite_finishes_boundedly() -> None: assert _repeated_artifact_mutation_can_finish( ["write_file"], html_verified=True, ) assert not _repeated_artifact_mutation_can_finish( ["write_file"], html_verified=False, ) assert not _repeated_artifact_mutation_can_finish( ["send_email"], html_verified=True, ) def test_malformed_text_write_uses_one_bounded_body_handoff() -> None: assert _malformed_write_needs_body_handoff( {"write_file"}, ["/workspace/output.html"], attempts=0, ) assert not _malformed_write_needs_body_handoff( {"write_file"}, ["/workspace/output.html"], attempts=1, ) assert not _malformed_write_needs_body_handoff( {"write_file"}, ["/workspace/output.png"], attempts=0, ) def test_post_finish_reinspection_without_correction_converges() -> None: args = dict( finish_nudge_sent=True, correction_seen=False, force_answer=False, verification_only=True, current_inspection=True, can_complete=True, ) assert _post_finish_inspection_should_converge(**args) assert not _post_finish_inspection_should_converge( **{**args, "correction_seen": True} ) assert not _post_finish_inspection_should_converge( **{**args, "verification_only": False} ) def test_explicit_notes_rule_takes_precedence_over_documents() -> None: prompt = _minimal_native_tool_prompt({"manage_notes", "manage_documents"}) assert "When the user explicitly asks about a note or notes" in prompt assert "call `manage_notes`, not `manage_documents`" in prompt assert "An explicit note or notes request uses `manage_notes` instead" in prompt assert "`action: \"search\"`" in prompt assert "`action: \"view\"`" in prompt def test_notes_rule_is_not_injected_when_notes_tool_is_unavailable() -> None: prompt = _minimal_native_tool_prompt({"manage_documents"}) assert "## Notes tool rule" not in prompt assert "## Document tool rule" in prompt def test_minimal_email_rule_prefers_topic_search_then_read() -> None: prompt = _minimal_native_tool_prompt({"search_emails", "read_email", "list_emails"}) assert "## Email tool rule" in prompt assert "search_emails" in prompt assert "returned UID" in prompt assert "Use `list_emails` only for an explicit inbox/list/latest request" in prompt def test_explicit_personal_note_request_is_notes_only() -> None: assert _looks_like_explicit_notes_only_turn( "Search my notes for Roast profile tweaks and open the note." ) def test_cross_domain_or_file_request_is_not_notes_only() -> None: assert not _looks_like_explicit_notes_only_turn( "Find the note and add a calendar reminder." ) assert not _looks_like_explicit_notes_only_turn( "Edit the notes.py file and run its tests." ) def test_minimal_notes_clamp_suppresses_admin_schema_expansion() -> None: from pathlib import Path source = (Path(__file__).resolve().parents[1] / "src" / "agent_loop.py").read_text() clamp = source[source.index("if _minimal_explicit_notes_mode"):] assert clamp.index("_needs_admin = False") < clamp.index( "elif _ody_doc_finetune_mode" ) def test_low_signal_direct_path_requires_no_tool_domains() -> None: args = dict( low_signal_turn=True, casual_low_signal_turn=True, ambiguous_short_turn=False, standalone_link_fragment_turn=False, existing_conversation=False, qwen38_tool_router=False, continuation=False, plan_mode=False, approved_plan=False, guide_only=False, active_document_relevant=False, active_email=None, workspace=None, forced_tools=False, relevant_tools=None, client_active_skills=False, terminal_agent_mode=False, has_tui_host_bridge=False, ) assert _should_use_direct_low_signal_path(has_domains=False, **args) assert not _should_use_direct_low_signal_path(has_domains=True, **args) def test_tui_bridge_does_not_block_low_signal_clarification_direct_path() -> None: args = dict( low_signal_turn=True, casual_low_signal_turn=False, ambiguous_short_turn=True, standalone_link_fragment_turn=False, existing_conversation=False, qwen38_tool_router=False, continuation=False, plan_mode=False, approved_plan=False, guide_only=False, active_document_relevant=False, active_email=None, workspace=None, has_domains=False, forced_tools=False, relevant_tools=None, client_active_skills=False, terminal_agent_mode=False, has_tui_host_bridge=True, ) assert _should_use_direct_low_signal_path(**args) def test_standalone_link_fragment_gets_clarification_path() -> None: from pathlib import Path source = (Path(__file__).resolve().parents[1] / "src" / "agent_loop.py").read_text() assert "def _is_terse_link_request" in source assert "_standalone_link_fragment_turn = (" in source assert "and not _is_contextual_link_followup(messages, _last_user)" in source assert '"Which links do you mean? Tell me the topic or website list."' in source assert '"deterministic_clarification": True' in source def test_contextual_link_followup_accepts_referential_typo_tail() -> None: messages = [ {"role": "user", "content": "What are some good sites for public domain art?"}, { "role": "assistant", "content": "Good sources include Wikimedia Commons, The Met Open Access, Rijksmuseum, and Smithsonian Open Access.", }, {"role": "user", "content": "sned links for those"}, ] assert _is_terse_link_request("sned links for those") assert _is_contextual_link_followup(messages, "sned links for those") assert "public domain art" in _contextual_link_followup_topic(messages, "sned links for those").lower() def test_contextual_link_followup_accepts_bare_resource_reference_only_after_web_context() -> None: web_messages = [ {"role": "user", "content": "What are some good sites for public domain art?"}, { "role": "assistant", "content": "Useful public domain art sources include Wikimedia Commons, The Met Open Access, Rijksmuseum, and Smithsonian Open Access.", }, {"role": "user", "content": "for the websites"}, ] email_messages = [ {"role": "user", "content": "what is my latest email?"}, {"role": "assistant", "content": "Here is your latest email from Instagram."}, {"role": "user", "content": "for the websites"}, ] assert _is_terse_link_request("for the websites") assert _is_contextual_link_followup(web_messages, "for the websites") assert not _is_contextual_link_followup(email_messages, "for the websites") def test_ambiguous_short_turn_clamps_retrieval_before_vector_search() -> None: from pathlib import Path source = (Path(__file__).resolve().parents[1] / "src" / "agent_loop.py").read_text() assert '_relevant_tools = {"ask_user"}' in source assert "_ambiguous_short_turn" in source def test_calendar_missing_date_guidance_uses_ask_user_not_guessing() -> None: from pathlib import Path root = Path(__file__).resolve().parents[1] agent_loop = (root / "src" / "agent_loop.py").read_text() tool_index = (root / "src" / "tool_index.py").read_text() schemas = (root / "src" / "tool_schemas.py").read_text() assert "do not guess a reservation/event date" in agent_loop assert "never invent a day for a reservation" in agent_loop assert "If create/update lacks a required date, time, or target event, call ask_user once" in tool_index assert "include an 'Exact date' option" in schemas def test_qwen35_tool_router_uses_broad_compact_map() -> None: from src.agent_loop import _QWEN38_TOOL_ROUTER_PROMPT, _is_qwen38_tool_router assert _is_qwen38_tool_router("odysseus-qwen3.5-9b-tool-router-v4-q4") assert _is_qwen38_tool_router("odysseus-qwen3.5-tools-pre-heretic") assert "manage_notes: notes/checklists" in _QWEN38_TOOL_ROUTER_PROMPT assert "mcp__email__list_emails" in _QWEN38_TOOL_ROUTER_PROMPT assert "mcp__email__search_emails" in _QWEN38_TOOL_ROUTER_PROMPT def test_qwen_tool_router_preserves_explicit_artifact_output_budget() -> None: from src.agent_loop import ( _allow_visual_tool_evidence_for_model, _malformed_native_tool_recovery_instruction, _qwen_tool_router_output_budget, ) assert _qwen_tool_router_output_budget(8192) == 8192 assert _qwen_tool_router_output_budget(None) == 1024 assert _allow_visual_tool_evidence_for_model( "odysseus-qwen3.5-tools-pre-heretic" ) assert not _allow_visual_tool_evidence_for_model( "qwen35-9b-tool-router-v4-firstaction-noschema-adapter" ) recovery = _malformed_native_tool_recovery_instruction({"write_file"}) assert "both path and content" in recovery assert "loops" in recovery def test_qwen_tool_router_replays_final_prose_after_buffering() -> None: from src.agent_loop import _should_emit_buffered_qwen_round assert _should_emit_buffered_qwen_round( odysseus_finetune=False, tool_router=True, has_tools=False, text="Finished answer", ) assert not _should_emit_buffered_qwen_round( odysseus_finetune=False, tool_router=True, has_tools=True, text="I should call a tool", ) assert not _should_emit_buffered_qwen_round( odysseus_finetune=False, tool_router=True, has_tools=False, text="Already streamed final answer", streamed_live=True, ) def test_qwen_agent_stream_releases_answer_after_orphan_think_close() -> None: from src.agent_loop import _incremental_qwen_visible_text assert _incremental_qwen_visible_text("The user wants a poem.") == "" assert _incremental_qwen_visible_text( "The user wants a poem.\n\n\nHere" ) == "Here" assert _incremental_qwen_visible_text( "The user wants a poem.\n\n\nHere is the poem" ) == "Here is the poem" def test_qwen_agent_stream_releases_clean_answer_after_prefix_disambiguates() -> None: from src.agent_loop import _incremental_qwen_visible_text assert _incremental_qwen_visible_text("Th") == "" assert _incremental_qwen_visible_text("The waves") == "The waves" def test_private_browser_product_catalog_requires_multiple_comparable_items() -> None: from src.agent_loop import _private_browser_product_catalog_ready catalog = """ Showing results for "chair". We found 1455 products. StaticText "Price $ 15.00" button "Review: 4.5 out of 5 stars. Total reviews: (316)" StaticText "Price $ 199.00" button "Review: 4.7 out of 5 stars. Total reviews: (613)" """ assert _private_browser_product_catalog_ready(catalog) assert not _private_browser_product_catalog_ready( 'Showing results for "chair". StaticText "Price $ 15.00"' ) def test_private_browser_open_without_dom_refs_queues_snapshot() -> None: from src.agent_loop import _private_browser_open_needs_snapshot assert _private_browser_open_needs_snapshot("open", "Opened https://example.com") assert not _private_browser_open_needs_snapshot( "open", 'link "Shop" [ref=e12]' ) assert not _private_browser_open_needs_snapshot("snapshot", "") def test_private_browser_product_enter_needs_results_snapshot() -> None: from src.agent_loop import _private_browser_product_submit_needs_snapshot assert _private_browser_product_submit_needs_snapshot( "press", {"key": "Enter"}, "find the largest chair" ) assert not _private_browser_product_submit_needs_snapshot( "press", {"key": "Escape"}, "find the largest chair" ) assert not _private_browser_product_submit_needs_snapshot( "press", {"key": "Enter"}, "submit the contact form" ) def test_injected_untrusted_context_is_not_counted_as_a_prior_user_turn() -> None: from src.agent_loop import _user_turn_count from src.prompt_security import untrusted_context_message messages = [ untrusted_context_message("saved memory: core", "Lives in Japan"), {"role": "user", "content": "Where is Sweden?"}, ] assert _user_turn_count(messages) == 1 def test_orphan_qwen_think_closer_hides_analysis_prefix() -> None: from src.agent_loop import _visible_response_text raw = "The user asks a geography question. I should answer directly.\n\n\nSweden is in Northern Europe." assert _visible_response_text(raw) == "Sweden is in Northern Europe." def test_what_do_you_remember_is_an_explicit_memory_list_request() -> None: from src.agent_loop import _parse_explicit_memory_lookup_request block = _parse_explicit_memory_lookup_request("What do you remember about me?") assert block is not None assert block.tool_type == "manage_memory" assert block.content == "list" def test_qwen_terminal_summary_handles_email_search_results() -> None: from src.agent_loop import _ody_qwen_terminal_tool_summary summary = _ody_qwen_terminal_tool_summary( { "tool": "mcp__email__search_emails", "command": '{"query":"Runpod","max_results":1}', "output": ( 'Found 1 email(s) matching "Runpod":\n\n' "1. **Your Runpod receipt [#1244-0851]**\n" " From: Runpod (support@runpod.io)\n" " Date: Thu, 20 Aug 2026 06:28:39 +0000\n" " Folder: INBOX\n" " UID: 91031\n" " Source: cached index" ), } ) assert "Here is your latest email:" in summary assert "Your Runpod receipt" in summary assert "UID 91031" in summary def test_qwen35_tui_workspace_uses_coding_only_prompt() -> None: from src.agent_loop import _build_system_prompt prompt, _ = _build_system_prompt( [{"role": "user", "content": "Edit the parser and run its tests."}], "qwen35-9b-tool-router-v4-firstaction-noschema-adapter", None, None, workspace="/workspace/project", client_runtime_context={ "surface": "odysseus-tui", "host_shell_bridge": {"url": "http://127.0.0.1:1/run", "token": "x"}, }, ) content = prompt[0]["content"] assert "Odysseus TUI workspace tools" in content assert "old_string" in content assert "mcp__email__search_emails" not in content def test_verified_tui_coding_summary_uses_tool_evidence() -> None: from src.agent_loop import _tui_verified_coding_summary summary = _tui_verified_coding_summary([ { "tool": "edit_file", "command": json.dumps({"path": "src/parser.py"}), "exit_code": 0, }, { "tool": "host_shell", "command": json.dumps({"command": "pytest -q"}), "exit_code": 1, }, { "tool": "host_shell", "command": json.dumps({"command": "pytest -q"}), "exit_code": 0, }, ]) assert "`src/parser.py`" in summary assert "`pytest -q` failed" in summary assert "`pytest -q` passed" in summary def test_partial_tui_mutation_failure_does_not_claim_no_change() -> None: from src.agent_loop import _tui_coding_failure_summary summary = _tui_coding_failure_summary([ { "tool": "edit_file", "command": json.dumps({"path": "src/parser.py"}), "exit_code": 0, }, ]) assert "Changed:" in summary assert "`src/parser.py`" in summary assert "verification completed" in summary assert "No workspace change" not in summary def test_qwen35_router_repairs_known_dropped_final_letter_artifacts() -> None: from src.agent_loop import _normalize_ody_qwen_text_artifacts assert _normalize_ody_qwen_text_artifacts( "I am Odysseus, an AI assistan." ) == "I am Odysseus, an AI assistant." def test_qwen_tool_router_does_not_get_notes_prompt_override() -> None: from pathlib import Path source = (Path(__file__).resolve().parents[1] / "src" / "agent_loop.py").read_text() block = source[ source.index("qwen_tool_router_mode = _is_qwen38_tool_router(candidate_model)"): source.index(" _runtime_directive = _tui_runtime_directive") ] assert "doc_mode and not qwen_tool_router_mode" in block assert "notes_mode and not qwen_tool_router_mode" in block assert "and not qwen_tool_router_mode" in block def test_qwen35_router_distinguishes_session_listing_from_chat_search() -> None: from src.agent_loop import _qwen38_router_tool_names assert _qwen38_router_tool_names("List my chat sessions.") == {"list_sessions"} assert _qwen38_router_tool_names("Search prior chats for the parser fix.") == {"search_chats"} assert "manage_contact" in _qwen38_router_tool_names("List my contacts.") assert "manage_research" in _qwen38_router_tool_names("List my saved research reports.") assert _qwen38_router_tool_names("List my tasks.") == {"manage_tasks"} def test_qwen_task_locator_followup_helpers_are_narrow() -> None: from src.agent_loop import ( _parse_qwen_task_mutation_request, _single_task_id_from_manage_tasks_list, ) assert _parse_qwen_task_mutation_request("Pause the task named nightly check.") == "pause" assert _parse_qwen_task_mutation_request("Resume only that recurring task.") == "resume" assert _parse_qwen_task_mutation_request("Delete the disposable task.") == "delete" assert _parse_qwen_task_mutation_request("List my tasks.") == "" assert _single_task_id_from_manage_tasks_list( "Found 1 tasks:\n" "1. ODY-EVAL (ec8fd302-90f1-4e41-97fe-0eebedf27334) — active" ) == "ec8fd302-90f1-4e41-97fe-0eebedf27334" assert _single_task_id_from_manage_tasks_list( "Found 2 tasks:\n1. A (a)\n2. B (b)" ) == "" def test_qwen35_router_maps_explicit_web_and_skill_intents() -> None: from src.agent_loop import _qwen38_router_tool_names assert _qwen38_router_tool_names("What is the latest Qwen release?") == {"web_search"} assert _qwen38_router_tool_names("What skills are available for this agent?") == {"manage_skills"} def test_skill_listing_summary_is_bounded_and_user_facing() -> None: from src.agent_loop import _skills_list_summary_from_tool_output raw = "\n".join([ "## Published", *[ f"- **skill-{i}** (general): Very long description that should not be repeated in the compact summary" for i in range(10) ], "## Drafts", *[f"- **draft-{i}** [draft]: Draft description should also be removed" for i in range(3)], ]) summary = _skills_list_summary_from_tool_output(raw) visible = summary.split("