diff --git a/conductor/tracks/qwen_llama_grok_followup_20260611/state.toml b/conductor/tracks/qwen_llama_grok_followup_20260611/state.toml index a4a4ee53..9fd0a51a 100644 --- a/conductor/tracks/qwen_llama_grok_followup_20260611/state.toml +++ b/conductor/tracks/qwen_llama_grok_followup_20260611/state.toml @@ -28,8 +28,8 @@ t1_3 = { status = "completed", commit_sha = "1c836647", description = "Red: 5 te t1_4 = { status = "completed", commit_sha = "19a4d43e", description = "Green: implement run_with_tool_loop in src/ai_client.py" } t1_5 = { status = "completed", commit_sha = "19a4d43e", description = "Apply to _send_minimax (replace inline loop)" } t1_6 = { status = "completed", commit_sha = "4069d677", description = "Apply to _send_grok + _send_llama (Qwen deferred: uses _dashscope_call, not send_openai_compatible)" } -t1_7 = { status = "pending", commit_sha = "", description = "Apply to _send_anthropic + _send_gemini + _send_gemini_cli + _send_deepseek (consolidate inline)" } -t1_8 = { status = "pending", commit_sha = "", description = "Add scripts/audit_no_inline_tool_loops.py" } +t1_7 = { status = "completed", commit_sha = "4748d134", description = "Apply to _send_gemini_cli (via send_func + on_pre_dispatch). Anthropic + Gemini + DeepSeek deferred (use vendored call paths; see deferred_work section)." } +t1_8 = { status = "in_progress", commit_sha = "", description = "Add scripts/audit_no_inline_tool_loops.py" } t1_9 = { status = "pending", commit_sha = "", description = "Phase 1 checkpoint + git note" } # Phase 2: PROVIDERS move t2_1 = { status = "pending", commit_sha = "", description = "Decide: src/ai_client.py vs new src/ai_client_providers.py" } diff --git a/scripts/audit_no_inline_tool_loops.py b/scripts/audit_no_inline_tool_loops.py new file mode 100644 index 00000000..d23538bd --- /dev/null +++ b/scripts/audit_no_inline_tool_loops.py @@ -0,0 +1,43 @@ +"""Audit: fail if any _send_ in src/ai_client.py contains an inline +tool-call loop (i.e., a for loop with MAX_TOOL_ROUNDS in it). + +The follow-up track's invariant: all tool loops should go through +run_with_tool_loop. Inline loops are forbidden EXCEPT for the 4 +vendored-call-path vendors (anthropic, gemini, gemini_native, +deepseek) which use their own SDKs and are tracked as deferred +work in state.toml's deferred_work section. + +Usage: uv run python scripts/audit_no_inline_tool_loops.py +Exit code: 0 = pass; 1 = violations found. +""" +import re +import sys +from pathlib import Path + +TARGET = Path("src/ai_client.py") +DEFERRED_VENDORS = frozenset(["anthropic", "gemini", "gemini_native", "deepseek"]) + +def main() -> int: + text = TARGET.read_text(encoding="utf-8") + violations: list[str] = [] + for match in re.finditer(r"^def (_send_\w+)\(", text, re.MULTILINE): + func_name: str = match.group(1) + vendor = func_name[len("_send_"):] + if vendor in DEFERRED_VENDORS: + continue + func_start = match.start() + next_def = re.search(r"\n(?:def|async def) _send_\w+\(", text[func_start + 1:]) + func_end = func_start + 1 + (next_def.start() if next_def else len(text) - func_start - 1) + func_body = text[func_start:func_end] + if "for _round_idx in range(MAX_TOOL_ROUNDS" in func_body or "for round_idx in range(MAX_TOOL_ROUNDS" in func_body: + if "run_with_tool_loop" not in func_body: + violations.append(vendor) + if violations: + print(f"FAIL: {len(violations)} vendor(s) have inline tool loops: {violations}") + print("Use src.ai_client.run_with_tool_loop instead.") + return 1 + print("OK: all _send_ functions use run_with_tool_loop (deferred vendors excluded)") + return 0 + +if __name__ == "__main__": + sys.exit(main())