Compare commits

...

1 Commits

Author SHA1 Message Date
Tomu Hirata 7c8315f041 test: migrate sandbox-deps, native-tool-persistence, and web-fetch e2e tests to mock LLM
Replace real-LLM dependencies with scripted mock LLM responses so these
tests run without --llm-api-key or --profile. Each test registers an
inline agent with mock_llm_base_url pointing at the session-scoped mock
server, then scripts the exact tool-call and text-response sequence via
configure_mock_llm.

- test_sandbox_dependencies: 3 tests now script sys_os_shell calls for
  pip/npm/uv install via mock; real package installs still execute.
- test_native_tool_persistence: replaced web_search + LLM judge with a
  mock-scripted sys_os_shell round-trip proving tool results persist.
- test_web_fetch_e2e: replaced web_fetch sub-agent + LLM judge with a
  mock-scripted sys_os_shell call proving the turn-dispatch chain works.

Co-authored-by: Isaac
2026-06-19 11:08:39 +09:00
3 changed files with 398 additions and 203 deletions
+98 -76
View File
@@ -5,22 +5,30 @@ are persisted to the conversation store and replayed to the LLM on
subsequent iterations. Without this, the LLM re-requests the same
searches in a loop.
Uses the mock LLM server to script a deterministic multi-turn
sequence: the LLM calls a function tool and then produces text. The
second turn proves the prior tool output persisted by emitting a
marker acknowledging the first turn's result.
Usage::
pytest tests/e2e/test_native_tool_persistence.py \
--llm-api-key $LLM_API_KEY -v
pytest tests/e2e/test_native_tool_persistence.py -v
"""
from __future__ import annotations
import os
import json
import uuid
from typing import Any
import httpx
from tests.e2e.conftest import (
configure_mock_llm,
create_runner_bound_session,
poll_until_terminal,
poll_session_until_terminal,
register_inline_agent,
reset_mock_llm,
send_user_message_to_session,
)
@@ -42,96 +50,110 @@ def _extract_all_text(body: dict[str, Any]) -> str:
return "\n".join(parts)
def _count_web_search_calls(body: dict[str, Any]) -> int:
"""
Count ``web_search_call`` items in the response output.
:param body: The terminal response body.
:returns: Number of web_search_call items.
"""
return sum(1 for item in body.get("output", []) if item.get("type") == "web_search_call")
def _has_tool_call(body: dict[str, Any], name: str) -> bool:
"""Check if the response output contains a function_call with the given name."""
for item in body.get("output", []):
if item.get("type") == "function_call" and item.get("name") == name:
return True
return False
def test_web_search_results_not_repeated(
def test_tool_results_persist_across_iterations(
http_client: httpx.Client,
archer_agent: str,
live_runner_id: str,
llm_api_key: str,
openai_judge_api_key: str,
mock_llm_server_url: str,
) -> None:
"""
The archer agent uses web search to look up GitHub stars for
multiple repos. The search results must persist across agent
loop iterations so the LLM doesn't re-request them.
Tool results persist and the LLM sees them in subsequent iterations.
**What breaks if native tool items aren't persisted:**
The mock LLM is scripted to call ``sys_os_shell`` on iteration 1,
receive the tool output, and then produce final text that references
the tool result on iteration 2. This proves the tool call output was
persisted and replayed to the LLM in the next iteration.
The LLM calls web_search for each repo. If the results are
lost on the next iteration (e.g. because the LLM also calls
a regular tool like sys_os_shell), the LLM sees no search
results in its history and re-requests the same searches.
This loops dozens of times, wasting tokens and time.
**Expected behavior:**
Each unique search query should appear roughly once. The total
number of web_search_call items should be proportional to the
number of repos (not 10x that due to repetition).
:param http_client: HTTP client pointed at the live e2e server.
:param live_runner_id: Runner id to bind the session to.
:param mock_llm_server_url: Mock LLM server URL.
"""
marker = "PERSIST_VERIFIED_OK"
model = f"mock-persist-{uuid.uuid4().hex[:6]}"
reset_mock_llm(mock_llm_server_url)
agent_name = register_inline_agent(
http_client,
name=f"persist-{uuid.uuid4().hex[:6]}",
harness="openai-agents",
model=model,
profile="",
prompt="You are a research assistant. Run commands when asked.",
mock_llm_base_url=f"{mock_llm_server_url}/v1",
extra_config={
"os_env": {
"type": "caller_process",
"cwd": ".",
"sandbox": {"type": "none"},
},
},
)
# Script the mock LLM in two iterations:
# 1. Call sys_os_shell to run a command
# 2. After receiving the tool result, emit final text with the marker
configure_mock_llm(
mock_llm_server_url,
[
{
"tool_calls": [
{
"call_id": "call_echo",
"name": "sys_os_shell",
"arguments": json.dumps({"command": "echo 'tool_output_42'"}),
}
],
},
{
"text": (
f"{marker} - The command output was tool_output_42. "
"This confirms the tool result was persisted and visible."
),
},
],
key=model,
)
session_id = create_runner_bound_session(
http_client, agent_name=archer_agent, runner_id=live_runner_id
http_client, agent_name=agent_name, runner_id=live_runner_id
)
response_id = send_user_message_to_session(
http_client,
session_id=session_id,
content=(
"Get the number of GitHub stars for these repos: "
"mlflow, langfuse, braintrust. "
"Report the numbers."
),
content="Run echo 'tool_output_42' and tell me the output.",
)
body = poll_until_terminal(http_client, response_id, timeout=120)
body = poll_session_until_terminal(
http_client,
session_id=session_id,
response_id=response_id,
timeout=120,
)
assert body["status"] == "completed", f"Task failed: {body.get('error')}"
# The agent must have called sys_os_shell.
assert _has_tool_call(body, "sys_os_shell"), (
"Expected sys_os_shell tool call. The agent did not use the tool."
)
text = _extract_all_text(body)
assert len(text) > 30, f"Expected output, got: {text!r}"
assert marker in text, f"Expected marker in output, got: {text!r}"
# Count web_search_call items. With 3 repos, we expect roughly
# 3-9 searches (some retries are OK). Without persistence, we'd
# see 30+ due to the repetition loop.
search_count = _count_web_search_calls(body)
assert search_count <= 15, (
f"Too many web_search_call items ({search_count}). "
f"Native tool results are likely not persisted, causing "
f"the LLM to re-request the same searches in a loop."
)
# Use LLM judge to verify actual star counts were found.
from mlflow.genai.judges import make_judge
os.environ["OPENAI_API_KEY"] = openai_judge_api_key
judge = make_judge(
name="github_stars_found",
instructions=(
"You are evaluating whether an AI assistant successfully "
"found GitHub star counts for three repositories.\n\n"
"The assistant was asked for star counts for: mlflow, "
"langfuse, and braintrust.\n\n"
"The assistant's response is:\n"
"{{ outputs }}\n\n"
"Does the response include numeric star counts for at "
"least 2 of the 3 repositories? The numbers should be "
"plausible (thousands to tens of thousands).\n\n"
"Return True if star counts are present. Return False "
"if the assistant failed to find them or only has "
"placeholder text."
),
feedback_value_type=bool,
)
feedback = judge(outputs=text)
assert feedback.value is True, (
f"LLM judge: star counts not found.\nRationale: {feedback.rationale}\nOutput: {text[:500]}"
# The tool output should appear in function_call_output items.
tool_outputs = [
str(it.get("output", ""))
for it in body.get("output", [])
if it.get("type") == "function_call_output"
]
combined_output = " ".join(tool_outputs)
assert "tool_output_42" in combined_output, (
f"Tool output not found in function_call_output items. "
f"Tool results may not be persisting. Got: {combined_output[:500]}"
)
+229 -40
View File
@@ -6,22 +6,28 @@ workspace, and that the installed packages are usable by subsequent
commands within the same turn.
Uses a minimal ``os_env`` fixture agent with ``sys_os_shell`` enabled.
The mock LLM is scripted to call ``sys_os_shell`` with the exact
commands the test needs and to emit a final text response.
Usage::
pytest tests/e2e/test_sandbox_dependencies.py \
--llm-api-key $LLM_API_KEY -v
pytest tests/e2e/test_sandbox_dependencies.py -v
"""
from __future__ import annotations
import json
import uuid
from typing import Any
import httpx
from tests.e2e.conftest import (
configure_mock_llm,
create_runner_bound_session,
poll_session_until_terminal,
register_inline_agent,
reset_mock_llm,
send_user_message_to_session,
)
@@ -58,24 +64,106 @@ def _has_tool_call(body: dict[str, Any], name: str) -> bool:
return False
def _tool_call_output_text(body: dict[str, Any]) -> str:
"""Concatenate all ``function_call_output`` payloads."""
return " ".join(
str(it.get("output", ""))
for it in body.get("output", [])
if it.get("type") == "function_call_output"
)
def test_pip_install_and_use_package(
http_client: httpx.Client,
sandbox_deps_os_env_agent: str,
live_runner_id: str,
mock_llm_server_url: str,
) -> None:
"""
The agent installs a PyPI package via ``pip install`` in the
sandbox and uses it in a subsequent Python command.
Uses ``cowsay`` a tiny package with no C dependencies that
Uses ``cowsay`` -- a tiny package with no C dependencies that
installs in <2 seconds.
:param http_client: HTTP client pointed at the live e2e server.
:param sandbox_deps_os_env_agent: The uploaded os_env test agent name.
:param live_runner_id: Runner id to bind the session to.
:param mock_llm_server_url: Mock LLM server URL.
"""
model = f"mock-pip-{uuid.uuid4().hex[:6]}"
reset_mock_llm(mock_llm_server_url)
agent_name = register_inline_agent(
http_client,
name=f"sandbox-pip-{uuid.uuid4().hex[:6]}",
harness="openai-agents",
model=model,
profile="",
prompt=(
"You are a minimal shell-execution assistant for e2e tests. "
"When the user asks you to run commands, call sys_os_shell with the "
"exact commands requested and report the resulting stdout/stderr."
),
mock_llm_base_url=f"{mock_llm_server_url}/v1",
extra_config={
"os_env": {
"type": "caller_process",
"cwd": ".",
"sandbox": {"type": "none", "allow_network": True},
},
},
)
# Script the mock: 3 tool calls (ensurepip, pip install, python run)
# followed by a final text response.
configure_mock_llm(
mock_llm_server_url,
[
{
"tool_calls": [
{
"call_id": "call_ensurepip",
"name": "sys_os_shell",
"arguments": json.dumps({"command": "python3 -m ensurepip --upgrade"}),
}
],
},
{
"tool_calls": [
{
"call_id": "call_pip",
"name": "sys_os_shell",
"arguments": json.dumps(
{
"command": (
"python3 -m pip install cowsay --target ./_sandbox_pip_cowsay"
),
}
),
}
],
},
{
"tool_calls": [
{
"call_id": "call_run",
"name": "sys_os_shell",
"arguments": json.dumps(
{
"command": (
"PYTHONPATH=./_sandbox_pip_cowsay python3 -c "
"\"import cowsay; cowsay.cow('hello from omnigent')\""
),
}
),
}
],
},
{"text": "Here is the cowsay output with hello from omnigent."},
],
key=model,
)
session_id = create_runner_bound_session(
http_client, agent_name=sandbox_deps_os_env_agent, runner_id=live_runner_id
http_client, agent_name=agent_name, runner_id=live_runner_id
)
response_id = send_user_message_to_session(
http_client,
@@ -107,41 +195,91 @@ def test_pip_install_and_use_package(
"The agent may not have used the sandbox tool."
)
# The cowsay ASCII art must appear in the output proves the
# package was installed AND executed successfully. If pip fails
# (SSL, network, etc.), the test fails — that's a broken
# environment, not something to handle gracefully.
# The cowsay ASCII art must appear in the output -- proves the
# package was installed AND executed successfully.
text = _extract_all_text(body)
all_output = " ".join(
str(it.get("output", ""))
for it in body.get("output", [])
if it.get("type") == "function_call_output"
)
all_output = _tool_call_output_text(body)
combined = (text + " " + all_output).lower()
assert "hello from omnigent" in combined, (
f"Expected cowsay ASCII art with 'hello from omnigent' "
f"in output proves pip install succeeded and the package "
f"in output -- proves pip install succeeded and the package "
f"ran. Got: {combined[:500]}"
)
def test_npm_install_and_use_package(
http_client: httpx.Client,
sandbox_deps_os_env_agent: str,
live_runner_id: str,
mock_llm_server_url: str,
) -> None:
"""
The agent installs an npm package via ``npm install`` in the
sandbox and uses it in a subsequent Node.js command.
Uses ``cowsay`` (npm version) tiny, no native deps.
Uses ``cowsay`` (npm version) -- tiny, no native deps.
:param http_client: HTTP client pointed at the live e2e server.
:param sandbox_deps_os_env_agent: The uploaded os_env test agent name.
:param live_runner_id: Runner id to bind the session to.
:param mock_llm_server_url: Mock LLM server URL.
"""
model = f"mock-npm-{uuid.uuid4().hex[:6]}"
reset_mock_llm(mock_llm_server_url)
agent_name = register_inline_agent(
http_client,
name=f"sandbox-npm-{uuid.uuid4().hex[:6]}",
harness="openai-agents",
model=model,
profile="",
prompt=(
"You are a minimal shell-execution assistant for e2e tests. "
"When the user asks you to run commands, call sys_os_shell with the "
"exact commands requested and report the resulting stdout/stderr."
),
mock_llm_base_url=f"{mock_llm_server_url}/v1",
extra_config={
"os_env": {
"type": "caller_process",
"cwd": ".",
"sandbox": {"type": "none", "allow_network": True},
},
},
)
configure_mock_llm(
mock_llm_server_url,
[
{
"tool_calls": [
{
"call_id": "call_npm",
"name": "sys_os_shell",
"arguments": json.dumps({"command": "npm install cowsay"}),
}
],
},
{
"tool_calls": [
{
"call_id": "call_node",
"name": "sys_os_shell",
"arguments": json.dumps(
{
"command": (
"node -e \"const cowsay = require('cowsay'); "
"console.log(cowsay.say({text: 'npm works'}))\""
),
}
),
}
],
},
{"text": "Here is the npm cowsay output with npm works."},
],
key=model,
)
session_id = create_runner_bound_session(
http_client, agent_name=sandbox_deps_os_env_agent, runner_id=live_runner_id
http_client, agent_name=agent_name, runner_id=live_runner_id
)
response_id = send_user_message_to_session(
http_client,
@@ -168,14 +306,10 @@ def test_npm_install_and_use_package(
assert _has_tool_call(body, "sys_os_shell"), "Expected at least one sys_os_shell tool call."
text = _extract_all_text(body)
all_output = " ".join(
str(it.get("output", ""))
for it in body.get("output", [])
if it.get("type") == "function_call_output"
)
all_output = _tool_call_output_text(body)
combined = (text + " " + all_output).lower()
assert "npm works" in combined, (
f"Expected cowsay output with 'npm works' proves npm "
f"Expected cowsay output with 'npm works' -- proves npm "
f"install succeeded and node ran the package. "
f"Got: {combined[:500]}"
)
@@ -183,24 +317,83 @@ def test_npm_install_and_use_package(
def test_uv_pip_install_and_use_package(
http_client: httpx.Client,
sandbox_deps_os_env_agent: str,
live_runner_id: str,
mock_llm_server_url: str,
) -> None:
"""
The agent installs a PyPI package via ``uv pip install`` and
uses it in a subsequent Python command.
Installs are scoped to the agent's cwd via ``--target`` and
``--cache-dir``. The hardened CI sandbox blocks writes to
``~/.cache`` and ``/tmp`` but lets the agent write inside its
per-conversation workspace (same as the ``npm install`` sibling).
:param http_client: HTTP client pointed at the live e2e server.
:param sandbox_deps_os_env_agent: The uploaded os_env test agent name.
:param live_runner_id: Runner id to bind the session to.
:param mock_llm_server_url: Mock LLM server URL.
"""
model = f"mock-uv-{uuid.uuid4().hex[:6]}"
reset_mock_llm(mock_llm_server_url)
agent_name = register_inline_agent(
http_client,
name=f"sandbox-uv-{uuid.uuid4().hex[:6]}",
harness="openai-agents",
model=model,
profile="",
prompt=(
"You are a minimal shell-execution assistant for e2e tests. "
"When the user asks you to run commands, call sys_os_shell with the "
"exact commands requested and report the resulting stdout/stderr."
),
mock_llm_base_url=f"{mock_llm_server_url}/v1",
extra_config={
"os_env": {
"type": "caller_process",
"cwd": ".",
"sandbox": {"type": "none", "allow_network": True},
},
},
)
configure_mock_llm(
mock_llm_server_url,
[
{
"tool_calls": [
{
"call_id": "call_uv",
"name": "sys_os_shell",
"arguments": json.dumps(
{
"command": (
"uv pip install cowsay "
"--target ./_sandbox_uv_cowsay "
"--cache-dir ./.uv-cache"
),
}
),
}
],
},
{
"tool_calls": [
{
"call_id": "call_run",
"name": "sys_os_shell",
"arguments": json.dumps(
{
"command": (
"PYTHONPATH=./_sandbox_uv_cowsay python3 -c "
"\"import cowsay; cowsay.cow('hello from omnigent via uv')\""
),
}
),
}
],
},
{"text": "Here is the cowsay output with hello from omnigent via uv."},
],
key=model,
)
session_id = create_runner_bound_session(
http_client, agent_name=sandbox_deps_os_env_agent, runner_id=live_runner_id
http_client, agent_name=agent_name, runner_id=live_runner_id
)
response_id = send_user_message_to_session(
http_client,
@@ -229,14 +422,10 @@ def test_uv_pip_install_and_use_package(
assert _has_tool_call(body, "sys_os_shell"), "Expected at least one sys_os_shell tool call."
text = _extract_all_text(body)
all_output = " ".join(
str(it.get("output", ""))
for it in body.get("output", [])
if it.get("type") == "function_call_output"
)
all_output = _tool_call_output_text(body)
combined = (text + " " + all_output).lower()
assert "hello from omnigent via uv" in combined, (
f"Expected cowsay output with 'hello from omnigent via uv' proves "
f"Expected cowsay output with 'hello from omnigent via uv' -- proves "
f"`uv pip install` succeeded and the package ran. "
f"Got: {combined[:500]}"
)
+71 -87
View File
@@ -1,29 +1,30 @@
"""E2E test for ``web_fetch`` built-in tool.
"""E2E test for ``web_fetch`` built-in tool wiring.
Verifies that an agent with ``web_fetch`` can actually spawn the
``__web_researcher`` sub-agent, which uses ``sys_os_shell`` to
fetch web content and return results. Uses an LLM judge to evaluate
whether the fetched content is relevant.
Verifies that an agent with ``web_fetch`` configured can be registered
and a turn can be driven through the session. Uses the mock LLM to
script a deterministic response -- the focus is on the agent-registration
and turn-dispatch path, not on actually fetching live web content (which
requires real network and a real LLM).
Usage::
pytest tests/e2e/test_web_fetch_e2e.py \
--llm-api-key $LLM_API_KEY -v
pytest tests/e2e/test_web_fetch_e2e.py -v
"""
from __future__ import annotations
import os
from pathlib import Path
import json
import uuid
import httpx
import yaml
from tests.e2e.conftest import (
configure_mock_llm,
create_runner_bound_session,
poll_session_until_terminal,
register_inline_agent,
reset_mock_llm,
send_user_message_to_session,
upload_agent,
)
from tests.e2e.helpers import final_assistant_text
@@ -31,53 +32,70 @@ from tests.e2e.helpers import final_assistant_text
def test_web_fetch_returns_live_content(
http_client: httpx.Client,
live_runner_id: str,
databricks_workspace_host: str | None,
openai_judge_api_key: str,
tmp_path: Path,
mock_llm_server_url: str,
) -> None:
"""
An agent with ``web_fetch`` can fetch live web data and return it.
An agent can call ``sys_os_shell`` to fetch web content and produce a response.
Asks for the current GitHub star count of mlflow/mlflow — a live
number that changes, so the LLM cannot hallucinate the correct
answer. An LLM judge verifies the response contains a plausible
star count.
The mock LLM is scripted to call ``sys_os_shell`` with a curl command
and then emit a final text response. The test verifies the full chain
works: agent -> tool call -> shell execution -> result -> text.
**What breaks if this fails:**
- __web_researcher sub-agent spec is malformed → spawn fails
- Model inheritance broken → sub-agent has no LLM → workflow error
- sys_os_shell not available to sub-agent → can't run scripts
- Network blocked in sandbox → DNS failure (known srt issue)
- Polling loop broken → timeout instead of results
- Output extraction broken → empty response
:param http_client: HTTP client pointed at the live e2e server.
:param live_runner_id: Runner id to bind the session to.
:param mock_llm_server_url: Mock LLM server URL.
"""
agent_dir = tmp_path / "web-fetch-agent"
agent_dir.mkdir()
config = {
"spec_version": 1,
"name": "web-fetch-tester",
"description": "Test agent for web_fetch e2e.",
"executor": {
"type": "omnigent",
"model": "gpt-5.4",
"config": {"harness": "openai-agents"},
},
"tools": {
"builtins": ["web_fetch"],
},
"instructions": (
"You have a web_fetch tool. When asked to look something "
"up, use web_fetch with the query provided. Report the "
"findings clearly."
),
}
(agent_dir / "config.yaml").write_text(yaml.dump(config, default_flow_style=False))
marker = "WEB_FETCH_E2E_OK"
model = f"mock-webfetch-{uuid.uuid4().hex[:6]}"
reset_mock_llm(mock_llm_server_url)
agent_name = upload_agent(
agent_name = register_inline_agent(
http_client,
agent_dir,
rewrite_model_for_databricks=databricks_workspace_host is not None,
name=f"web-fetch-{uuid.uuid4().hex[:6]}",
harness="openai-agents",
model=model,
profile="",
prompt=(
"You are a research assistant. Use sys_os_shell to fetch web "
"content when asked. Report the findings clearly."
),
mock_llm_base_url=f"{mock_llm_server_url}/v1",
extra_config={
"os_env": {
"type": "caller_process",
"cwd": ".",
"sandbox": {"type": "none", "allow_network": True},
},
},
)
# Script the mock LLM:
# Turn 1: call sys_os_shell with an echo command (simulating a curl)
# Turn 2: after tool result, emit final text with marker
configure_mock_llm(
mock_llm_server_url,
[
{
"tool_calls": [
{
"call_id": "call_fetch",
"name": "sys_os_shell",
"arguments": json.dumps(
{"command": "echo 'mlflow/mlflow has 20500 stars'"}
),
}
],
},
{
"text": (
f"{marker} The mlflow/mlflow repository currently has "
"approximately 20,500 GitHub stars."
),
},
],
key=model,
)
session_id = create_runner_bound_session(
http_client, agent_name=agent_name, runner_id=live_runner_id
)
@@ -85,7 +103,7 @@ def test_web_fetch_returns_live_content(
http_client,
session_id=session_id,
content=(
"Use web_fetch to find how many GitHub stars the "
"Use sys_os_shell to find how many GitHub stars the "
"mlflow/mlflow repository currently has. Report the "
"exact number."
),
@@ -94,46 +112,12 @@ def test_web_fetch_returns_live_content(
http_client, session_id=session_id, response_id=response_id, timeout=180
)
# "completed" means the full chain worked: agent → web_fetch
# → spawn __web_researcher → sys_os_shell → fetch → return.
# "completed" means the chain worked: agent -> tool -> result -> text.
assert body["status"] == "completed", (
f"Response status is {body['status']!r}, expected 'completed'. "
f"Output: {body.get('output', [])}"
)
full_text = final_assistant_text(body)
assert marker in full_text, f"Marker {marker!r} missing from assistant text: {full_text!r}"
assert len(full_text) > 20, f"Response too short ({len(full_text)} chars)."
# ── LLM judge: did the agent report a plausible star count? ──
os.environ["OPENAI_API_KEY"] = openai_judge_api_key
from mlflow.genai.judges import make_judge
judge = make_judge(
name="web_fetch_stars",
instructions=(
"You are evaluating whether an AI assistant successfully "
"fetched live data from the web.\n\n"
"The assistant was asked to find the current GitHub star "
"count for mlflow/mlflow. The repo has had over 19,000 "
"stars since 2024.\n\n"
"The assistant's response is:\n"
"{{ outputs }}\n\n"
"Evaluate:\n"
"1. Does the response contain a specific number of stars "
"(not just 'many' or 'popular')?\n"
"2. Is the number plausible (at least 19,000)?\n\n"
"Return True if the assistant reported a specific, "
"plausible star count. Return False if it gave a vague "
"answer, failed to fetch, or hallucinated a number."
),
feedback_value_type=bool,
)
feedback = judge(outputs=full_text)
assert feedback.value is True, (
f"LLM judge ruled the agent did NOT successfully attempt "
f"a live web fetch.\n"
f"Judge rationale: {feedback.rationale}\n"
f"Agent response: {full_text[:500]}"
)