The stirrup-agent runner drives Artificial Analysis’ Stirrup framework against the AssetOpsBench MCP servers. It is a peer to plan-execute, claude-agent, openai-agent, and deep-agent: same CLI contract, same persisted Trajectory, scored by the same uv run evaluate.
Unlike the other runners, Stirrup is a code-capable agent — it can write and execute Python to solve a task, in addition to calling the domain MCP tools. That introduces a code track distinct from the tools-only runners.
deep-agent (no subprocess, no session-file parsing). The runner maps Stirrup’s returned message history straight into the shared Trajectory.watsonx/... and other <provider>/<model> strings work directly; the litellm_proxy/ prefix is also supported.The dependency and entry point are declared in pyproject.toml
(stirrup[mcp,litellm,docker] and the stirrup-agent script). From the repo root:
uv sync
Confirm the entry point resolved (no model call):
uv run stirrup-agent --help
Run the unit tests for the trajectory mapping:
uv run pytest src/agent/stirrup_agent/tests/ -q
Start with --no-code — it needs no Docker, so it isolates “does the MCP wiring
work” from “does code execution work.” Set your model credentials, then:
export WATSONX_APIKEY=... WATSONX_PROJECT_ID=... WATSONX_URL=https://us-south.ml.cloud.ibm.com
uv run stirrup-agent --no-code --show-trajectory \
--model-id watsonx/meta-llama/llama-4-maverick-17b-128e-instruct-fp8 \
"What sites are available?"
In the --show-trajectory output you should see a real domain tool call
(e.g. iot__sites) and a sensible answer. That confirms the {server}__{tool}
wiring and the trajectory parse.
Startup warnings. Stirrup prints three: local filesystem access (only on
--code-backend local), “Missing default tool: WebToolProvider” (intentional — web tools are deliberately not attached so they cannot contaminate the benchmark), and “no output_dir set” (files the agent creates are discarded; harmless for computational answers). None is an error.
Tool registration is independent of whether the model used a tool. To confirm every server connected, list the provider’s tools directly:
uv run python - <<'PY'
import asyncio
from agent.stirrup_agent.runner import StirrupAgentRunner
async def main():
provider = StirrupAgentRunner(code_enabled=False)._build_mcp_provider()
async with provider.connect() as p:
names = sorted(t.name for t in p.get_all_tools())
by = {}
for n in names:
by.setdefault(n.split("__", 1)[0], []).append(n)
for srv, ts in by.items():
print(f"{srv}: {len(ts)}")
asyncio.run(main())
PY
Expected default counts: iot 12, utilities 6, fmsr 3, tsfm 41, wo 15, vibration 8
(85 MCP tools total; the agent additionally has Stirrup’s own finish tool). If AOB_READONLY=1
is set, wo exposes 9 read tools instead of 15.
The runner’s default model is
watsonx/meta-llama/llama-4-maverick-17b-128e-instruct-fp8.
--model-id prefix |
Client | Notes |
|---|---|---|
<provider>/<model> |
Stirrup LiteLLMClient |
Native LiteLLM. watsonx/..., anthropic/..., etc. work directly. |
litellm_proxy/ or tokenrouter/ |
ChatCompletionsClient |
Uses only the OpenAI-compatible Chat Completions API. |
Required env vars match the rest of the repo: the standard watsonx vars for the
native route, LITELLM_BASE_URL / LITELLM_API_KEY for the LiteLLM proxy, or
TOKENROUTER_BASE_URL / TOKENROUTER_API_KEY for TokenRouter.
For context management, the runner gives Stirrup a 100,000-token working-context budget while leaving each underlying client’s 64,000 maximum-output-token default unchanged. The runner requests summarization at 75% of that budget, or approximately 75,000 tokens. This budget controls context compaction and is not model metadata supplied by TokenRouter. When summarization occurs, the complete generated summary is printed during the run rather than the truncated preview used by Stirrup’s default logger.
| Flag | Behaviour |
|---|---|
--code-enabled (default) |
Adds a code-execution tool. The agent may solve a scenario by writing code. Code track. |
--no-code |
Tools only. Directly comparable to claude-agent / openai-agent / deep-agent. |
A code-enabled run and a tools-only run are not comparable 1:1 — report them on separate leaderboard tracks.
Backends (--code-backend):
| Backend | Isolation | Host file access | When to use |
|---|---|---|---|
docker |
full | none (container filesystem) | unattended runs; the default |
local |
none | reads host paths directly | development / trusted inputs; fastest, no Docker |
localruns model-authored code on your host with your permissions. Use it for inputs you control; preferdockerfor unattended or untrusted runs.
When code execution is enabled, the runner appends backend-specific guidance to
the shared agent prompt. It directs the model to answer directly when domain
knowledge, MCP results, or basic reasoning are sufficient; reserve code_exec
for necessary computation, data processing, workspace inspection, or validation;
and never use it for planning, comments, placeholders, or empty scripts. When
code is needed, the prompt prefers one bounded inspect/analyze/verify script and
small outputs. Docker runs identify /workspace and the installed NumPy, pandas,
and SciPy packages; local runs warn that commands execute with the current user’s
host permissions. --no-code keeps the shared prompt unchanged.
The Docker Python SDK must reach the daemon. On macOS with Rancher Desktop the
socket is not at the default path, so point the SDK at it (find it with
docker context inspect | grep Host):
export DOCKER_HOST=unix:///Users/<you>/.rd/docker.sock # Rancher Desktop
docker info # confirm daemon is up
uv run python -c "import docker; print(docker.from_env().version()['Version'])"
Rancher Desktop must use the dockerd (moby) engine, not containerd.
The default image name is assetops-code. Build it once from the bundled
Dockerfile, which adds NumPy, pandas, and SciPy to python:3.12-slim:
docker build -f src/agent/stirrup_agent/Dockerfile.code -t assetops-code .
Set STIRRUP_CODE_IMAGE only when using a different tag.
Dockerfile.code:
FROM python:3.12-slim
RUN pip install --no-cache-dir \
"numpy>=1.24" "pandas>=2.0" "scipy>=1.10"
export DOCKER_HOST=unix:///Users/<you>/.rd/docker.sock
export STIRRUP_CODE_IMAGE=assetops-code
uv run stirrup-agent --code-backend docker --show-trajectory \
--workspace-dir /tmp/assetopsbench-stirrup/smoke \
--preserve-workspace \
--model-id watsonx/meta-llama/llama-4-maverick-17b-128e-instruct-fp8 \
"Run python to compute the factorial of 12 and tell me the result"
The first run pulls/builds the image, so expect a delay. Look for a code_exec
call and the right answer (479001600).
On the code track, MCP text results larger than 100 KiB (102,400 UTF-8 bytes) are
automatically written under mcp_results/ in the active code workspace. The
tool response returned to the model is a compact JSON handle containing the
relative path, tool arguments, byte count, and SHA-256 digest. code_exec can
read that path directly without copying the original response through another
model turn.
The file contains the complete, unmodified MCP response—including null fields—
rather than a projected subset. Code should inspect only the schema, counts, a
small sample, or the specific rows or fields needed, then process the artifact
in place. For artifacts larger than 200 KiB, extract and process the relevant
subset in bounded batches instead of printing the full payload.
Within one agent run, identical read calls reuse an intact existing artifact. Successful work-order mutations, catalog mutations, and TSFM run-producing tools clear the read cache so later reads observe their changes. This is a run-local snapshot cache; it does not detect updates made externally during the run. Artifacts are content-addressed, so a refreshed response does not overwrite an earlier snapshot. Smaller responses remain inline, and persistence failures safely fall back to the original inline MCP result.
For example:
python3 - <<'PY'
import json
with open("mcp_results/wo__list_workorders_<query-id>_<content-id>.json") as f:
result = json.load(f)
print(len(result["work_orders"]))
PY
The workspace is temporary unless --preserve-workspace is enabled.
By default, Stirrup creates a temporary execution directory for code_exec and
removes it when the agent session exits. Pass --workspace-dir to choose the host
base directory for that sandbox, and pass --preserve-workspace to copy the final
code-execution files back into that directory before cleanup.
For scenario-suite runs, use a root outside the repo:
uv run python -m benchmark.scenario_suite_runner \
--scenario-root /path/to/scenarios_data \
--agent_name stirrup_agent \
--stirrup-workspace-root /tmp/assetopsbench-stirrup-workspaces \
--preserve-workspaces
For scenario 401, the preserved files are available under a nested path such as:
/tmp/assetopsbench-stirrup-workspaces/stirrup_agent/tokenrouter-MiniMax-M3/stirrup_agent_401/
In addition to the common flags (--model-id,
--show-trajectory, --json, --run-id, --scenario-id, …):
| Flag | Description |
|---|---|
--code-enabled |
Enable code execution (default). The code track. |
--no-code |
Tools-only; comparable to the other runners. |
--code-backend |
docker (default) or local. |
--max-turns N |
Max agent turns (default: 30). |
--reasoning-effort LEVEL |
Reasoning effort (none, minimal, low, medium, high, xhigh, max, or default); provider default when omitted. |
--workspace-dir PATH |
Host base directory for Docker/local code-execution workspaces. |
--preserve-workspace |
Copy final code-execution files into --workspace-dir before cleanup. |
Environment variable: STIRRUP_CODE_IMAGE (Docker image; default assetops-code).
Because a code-capable agent can bypass the domain tools (solve by writing code
instead of calling wo__* / tsfm__* / …), every run records how its tool calls
were routed, as span attributes on the run:
| Attribute | Meaning |
|---|---|
agent.code_track |
Whether code execution was enabled. |
agent.domain_tool_calls |
Count of MCP ({server}__{tool}) calls. |
agent.code_tool_calls |
Count of code_exec calls. |
agent.tool_bypass |
true if it used code but no domain tools. |
This quantifies how often the agent abandoned the grounded tools — a reportable measurement, not a bug. See docs/observability.md for reading spans.
A reproducible checklist a teammate can follow to confirm a working setup:
# 0. install + entry point
uv sync
uv run stirrup-agent --help
# 1. unit tests (no services)
uv run pytest src/agent/stirrup_agent/tests/ -q
# 2. all six MCP servers register (see "Verify all six MCP servers" above)
# 3. tools-only smoke (no Docker)
uv run stirrup-agent --no-code --show-trajectory \
--model-id watsonx/meta-llama/llama-4-maverick-17b-128e-instruct-fp8 \
"What sites are available?"
# 4. local code track (no Docker)
uv run stirrup-agent --code-backend local --show-trajectory \
--model-id watsonx/meta-llama/llama-4-maverick-17b-128e-instruct-fp8 \
"Compute the factorial of 12 using python"
# 5. docker code track (after building assetops-code + setting DOCKER_HOST)
uv run stirrup-agent --code-backend docker --show-trajectory \
--model-id watsonx/meta-llama/llama-4-maverick-17b-128e-instruct-fp8 \
"Run python to compute the factorial of 12 and tell me the result"
# 6. persist
export AGENT_TRAJECTORY_DIR=$(pwd)/traces/trajectories
uv run stirrup-agent --no-code --run-id stirrup-smoke --scenario-id 101 \
--model-id watsonx/meta-llama/llama-4-maverick-17b-128e-instruct-fp8 \
"List all failure modes of asset Chiller."
| Symptom | Cause / fix |
|---|---|
docker.errors.DockerException: Error while fetching server API version ... FileNotFoundError |
SDK can’t find the daemon socket. export DOCKER_HOST=unix://<path from 'docker context inspect | grep Host'>. |
Docker connects but code_exec hits ModuleNotFoundError |
Sandbox image lacks the library; build/point STIRRUP_CODE_IMAGE at assetops-code (or an image with the stack). |
| Agent code cannot open a server-returned dataset or result file pointer in Docker | Those host-side pointers are separate from automatically bridged mcp_results/ snapshots. Use an MCP tool that consumes the pointer, or use --code-backend local when direct host-file access is required. |
uv sync fails to resolve |
A pin (e.g. litellm==...) clashing with Stirrup’s range; relax to a compatible range. |
stirrup import errors |
stirrup[mcp,litellm,docker] not installed; re-run uv sync. |
| A server missing from the tool list | Its subprocess failed to start (missing CouchDB creds for iot/wo/vibration, or model load for tsfm). |
src/agent/stirrup_agent/ — runner, trajectory mapping, workspace bridging,
CLI, Docker image, and focused tests.src/agent/_prompts.py — concise enforcement of user-requested response formats.pyproject.toml — stirrup[mcp,litellm,docker] dependency and the stirrup-agent
entry point.The MCP servers themselves are unchanged.