# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Focused tests for the GGUF llama.cpp agentic tool loop. These tests drive ``LlamaCppBackend.generate_chat_completion_with_tools`` with fake llama-server SSE streams. They require no model, subprocess, GPU, or network access. """ from __future__ import annotations import contextlib import copy import json import sys from pathlib import Path _BACKEND_DIR = str(Path(__file__).resolve().parent.parent) if _BACKEND_DIR not in sys.path: sys.path.insert(0, _BACKEND_DIR) from core.inference.llama_cpp import LlamaCppBackend from state import tool_approvals from state.tool_approvals import TOOL_REJECTED_MESSAGE, resolve_tool_decision def _sse(delta: dict) -> str: return "data: " + json.dumps({"choices": [{"index": 0, "delta": delta}]}) + "\n" def _done() -> str: return "data: [DONE]\n" def _make_backend(monkeypatch, streams: list[list[str]], payloads: list[dict]): backend = LlamaCppBackend.__new__(LlamaCppBackend) backend._process = object() backend._healthy = True backend._port = 48847 backend._api_key = None backend._effective_context_length = 4096 backend._supports_reasoning = False backend._reasoning_always_on = False backend._reasoning_style = "enable_thinking" backend._supports_preserve_thinking = False @contextlib.contextmanager def fake_stream_with_retry( _client, _url, payload, _cancel_event, headers = None, ): payloads.append(copy.deepcopy(payload)) yield type("FakeResponse", (), {"status_code": 200, "chunks": streams.pop(0)})() def fake_iter_text_cancellable(response, _cancel_event): yield from response.chunks monkeypatch.setattr(backend, "_stream_with_retry", fake_stream_with_retry) monkeypatch.setattr(backend, "_iter_text_cancellable", fake_iter_text_cancellable) return backend def _tool_names(payload: dict) -> list[str]: return [ (tool.get("function") or {}).get("name") for tool in payload.get("tools", []) if (tool.get("function") or {}).get("name") ] def _structured_tool_call(tool_name: str, arguments: dict, call_id: str) -> list[str]: return [ _sse( { "tool_calls": [ { "index": 0, "id": call_id, "type": "function", "function": { "name": tool_name, "arguments": json.dumps(arguments), }, } ] } ), _done(), ] def test_structured_tool_call_after_visible_preface_is_executed(monkeypatch): """llama-server may emit content first and then native delta.tool_calls. Studio must not drop that tool call after it has streamed the preface. """ tool_call_id = "call_render_late" first_stream = [ _sse({"content": "Here is the artifact.\n\n"}), _sse( { "tool_calls": [ { "index": 0, "id": tool_call_id, "type": "function", "function": { "name": "render_html", "arguments": json.dumps( { "code": "
red
", "title": "Simple Red Square", } ), }, } ] } ), _done(), ] second_stream = [ _sse({"content": "Done."}), _done(), ] payloads: list[dict] = [] backend = _make_backend(monkeypatch, [first_stream, second_stream], payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "Rendered HTML artifact: Simple Red Square." monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) tools = [ { "type": "function", "function": { "name": "render_html", "description": "Render HTML.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], }, }, } ] events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "Make a red square."}], tools = tools, max_tool_iterations = 1, ) ) content_events = [e for e in events if e.get("type") == "content"] assert content_events[0]["text"] == "Here is the artifact.\n\n" first_content_index = next( i for i, event in enumerate(events) if event.get("type") == "content" ) actual_tool_start_index = next( i for i, event in enumerate(events) if event.get("type") == "tool_start" and event.get("arguments", {}).get("code") ) assert first_content_index < actual_tool_start_index assert calls == [ ( "render_html", { "code": "
red
", "title": "Simple Red Square", }, ) ] assert any(e.get("type") == "tool_end" and e.get("tool_name") == "render_html" for e in events) # The second llama-server request should include the assistant preface # plus the structured tool call, preserving OpenAI-compatible ordering. assert len(payloads) == 2 assistant_messages = [m for m in payloads[1]["messages"] if m.get("role") == "assistant"] assert assistant_messages[-1]["content"] == "Here is the artifact.\n\n" assert assistant_messages[-1]["tool_calls"][0]["id"] == tool_call_id assert assistant_messages[-1]["tool_calls"][0]["function"]["name"] == "render_html" def test_repeat_render_html_nudge_is_not_user_visible_error(monkeypatch): """A repeated render_html call is an internal no-op, not a visible card.""" first_stream = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_first", "type": "function", "function": { "name": "render_html", "arguments": json.dumps( { "code": "first", "title": "First", } ), }, } ] } ), _done(), ] repeat_stream = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_repeat", "type": "function", "function": { "name": "render_html", "arguments": json.dumps( { "code": "repeat", "title": "Repeat", } ), }, } ] } ), _done(), ] final_stream = [_sse({"content": "Short note."}), _done()] payloads: list[dict] = [] backend = _make_backend(monkeypatch, [first_stream, repeat_stream, final_stream], payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "Rendered HTML artifact: First." monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) tools = [ { "type": "function", "function": { "name": "render_html", "description": "Render HTML.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], }, }, }, {"type": "function", "function": {"name": "web_search"}}, ] events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "Make a red square."}], tools = tools, max_tool_iterations = 2, ) ) assert calls == [ ( "render_html", {"code": "first", "title": "First"}, ) ] assert _tool_names(payloads[1]) == ["web_search"] actual_tool_starts = [ event for event in events if event.get("type") == "tool_start" and event.get("arguments", {}).get("code") ] tool_ends = [ event for event in events if event.get("type") == "tool_end" and event.get("tool_name") == "render_html" ] assert len(actual_tool_starts) == 1 assert len(tool_ends) == 1 assert len(payloads) == 3 render_tool_messages = [ message for message in payloads[2]["messages"] if message.get("role") == "tool" and message.get("name") == "render_html" ] assert len(render_tool_messages) == 1 internal_nudges = [ message for message in payloads[2]["messages"] if message.get("role") == "user" and "Do not call render_html again" in message.get("content", "") ] assert len(internal_nudges) == 1 def test_render_html_success_drops_tool_schema_before_final_pass(monkeypatch): first_stream = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_first", "type": "function", "function": { "name": "render_html", "arguments": json.dumps({"code": "ok"}), }, } ] } ), _done(), ] final_stream = [_sse({"content": "Done."}), _done()] payloads: list[dict] = [] backend = _make_backend(monkeypatch, [first_stream, final_stream], payloads) def fake_execute_tool(name, arguments, **_kwargs): return "Rendered HTML artifact: Done." monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "Render this."}], tools = [{"type": "function", "function": {"name": "render_html"}}], max_tool_iterations = 3, ) ) assert len(payloads) == 2 assert "tools" not in payloads[1] assert any(event.get("type") == "content" and event.get("text") == "Done." for event in events) final_user_messages = [ m.get("content", "") for m in payloads[1]["messages"] if m.get("role") == "user" ] assert not any("used all available tool calls" in message for message in final_user_messages) def test_non_consecutive_duplicate_web_search_is_internal_noop(monkeypatch): first_search = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_search_1", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, } ] } ), _done(), ] python_call = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_python", "type": "function", "function": { "name": "python", "arguments": json.dumps({"code": "print('ok')"}), }, } ] } ), _done(), ] duplicate_search = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_search_2", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, } ] } ), _done(), ] final_stream = [_sse({"content": "Final answer from gathered data."}), _done()] payloads: list[dict] = [] backend = _make_backend( monkeypatch, [first_search, python_call, duplicate_search, final_stream], payloads, ) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return f"ok:{name}" monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) tools = [ {"type": "function", "function": {"name": "web_search"}}, {"type": "function", "function": {"name": "python"}}, ] events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "search gpus in 2026 prices and use python"}], tools = tools, max_tool_iterations = 3, ) ) assert calls == [ ("web_search", {"query": "gpu prices 2026"}), ("python", {"code": "print('ok')"}), ] assert [ event.get("tool_name") for event in events if event.get("type") == "tool_start" and event.get("tool_name") ] == ["web_search", "python"] assert [ event.get("tool_name") for event in events if event.get("type") == "tool_end" and event.get("tool_name") ] == ["web_search", "python"] assert not [ event for event in events if event.get("tool_call_id") == "call_search_2" and event.get("type") in {"tool_start", "tool_end"} ] assert len(payloads) == 4 assert _tool_names(payloads[3]) == ["web_search", "python"] duplicate_nudges = [ message for message in payloads[3]["messages"] if message.get("role") == "user" and "already completed successfully" in message.get("content", "") ] assert len(duplicate_nudges) == 1 def test_duplicate_web_search_noop_allows_distinct_followup_tool(monkeypatch): first_search = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_search_1", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, } ] } ), _done(), ] duplicate_search = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_search_2", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, } ] } ), _done(), ] python_call = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_python", "type": "function", "function": { "name": "python", "arguments": json.dumps({"code": "print('ok')"}), }, } ] } ), _done(), ] final_stream = [_sse({"content": "Final answer from gathered data."}), _done()] payloads: list[dict] = [] backend = _make_backend( monkeypatch, [first_search, duplicate_search, python_call, final_stream], payloads, ) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return f"ok:{name}" monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) tools = [ {"type": "function", "function": {"name": "web_search"}}, {"type": "function", "function": {"name": "python"}}, ] events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "search gpus in 2026 prices and use python"}], tools = tools, max_tool_iterations = 4, ) ) assert calls == [ ("web_search", {"query": "gpu prices 2026"}), ("python", {"code": "print('ok')"}), ] assert [ event.get("tool_name") for event in events if event.get("type") == "tool_start" and event.get("tool_name") ] == ["web_search", "python"] assert [ event.get("tool_name") for event in events if event.get("type") == "tool_end" and event.get("tool_name") ] == ["web_search", "python"] assert not [ event for event in events if event.get("tool_call_id") == "call_search_2" and event.get("type") in {"tool_start", "tool_end"} ] assert len(payloads) == 4 assert _tool_names(payloads[2]) == ["web_search", "python"] duplicate_nudges = [ message for message in payloads[2]["messages"] if message.get("role") == "user" and "already completed successfully" in message.get("content", "") ] assert len(duplicate_nudges) == 1 def test_repeated_duplicate_noop_transitions_to_final_pass(monkeypatch): first_search = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_search_1", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, } ] } ), _done(), ] duplicate_one = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_search_2", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, } ] } ), _done(), ] duplicate_two = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_search_3", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, } ] } ), _done(), ] final_stream = [_sse({"content": "Final answer from first search."}), _done()] payloads: list[dict] = [] backend = _make_backend( monkeypatch, [first_search, duplicate_one, duplicate_two, final_stream], payloads, ) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "result" monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "search gpus"}], tools = [{"type": "function", "function": {"name": "web_search"}}], max_tool_iterations = 10, ) ) assert calls == [("web_search", {"query": "gpu prices 2026"})] assert [event.get("tool_call_id") for event in events if event.get("type") == "tool_end"] == [ "call_search_1" ] assert len(payloads) == 4 assert "tools" not in payloads[-1] assert any( event.get("type") == "content" and event.get("text") == "Final answer from first search." for event in events ) def test_same_turn_duplicate_web_search_is_internal_noop(monkeypatch): same_turn_duplicates = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_search_1", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, }, { "index": 1, "id": "call_search_2", "type": "function", "function": { "name": "web_search", "arguments": json.dumps({"query": "gpu prices 2026"}), }, }, ] } ), _done(), ] final_stream = [_sse({"content": "Final answer."}), _done()] payloads: list[dict] = [] backend = _make_backend(monkeypatch, [same_turn_duplicates, final_stream], payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "search-result" monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "search gpus"}], tools = [{"type": "function", "function": {"name": "web_search"}}], max_tool_iterations = 2, ) ) assert calls == [("web_search", {"query": "gpu prices 2026"})] assert [event.get("tool_call_id") for event in events if event.get("type") == "tool_end"] == [ "call_search_1" ] assert not [ event for event in events if event.get("tool_call_id") == "call_search_2" and event.get("type") in {"tool_start", "tool_end"} ] def test_same_turn_repeated_render_html_does_not_emit_second_provisional_start(monkeypatch): same_turn_render_calls = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_html_1", "type": "function", "function": { "name": "render_html", "arguments": json.dumps({"code": "one"}), }, }, { "index": 1, "id": "call_html_2", "type": "function", "function": { "name": "render_html", "arguments": json.dumps({"code": "two"}), }, }, ] } ), _done(), ] final_stream = [_sse({"content": "Final answer."}), _done()] payloads: list[dict] = [] backend = _make_backend(monkeypatch, [same_turn_render_calls, final_stream], payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "Rendered HTML artifact: One." monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "render html"}], tools = [{"type": "function", "function": {"name": "render_html"}}], max_tool_iterations = 2, ) ) assert calls == [("render_html", {"code": "one"})] assert [ event.get("tool_call_id") for event in events if event.get("type") == "tool_start" and not event.get("arguments") ] == ["call_html_1"] assert not [ event for event in events if event.get("tool_call_id") == "call_html_2" and event.get("type") in {"tool_start", "tool_end"} ] assert len(payloads) == 2 assert "tools" not in payloads[1] render_nudges = [ message for message in payloads[1]["messages"] if message.get("role") == "user" and "Do not call render_html again" in message.get("content", "") ] assert len(render_nudges) == 1 def test_disabled_tool_call_is_internal_noop(monkeypatch): disabled_python = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_python_disabled", "type": "function", "function": { "name": "python", "arguments": json.dumps({"code": "print(1)"}), }, } ] } ), _done(), ] final_stream = [_sse({"content": "I cannot run Python here."}), _done()] payloads: list[dict] = [] backend = _make_backend(monkeypatch, [disabled_python, final_stream], payloads) def fake_execute_tool(name, arguments, **_kwargs): raise AssertionError(f"unexpected tool execution: {name} {arguments}") monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "run python"}], tools = [{"type": "function", "function": {"name": "web_search"}}], max_tool_iterations = 1, ) ) assert not [event for event in events if event.get("type") in {"tool_start", "tool_end"}] assert len(payloads) == 2 disabled_nudges = [ message for message in payloads[1]["messages"] if message.get("role") == "user" and "not enabled" in message.get("content", "") ] assert len(disabled_nudges) == 1 def test_render_html_success_does_not_reprompt_render_html_intent(monkeypatch): """After render_html succeeds, do not force another render_html call. The post-tool model pass can say it will use render_html again without emitting a tool call. That should be accepted as a final model mistake, not turned into repeated internal re-prompts after the artifact already exists. """ first_stream = [ _sse( { "tool_calls": [ { "index": 0, "id": "call_first", "type": "function", "function": { "name": "render_html", "arguments": json.dumps( { "code": "first", "title": "First", } ), }, } ] } ), _done(), ] post_tool_stream = [ _sse({"content": "I will now use render_html again."}), _done(), ] payloads: list[dict] = [] backend = _make_backend(monkeypatch, [first_stream, post_tool_stream], payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "Rendered HTML artifact: First." monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) tools = [ { "type": "function", "function": { "name": "render_html", "description": "Render HTML.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], }, }, } ] events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "Make a red square."}], tools = tools, max_tool_iterations = 1, ) ) assert len(payloads) == 2 assert len(calls) == 1 assert any( event.get("type") == "content" and event.get("text") == "I will now use render_html again." for event in events ) def test_internal_reprompt_attempts_do_not_duplicate_visible_text(monkeypatch): """No-tool re-prompt attempts should not concatenate into the UI.""" streams = [ [_sse({"content": "I will use render_html now."}), _done()], [_sse({"content": "Understood. I will use render_html now."}), _done()], ] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) def fake_execute_tool(name, arguments, **_kwargs): raise AssertionError(f"unexpected tool execution: {name} {arguments}") monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) tools = [ { "type": "function", "function": { "name": "render_html", "description": "Render HTML.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], }, }, } ] events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "Make a red square."}], tools = tools, max_tool_iterations = 1, ) ) content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] assert content_texts == ["I will use render_html now."] assert len(payloads) == 2 def test_forced_reprompt_plain_final_answer_is_visible(monkeypatch): """A hidden forced re-prompt may fall back to a plain final answer.""" streams = [ [_sse({"content": "I will use render_html now."}), _done()], [ _sse({"content": "No tool is needed. Final answer: use a red square."}), _done(), ], ] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) def fake_execute_tool(name, arguments, **_kwargs): raise AssertionError(f"unexpected tool execution: {name} {arguments}") monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "Make a red square."}], tools = [ { "type": "function", "function": { "name": "render_html", "description": "Render HTML.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], }, }, } ], max_tool_iterations = 1, ) ) content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] assert content_texts == [ "I will use render_html now.", "No tool is needed. Final answer: use a red square.", ] assert len(payloads) == 2 def test_internal_reprompt_disabled_when_auto_heal_disabled(monkeypatch): streams = [[_sse({"content": "I will use render_html now."}), _done()]] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) def fake_execute_tool(name, arguments, **_kwargs): raise AssertionError(f"unexpected tool execution: {name} {arguments}") monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) tools = [ { "type": "function", "function": { "name": "render_html", "description": "Render HTML.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], }, }, } ] events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "Make a red square."}], tools = tools, max_tool_iterations = 1, auto_heal_tool_calls = False, ) ) content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] assert content_texts == ["I will use render_html now."] assert len(payloads) == 1 def test_auto_heal_disabled_parses_well_formed_xml_when_tools_enabled(monkeypatch): streams = [ [ _sse( { "content": '{"name":"web_search","arguments":{"query":"x"}}' } ), _done(), ], [_sse({"content": "done"}), _done()], ] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "result" monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "search"}], tools = [{"type": "function", "function": {"name": "web_search"}}], auto_heal_tool_calls = False, max_tool_iterations = 1, ) ) assert calls == [("web_search", {"query": "x"})] assert not any( event.get("type") == "content" and "" in event.get("text", "") for event in events ) def test_reprompted_tool_call_still_streams_final_answer(monkeypatch): """Suppression ends once a forced re-prompt actually calls a tool.""" streams = [ [_sse({"content": "I will use render_html now."}), _done()], [ _sse( { "tool_calls": [ { "index": 0, "id": "call_forced", "type": "function", "function": { "name": "render_html", "arguments": json.dumps( { "code": "forced", "title": "Forced", } ), }, } ] } ), _done(), ], [_sse({"content": "Final note after tool."}), _done()], ] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "Rendered HTML artifact: Forced." monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) tools = [ { "type": "function", "function": { "name": "render_html", "description": "Render HTML.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], }, }, } ] events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "Make a red square."}], tools = tools, max_tool_iterations = 1, ) ) assert len(calls) == 1 content_texts = [event.get("text", "") for event in events if event.get("type") == "content"] assert content_texts == ["I will use render_html now.", "Final note after tool."] assert len(payloads) == 3 def test_confirm_tool_calls_allow_executes_gguf_tool(monkeypatch): streams = [ _structured_tool_call("python", {"code": "print(1)"}, "call_py"), [_sse({"content": "Done."}), _done()], ] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "OK" monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) monkeypatch.setattr("core.inference.llama_cpp.new_approval_id", lambda: "approval-1") monkeypatch.setattr( "core.inference.llama_cpp.begin_tool_decision", lambda *_a, **_k: object(), ) monkeypatch.setattr("core.inference.llama_cpp.wait_tool_decision", lambda *_a, **_k: "allow") events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "run python"}], tools = [{"type": "function", "function": {"name": "python"}}], max_tool_iterations = 1, confirm_tool_calls = True, session_id = "sess", ) ) starts = [event for event in events if event.get("type") == "tool_start"] assert len(starts) == 1 assert starts[0]["approval_id"] assert starts[0]["awaiting_confirmation"] is True assert calls == [("python", {"code": "print(1)"})] assert any(event.get("type") == "tool_end" and event.get("result") == "OK" for event in events) def test_confirm_tool_calls_close_after_prompt_cleans_gguf_slot(monkeypatch): approval_id = "approval-close" streams = [_structured_tool_call("python", {"code": "print(1)"}, "call_py")] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) monkeypatch.setattr( "core.inference.tools.execute_tool", lambda *_a, **_k: (_ for _ in ()).throw(AssertionError("tool should not run")), ) monkeypatch.setattr("core.inference.llama_cpp.new_approval_id", lambda: approval_id) with tool_approvals._lock: tool_approvals._pending.clear() gen = backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "run python"}], tools = [{"type": "function", "function": {"name": "python"}}], max_tool_iterations = 1, confirm_tool_calls = True, session_id = "sess", ) try: assert next(gen)["type"] == "status" start = next(gen) assert start["type"] == "tool_start" assert start["approval_id"] == approval_id with tool_approvals._lock: assert approval_id in tool_approvals._pending finally: gen.close() with tool_approvals._lock: assert approval_id not in tool_approvals._pending assert resolve_tool_decision(approval_id, "allow", session_id = "sess") is False def test_confirm_tool_calls_skips_gguf_rag_autoinject(monkeypatch): streams = [[_sse({"content": "Done."}), _done()]] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) def fail_autoinject(*_args, **_kwargs): raise AssertionError("RAG autoinject must not run before approval") monkeypatch.setattr("core.inference.tools.build_rag_autoinject", fail_autoinject) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "use docs"}], tools = [{"type": "function", "function": {"name": "search_knowledge_base"}}], max_tool_iterations = 1, confirm_tool_calls = True, session_id = "sess", rag_scope = {"thread_id": "t1"}, ) ) assert any(event.get("type") == "content" and event.get("text") == "Done." for event in events) def test_confirm_tool_calls_deny_skips_gguf_tool_and_retry_can_execute(monkeypatch): same_call = _structured_tool_call("python", {"code": "print(1)"}, "call_py") streams = [ same_call, _structured_tool_call("python", {"code": "print(1)"}, "call_py_retry"), [_sse({"content": "Done."}), _done()], ] payloads: list[dict] = [] backend = _make_backend(monkeypatch, streams, payloads) calls: list[tuple[str, dict]] = [] def fake_execute_tool(name, arguments, **_kwargs): calls.append((name, arguments)) return "OK" decisions = iter(["deny", "allow"]) approvals = iter(["approval-1", "approval-2"]) monkeypatch.setattr("core.inference.tools.execute_tool", fake_execute_tool) monkeypatch.setattr("core.inference.llama_cpp.new_approval_id", lambda: next(approvals)) monkeypatch.setattr( "core.inference.llama_cpp.begin_tool_decision", lambda *_a, **_k: object(), ) monkeypatch.setattr( "core.inference.llama_cpp.wait_tool_decision", lambda *_a, **_k: next(decisions), ) events = list( backend.generate_chat_completion_with_tools( messages = [{"role": "user", "content": "run python"}], tools = [{"type": "function", "function": {"name": "python"}}], max_tool_iterations = 2, confirm_tool_calls = True, session_id = "sess", ) ) starts = [event for event in events if event.get("type") == "tool_start"] ends = [event for event in events if event.get("type") == "tool_end"] assert len(starts) == 2 assert [event["result"] for event in ends] == [TOOL_REJECTED_MESSAGE, "OK"] assert calls == [("python", {"code": "print(1)"})]