Workload harnesses for driving test and evaluation traffic to Kagenti agents. The current implementation is the Exgentic A2A Runner (exgentic_a2a_runner/) β a standalone Python runner that integrates Exgentic benchmarks with Kagenti agents using the A2A (Agent-to-Agent) protocol. This harness implements the execution model defined in GitHub Issue #963.
The harness exists to robustly exercise agents and validate that the Kagenti platform is reliable, scalable, and observable.
- Exgentic MCP Integration: Communicates with Exgentic MCP server for benchmark tasks
- Parallel session processing: Configurable concurrency for efficient benchmark execution and stress testing
- A2A protocol support: Communicates with remote agents using the A2A protocol via JSON-RPC over HTTP
- OpenTelemetry instrumentation: Comprehensive traces, metrics, and logs
- Strict failure handling: Any error or timeout marks the session as failed
- Configurable via environment variables: Easy deployment and configuration
- Composable AuthBridge plugin pipeline: Per-agent selection of inbound/outbound plugins (
jwt-validation,token-exchange,a2a-parser,ibac, β¦) withenforce/observe/offpolicies β see AuthBridge Plugin Pipeline.
The minimal end-to-end path: stand up a Kagenti cluster, build the agent + MCP images, then deploy and evaluate. Each block here is self-contained β copy the commands you need.
# 1. Stand up the Kagenti cluster (one-time)
git clone git@github.com:kagenti/kagenti.git
cd kagenti
env CONTAINER_ENGINE=podman scripts/kind/setup-kagenti.sh --with-all --preload-images
cd ..
# 2. Install the runner
git clone git@github.com:kagenti/workload-harness.git
cd workload-harness/exgentic_a2a_runner
uv sync --python 3.12
source .venv/bin/activate
cp example.env .env # edit as needed (OPENAI_API_KEY, IBAC tunables, etc.)
# 3a. Plain run β no AuthBridge sidecar
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling
# 3b. Run with auth + token exchange only
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling \
--plugin-preset auth-only
# 3c. Run with IBAC enforcing intent-based access control
export IBAC_JUDGE_ENDPOINT=http://host.docker.internal:11434
export IBAC_JUDGE_MODEL=llama3.2:3b
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling \
--plugin-preset ibac-only
# 3d. Canary IBAC in observe mode (telemetry only, no blocking)
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling \
--plugin-preset ibac-only --plugin ibac:observeThe deploy + evaluate steps can also be run separately β see Usage. For full plugin pipeline mechanics, see AuthBridge Plugin Pipeline.
The runner follows this execution model for each benchmark session:
- Create Session:
(session_id, task) = mcp_server.create_session() - Invoke Agent:
agent.invoke_agent("{task}"). Pass session_id as meta_data. - Evaluate Session:
success = mcp_server.evaluate_session(session_id) - Close Session:
mcp_server.close_session(session_id) - Record Statistics: Track completion time, success rate, compute costs, tokens.
The runner currently drives three benchmarks via the Exgentic MCP server. Each is a separate MCP image (./build.sh <name> from agent-examples/mcp/exgentic_benchmarks) and is selected at deploy time with --benchmark <name>.
| Benchmark | What it tests | Tool surface | Notes |
|---|---|---|---|
gsm8k |
Grade-school math word problems β single-turn arithmetic reasoning. | Minimal β primarily LLM reasoning, light tool use. | Cheap and fast; good smoke test. The deploy script sets EXGENTIC_SET_BENCHMARK_RUNNER=direct for this benchmark. |
tau2 |
Multi-turn customer-support conversations against a simulated user. Measures whether the agent can complete realistic task flows over several turns. | Domain tools (retail, airline, telecom) plus a user-simulator LLM. | Deploy passes EXGENTIC_SET_BENCHMARK_USER_SIMULATOR_MODEL so the simulator runs on the same model as the agent. The IBAC plugin also lands its canonical attack-shape tests against tau-style multi-turn traffic β see ibac-benchmarking.md. |
appworld |
Long-horizon, tool-heavy tasks across a simulated app ecosystem (calendar, email, contacts, etc.). Stresses tool selection and planning. | Very wide β hundreds of tools across the simulated apps. | OpenAI models can't handle this tool surface without shortlisting; use gemini-2.5-pro or another model with strong tool selection. |
The model name passed via --model (or LLM_MODEL / EXGENTIC_SET_AGENT_MODEL) is consumed by LiteLLM on the agent side, so it follows LiteLLM's <provider>/<model> routing convention. The default is Azure/gpt-4.1.
OpenAI-compatible backends (vLLM, Ollama, llama.cpp, LM Studio, custom proxies, etc.) β prefix the model name with openai/ to force LiteLLM down its OpenAI-compatible route, and point OPENAI_API_BASE at your endpoint:
# Custom Azure deployment fronted by an OpenAI-compatible proxy
./deploy-agent.sh --benchmark tau2 --agent tool_calling \
--model openai/Azure/gpt-4o-mini
# Local model served via vLLM/Ollama
./deploy-agent.sh --benchmark gsm8k --agent tool_calling \
--model openai/llama3.1-70b-instructFor appworld, use a model with strong tool-selection β e.g. gemini-2.5-pro β rather than an OpenAI-route model.
β±οΈ Estimated Setup Time: ~15 minutes (excluding container image pulls)
- Python 3.11 or 3.12 (Python 3.13+ is not supported due to dependency compatibility)
- Note: The
uvpackage manager will automatically use Python 3.12 when you runuv sync --python 3.12, regardless of your system Python version
- Note: The
- uv package manager
- kubectl v0.6.0 (tested on v0.6.0-rc.2)
- Kagenti cluster running with:
- Kagenti backend in
kagenti-systemnamespace - Keycloak in
keycloaknamespace team1namespace for deployments
- Kagenti backend in
AuthBridge sidecar: the deploy scripts can attach an AuthBridge sidecar with a composable plugin pipeline (auth, token exchange, intent-based access control, β¦) to each agent. See AuthBridge Plugin Pipeline for the full surface; the sidecar is only injected when you pass a plugin selector.
git clone git@github.com:kagenti/kagenti.git
cd kagenti
env CONTAINER_ENGINE=podman scripts/kind/setup-kagenti.sh --with-all --preload-images
git clone git@github.com:yoavkatz/workload-harness.git
cd workload-harness
git checkout feature/exgentic-a2a-runner
cd exgentic_a2a_runner
uv sync --python 3.12
source .venv/bin/activate
# Deploy and configure MCP server using Kagenti Tool API
# This script now combines deployment and configuration in one step
./deploy-benchmark.sh --benchmark tau2
# Deploy and configure agent using Kagenti Agent API
# This script now combines deployment and configuration in one step
./deploy-agent.sh --benchmark tau2 --agent tool_callingNote: All deployment scripts now use named parameters:
Benchmark Deployment:
# Basic deployment with defaults (model: Azure/gpt-4.1, keycloak: admin/admin)
./deploy-benchmark.sh --benchmark tau2
# Deploy with custom model
./deploy-benchmark.sh --benchmark tau2 --model Azure/gpt-4o-mini
# Deploy with custom Keycloak credentials
./deploy-benchmark.sh --benchmark tau2 --model Azure/gpt-4o-mini --keycloak-user admin --keycloak-pass admin
# Show help
./deploy-benchmark.sh --helpAgent Deployment:
# Basic deployment with defaults (model: Azure/gpt-4.1, keycloak: admin/admin)
./deploy-agent.sh --benchmark tau2 --agent tool_calling
# Deploy with custom model
./deploy-agent.sh --benchmark tau2 --agent tool_calling --model Azure/gpt-4o-mini
# Deploy with custom Keycloak credentials
./deploy-agent.sh --benchmark tau2 --agent tool_calling --model Azure/gpt-4o-mini --keycloak-user admin --keycloak-pass admin
# Show help
./deploy-agent.sh --helpAgent Naming: Underscores in agent names are automatically converted to hyphens for Kubernetes compatibility (e.g., tool_calling becomes tool-calling).
Important: Both deployment scripts now combine deployment and configuration steps:
deploy-benchmark.sh will:
- Deploy the MCP server to the Kagenti cluster
- Automatically configure secrets before deployment:
- Updates
openai-secretwith OPENAI_API_KEY (if set in environment) - Creates/updates
hf-secretwith HF_TOKEN (uses dummy token if not set)
- Updates
- Configure environment variables (OPENAI_API_BASE, EXGENTIC_SET_BENCHMARK_RUNNER for gsm8k)
- Set memory limits and model settings
- Wait for the deployment to be ready
deploy-agent.sh will:
- Deploy the agent to the Kagenti cluster
- Automatically configure environment variables (OPENAI_API_BASE, OPENAI_API_KEY, LLM_MODEL)
- Set model settings (LLM_MODEL, EXGENTIC_SET_AGENT_MODEL)
- Wait for the deployment to be ready
Environment Variables for Deployment:
OPENAI_API_KEY: OpenAI API key (optional, updates openai-secret if set)HF_TOKEN: HuggingFace token (optional, creates hf-secret with dummy token if not set)OPENAI_API_BASE: OpenAI API base URL (optional, added to deployment env vars)
For benchmark fit and model-name conventions (including the openai/ prefix for OpenAI-compatible backends), see Benchmarks.
The runner can optionally route MCP traffic through an MCP Gateway instead of connecting directly to the benchmark MCP server. When enabled, the gateway acts as a single entry point that multiplexes access to registered MCP servers and namespaces their tools with a configurable prefix.
- Benchmark deployment (
deploy-benchmark.sh --use-mcp-gateway) creates anHTTPRouteand anMCPServerRegistrationCR that registers the MCP server with the gateway. - Agent deployment (
deploy-agent.sh --use-mcp-gateway) points the agent'sMCP_URLat the gateway service (mcp-gateway-istio.gateway-system.svc.cluster.local:8080) instead of the benchmark's MCP service directly. - Evaluation (
evaluate-benchmark.sh) port-forwards the gateway service and setsEXGENTIC_MCP_TOOL_PREFIXso the runner prepends the gateway-assigned prefix to every tool call (e.g.list_tasksbecomesexgentic_list_tasks).
Pass --use-mcp-gateway to any deployment or evaluation script:
# Individual scripts
./deploy-benchmark.sh --benchmark tau2 --use-mcp-gateway
./deploy-agent.sh --benchmark tau2 --agent tool_calling --use-mcp-gateway
# All-in-one
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling --use-mcp-gatewayYou can also set the flag in your .env file so it applies by default:
USE_MCP_GATEWAY=trueThe MCP Gateway exposes tools under a namespace prefix (default exgentic_<benchmark_name>). The runner reads EXGENTIC_MCP_TOOL_PREFIX and prepends it to every MCP tool call. When using the gateway via evaluate-benchmark.sh, this variable is set automatically.
The deploy scripts can attach an AuthBridge sidecar to each
deployed agent. AuthBridge is a forward proxy whose behavior is
defined by a composable pipeline of named plugins β jwt-validation,
token-exchange, token-broker, a2a-parser, mcp-parser,
inference-parser, ibac β each independently togglable and each
running under an on_error policy of enforce, observe, or off.
This replaces the older opaque --authbridge / --ibac toggles. The
full design lives in AUTHBRIDGE_PIPELINE_SPEC.md.
- The sidecar is opt-in per deploy. With no plugin selectors, the
operator does not inject a sidecar at all and the agent runs as
before. Pass any selector (
--plugin-preset,--plugin,--no-plugin,--plugin-config-file) and the sidecar is injected with the resolved pipeline. - The pipeline mediates every request the agent sends and receives. Inbound plugins run on traffic to the agent (auth validation, A2A parsing); outbound plugins run on traffic the agent makes to tools and LLMs (token exchange, MCP/inference parsing, IBAC judging). Adding a plugin adds a hop on that path β measure latency accordingly.
- Plugins share a
Sessionobject. Inbound plugins populate fields (e.g.a2a-parserextracts the user's intent intoSession.Intents) that outbound plugins read (e.g.ibaccompares each outbound action against that intent). Hard runtime dependencies are encoded in the framework β IBAC fails closed ifa2a-parserdidn't run. - The operator's base config enables every plugin by default. To
disable a plugin, the deploy overlay must explicitly emit
on_error: offfor it. The script handles this β any plugin not in your resolved selection is turned off in the overlay, not omitted. - Selector resolution is last-write-wins. A preset seeds the set;
subsequent
--plugin/--no-pluginflags apply in order. This makes "preset minus one plugin" or "preset, but canary IBAC" easy to express on the command line. - Configuration is delivered via overlay, hot-reloaded. No operator changes are required to flip the pipeline shape β the script writes a merged ConfigMap and AuthBridge picks it up.
These flags are accepted by deploy-agent.sh and forwarded by
deploy-and-evaluate.sh:
| Flag | Description |
|---|---|
--plugin-preset NAME |
Named bundle. Available: auth-only, ibac-only, full. |
--plugin NAME[:POLICY] |
Enable plugin with policy β {enforce(default), observe, off}. Repeatable. |
--no-plugin NAME |
Shorthand for --plugin NAME:off. Repeatable. |
--plugin-config-file PATH |
Flat-map YAML overlay merged after selectors. |
| Preset | Inbound | Outbound |
|---|---|---|
auth-only |
jwt-validation |
token-exchange |
ibac-only |
a2a-parser |
inference-parser, mcp-parser, ibac |
full |
a2a-parser, jwt-validation |
token-exchange, inference-parser, mcp-parser, ibac |
IBAC (Intent-Based Access Control) is an outbound plugin that compares
each agent action against the user's most-recent declared intent and
asks an LLM judge to deny requests that don't align β catching
prompt-injection-driven exfiltration that traditional auth gates miss.
See ibac-benchmarking.md for the full
benchmarking profile.
Prerequisites:
-
An OpenAI-compatible chat-completion endpoint for the judge (ollama, OpenAI, vLLM, Azure, etc.).
-
The cluster's AuthBridge sidecar image must include the
ibacplugin. IBAC landed inkagenti-extensionson 2026-05-17 (PR #421); use sidecar imagev0.6.0-alpha.7or newer.Caveat β not in the latest stable Kagenti release. As of this writing, the IBAC plugin and the additive plugin-pipeline merge behavior the deploy scripts depend on are only available in
v0.6.0-alpha.7, which has not yet been published in a stable Kagenti release. Installing from the officialv0.6.0chart release pulls an older alpha that will fail with errors likejwt-validation config: issuer is requiredduring pipeline apply. Until a release containing alpha.7 is cut, install Kagenti frommain:git clone git@github.com:kagenti/kagenti.git cd kagenti # use main, not a release tag env CONTAINER_ENGINE=podman scripts/kind/setup-kagenti.sh --with-all --preload-images
To verify the sidecar image actually deployed:
kubectl -n kagenti-system get cm kagenti-platform-config \ -o jsonpath='{.data.authbridge}'Expect
ghcr.io/kagenti/kagenti-extensions/authbridge:v0.6.0-alpha.7or newer.
Configure the judge in your .env (consumed by the IBAC plugin
fragment via envsubst when ibac is in the active set):
# Judge LLM base URL β OpenAI-compatible (POST /v1/chat/completions)
IBAC_JUDGE_ENDPOINT=http://host.docker.internal:11434
# Judge model id served by that endpoint
IBAC_JUDGE_MODEL=llama3.2:3b
# Per-judge-call timeout in milliseconds
IBAC_TIMEOUT_MS=15000
# Hostname of the agent's own LLM endpoint (auto-derived from
# OPENAI_API_BASE if unset). Added to the IBAC bypass list so the
# agent's own reasoning calls aren't recursively judged.
# IBAC_AGENT_LLM_HOST=host.docker.internalDeploy with IBAC enforcing -- UNTESTED --(full pipeline β auth + parsers + IBAC):
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling \
--plugin-preset fullIBAC without inbound auth β for environments where an upstream gateway already terminates auth but you still want intent-based blocking on outbound calls:
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling \
--plugin-preset ibac-onlyWhen IBAC blocks a request, the agent sees a 403 ibac.blocked from
the proxy and the ibac.evaluate span on that outbound call carries
verdict=deny, reason=blocked, and the (truncated) intent and
action description. See ibac-benchmarking.md for the full set of
metrics IBAC emits.
# Auth + token exchange only (no IBAC, no parsers).
./deploy-agent.sh --benchmark tau2 --agent tool_calling \
--plugin-preset auth-only
# Token-broker instead of token-exchange.
./deploy-agent.sh --benchmark tau2 --agent tool_calling \
--plugin-preset full \
--no-plugin token-exchange \
--plugin token-broker
# Custom set without a preset.
./deploy-agent.sh --benchmark tau2 --agent tool_calling \
--plugin jwt-validation --plugin token-exchange --plugin ibac
# No AuthBridge sidecar at all (omit all plugin flags).
./deploy-agent.sh --benchmark tau2 --agent tool_callingA flat YAML map keyed by plugin name; values are deep-merged into each
plugin's config: block on top of the fragment defaults:
ibac:
judge_model: "llama3.2:8b"
timeout_ms: 30000
token-exchange:
default_policy: exchange
jwt-validation:
bypass_paths:
- /healthz
- /metricsUnknown plugin names in the file are ignored with a WARN to stderr.
Every plugin you select must be compiled into the running sidecar
binary. The merge validates the YAML shape, but the sidecar will fail
at Configure with unknown plugin "<name>" after reload if a plugin
isn't registered:
reloader: reload failed error="build: outbound: unknown plugin \"<name>\""
The image tag is pinned in kagenti/charts/kagenti/values.yaml. To
verify what's running:
kubectl -n team1 get pod -l app.kubernetes.io/name=<agent-name> \
-o jsonpath='{range .items[0].spec.containers[?(@.name=="authbridge")]}{.image}{"\n"}{end}'Compatibility note. Newer chart versions tie sidecar image versions to operator versions (per-plugin config support, jwt-validation field shape). When bumping the sidecar image past
v0.5.0-rc.3, confirm your kagenti-operator is recent enough β older operators may emit ConfigMaps the newer sidecar can't parse.
unknown plugin "<name>"at reload: the sidecar binary doesn't have that plugin compiled in. Bump the image tag.- Mutex error:
token-exchangeandtoken-broker: both claimClaimAuthorizationHeader; the script rejects this before any kubectl call. Disable one with--no-plugin. Reads ... no earlier plugin writes it: parser ordering issue; shouldn't happen with the canonical-position table, but possible if a malformed--plugin-config-fileintroduces an unknown plugin. Seeframework-architecture.mdΒ§6 for the underlying rules.ibac.no_intentin IBAC telemetry: the inbound chain is misconfigured βa2a-parserdidn't run, soSession.Intentsis empty and IBAC fails closed. Confirma2a-parseris in the active inbound set (it is for theibac-onlyandfullpresets).- Unknown plugin / preset name: script-side error; valid names are listed in the spec Β§3.1, valid presets in Β§3.2.
Required: Create and configure your environment file:
cp example.env .envThen edit the .env file as needed.
| Environment Variable | Default | Description |
|---|---|---|
MAX_TASKS |
(none) |
Maximum number of sessions to process. Useful for testing with a subset. |
MAX_PARALLEL_SESSIONS |
1 |
Number of sessions to run concurrently. Set higher for parallel execution. |
ABORT_ON_FAILURE |
false |
Stop processing after the first failed session. |
| Environment Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO |
Log level for the runner. Set to DEBUG for verbose logging with detailed debug information. Options: DEBUG, INFO, WARNING, ERROR. |
LOG_PROMPT |
0 |
Log prompt payloads for debugging (1 to enable). |
LOG_RESPONSE |
0 |
Log response payloads for debugging (1 to enable). |
| Environment Variable | Default | Description |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
(none) |
OTLP collector endpoint (for this runner, use gRPC such as http://localhost:4317). If not set, no traces are exported. |
OTEL_SERVICE_NAME |
exgentic-a2a-runner |
Service name in traces. |
OTEL_RESOURCE_ATTRIBUTES |
(none) |
Additional resource attributes (format: key1=val1,key2=val2). |
OTEL_INSTRUMENT_REQUESTS |
true |
Auto-instrument HTTP requests. |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc |
OTLP protocol. The current exporter implementation in OTELInstrumentation._initialize_tracing() and OTELInstrumentation._initialize_metrics() uses OTLP gRPC. |
OTEL_EXPORTER_OTLP_INSECURE |
true |
Use insecure OTLP connection. |
| Environment Variable | Default | Description |
|---|---|---|
USE_MCP_GATEWAY |
false |
Route MCP traffic through the MCP Gateway instead of connecting directly to the MCP server. |
EXGENTIC_MCP_TOOL_PREFIX |
(empty) |
Prefix prepended to MCP tool names. Set to match the gateway's MCPServerRegistration.spec.toolPrefix (e.g. exgentic_). |
| Environment Variable | Default | Description |
|---|---|---|
EXGENTIC_MCP_TIMEOUT_SECONDS |
60 |
Timeout for MCP operations. |
A2A_TIMEOUT_SECONDS |
300 |
Timeout for A2A requests. |
A2A_AUTH_TOKEN |
(none) |
Bearer token for A2A authentication. |
A2A_VERIFY_TLS |
true |
Verify TLS certificates for HTTPS. |
A2A_ENDPOINT_PATH |
/ |
Endpoint path for A2A requests. |
The deploy-and-evaluate.sh script provides a convenient way to deploy both the benchmark and agent, then run the evaluation in a single command:
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_callingThis script will:
- Deploy the benchmark MCP server
- Deploy the agent
- Run the evaluation
MLflow tracing via the OTEL Collector is enabled by default. Pass --disable-mlflow to skip it.
Options:
# Basic usage with defaults (MLflow tracing enabled)
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling
# Route MCP traffic through the MCP Gateway
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling --use-mcp-gateway
# With custom model
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling --model Azure/gpt-4o-mini
# With custom Keycloak credentials
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling --model Azure/gpt-4o-mini --keycloak-user admin --keycloak-pass admin
# Dry run mode - print commands without executing them
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling --dry
# Show help
./deploy-and-evaluate.sh --helpThe --dry flag enables dry-run mode, which prints all commands that would be executed without actually running them. This is useful for:
- Verifying command syntax before execution
- Debugging deployment issues
- Understanding what the script will do
- Documentation and training purposes
# See what commands would be executed
./deploy-and-evaluate.sh --benchmark tau2 --agent tool_calling --dry
# Dry run with all options
./deploy-and-evaluate.sh --benchmark gsm8k --agent tool_calling \
--model Azure/gpt-4o-mini --mlflow --use-mcp-gateway --dryExample output:
========================================
Deploy and Evaluate Exgentic Benchmark
========================================
Benchmark: tau2
Agent: tool_calling
Model: Azure/gpt-4.1
Keycloak User: admin
MLflow tracing: false
MCP Gateway: false
Dry run: true
========================================
Step 1/3: Deploying Benchmark
========================================
[DRY RUN] Would execute:
./deploy-benchmark.sh --benchmark "tau2" --model "Azure/gpt-4.1" --keycloak-user "admin" --keycloak-pass "admin"
========================================
Step 2/3: Deploying Agent
========================================
[DRY RUN] Would execute:
./deploy-agent.sh --benchmark "tau2" --agent "tool_calling" --model "Azure/gpt-4.1" --keycloak-user "admin" --keycloak-pass "admin"
========================================
Step 3/3: Running Evaluation
========================================
[DRY RUN] Would execute:
./evaluate-benchmark.sh --benchmark "tau2" --agent "tool_calling" --experiment "default"
========================================
β Dry run completed - no commands executed
========================================
The evaluate-benchmark.sh script automatically:
- Uses HTTP routes to reach services (no port-forwarding for MCP/agent)
- Port-forwards the OTEL Collector (traces β MLflow) on dev laptops β skipped in-cluster
- Waits for services to be ready via HTTP health checks
- Tests connectivity to services
- Runs the benchmark evaluation
- Propagates the current OpenTelemetry trace context into outbound A2A HTTP requests so the agent can continue the same distributed trace when it supports W3C trace headers
- Cleans up port forwards on exit
./evaluate-benchmark.sh --benchmark tau2 --agent tool_calling
./evaluate-benchmark.sh --benchmark gsm8k --agent tool_calling At the end of each run, a summary is printed:
============================================================
RUN SUMMARY
============================================================
Sessions Attempted: 100
Sessions Succeeded: 95
Sessions Failed: 5
Evaluation Success: 92.6%
Total Wall Time: 1234.56s
Average Latency: 12345.67ms
P50 Latency: 10000.00ms
P95 Latency: 20000.00ms
============================================================
The runner emits comprehensive telemetry:
Each session creates a span (exgentic_a2a.session) with:
Attributes:
exgentic.session_id: Session identifierexgentic.mcp_server_url: MCP server URLexgentic.evaluation_result: Whether evaluation was successfula2a.base_url: A2A endpoint URLa2a.timeout_seconds: Timeout valueprompt.chars: Prompt size in charactersresponse.chars: Response size in characterssession.status:successorfaileda2a.duration_ms: End-to-end A2A operation latency in milliseconds
Child spans:
exgentic_a2a.prompt.build: Prompt constructionexgentic_a2a.a2a.send_prompt: End-to-end A2Asend_promptcallexgentic_a2a.mcp.evaluate_session: Session evaluationexgentic_a2a.mcp.close_session: Session cleanup
Auto-instrumented HTTP spans:
- Outbound
requestsspans for agent-card discovery,message/send, andtasks/getcalls
Events:
prompt_built: When prompt is constructedsession_failed: When session fails (includes error details)
Counters:
exgentic_a2a_sessions_total{status=success|failed}: Total sessions processedexgentic_a2a_errors_total{error_type=...}: Total errors by type
Histograms:
exgentic_a2a_session_latency_ms: End-to-end session latencyexgentic_a2a_evaluation_latency_ms: Evaluation operation latencyexgentic_a2a_session_creation_latency_ms: Session creation latencyexgentic_a2a_a2a_latency_ms: A2A request latencyexgentic_a2a_prompt_size_chars: Prompt size distributionexgentic_a2a_response_size_chars: Response size distribution
Gauge:
exgentic_a2a_inflight_sessions: Current sessions in flight (0 or 1)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β For Each Session β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Create Session β
β ββ> MCP: create_session() β (session_id, task) β
β β
β 2. Build Prompt β
β ββ> Include session_id in instructions β
β β
β 3. Invoke Agent β
β ββ> A2A: send_prompt(prompt) β response β
β β
β 4. Evaluate Session β
β ββ> MCP: evaluate_session(session_id) β success β
β β
β 5. Close Session β
β ββ> MCP: close_session(session_id) β
β β
β 6. Record Statistics β
β ββ> Track time, success, evaluation result β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The Kagenti cluster exposes an MLflow service in the kagenti-system namespace. An OTEL Collector forwards traces to MLflow's /v1/traces endpoint with OAuth2 authentication.
MLflow tracing is enabled by default. The script automatically port-forwards the OTEL Collector on a developer laptop and configures the required environment variables. To disable it, pass --disable-mlflow:
# Default β MLflow tracing enabled
env MAX_TASKS=1 MAX_PARALLEL_SESSIONS=1 ./evaluate-benchmark.sh --benchmark gsm8k --agent tool_calling
# Disable MLflow tracing
env MAX_TASKS=1 MAX_PARALLEL_SESSIONS=1 ./evaluate-benchmark.sh --benchmark gsm8k --agent tool_calling --disable-mlflowOpen http://mlflow.localtest.me:8080 in your browser to view traces and experiments.
The analyze-run.sh script provides comprehensive trace analysis by downloading Agent.Session traces from MLflow and generating detailed performance reports.
- Automatic MLflow connectivity: Connects to MLflow REST API with OAuth2 authentication and optional auto port-forwarding
- Trace filtering: Downloads Agent.Session root spans and all child spans
- Experiment filtering: Filter or compare traces by experiment name
- Performance metrics: Calculates timing statistics (avg, p50, p95, min, max) for:
- Session creation time
- Agent call time (end-to-end agent execution)
- Evaluation time
- LLM call time and token usage
- Tool call time
- Grouping: Groups traces by agent, benchmark, model, and parallel session count
- Detailed reports: Generates both summary statistics and individual trace details
# Basic usage (assumes MLflow is accessible at http://mlflow.localtest.me:8080)
./analyze-run.sh
# With custom MLflow URL and limit
./analyze-run.sh --url http://mlflow.localtest.me:8080 --limit 200
# Auto port-forward from kind cluster if MLflow is not accessible locally
./analyze-run.sh --forward --limit 50
# Filter by experiment name
./analyze-run.sh --experiment baseline
# Compare two experiments
./analyze-run.sh --compare baseline,test1| Option | Description | Default |
|---|---|---|
-u, --url URL |
MLflow REST API base URL | http://mlflow.localtest.me:8080 |
-l, --limit NUM |
Maximum number of traces to download | 100 |
-e, --experiment NAME |
Filter traces by experiment name attribute | (none) |
-c, --compare EXP1,EXP2 |
Compare two experiments (comma-separated) | (none) |
--experiment-id ID |
MLflow experiment ID to query | 0 |
-f, --forward |
Auto port-forward MLflow from kind cluster if not accessible | false |
-h, --help |
Show help message | - |
- Connectivity Test: Attempts to connect to MLflow REST API health endpoint
- Auto Port-Forward (if
--forwardis used): Sets up port-forwarding from kind cluster if MLflow is not accessible - OAuth2 Authentication: Obtains a bearer token from the cluster's
mlflow-oauth-secret - Trace Download: Queries MLflow's trace API for the specified experiment, with pagination
- Format Transformation: Converts MLflow trace format to the analysis input format via
download_mlflow_traces.py - Analysis: Pipes trace data to
analyze_traces.pyfor detailed analysis
The script generates two main sections:
1. Summary Statistics by Configuration
Groups traces by (agent, benchmark, model, parallel sessions) and shows:
- Count of traces
- Average, P50, P95, Min, Max for:
- Session creation time
- Agent call time
- Evaluation time
- LLM call time (with token counts)
- Tool call time
- Time distribution percentages (LLM%, Tool%, Other%)
2. Individual Trace Details
Lists each trace with:
- Trace ID
- Agent, Benchmark, Model, Parallel sessions
- Session creation time
- Agent call time (with LLM% and Tool% breakdown)
- Evaluation time
- LLM tokens (input/output)
- Tool call count and time
=== MLflow Trace Analysis ===
MLflow URL: http://mlflow.localtest.me:8080
Experiment ID: 0
Limit: 100
β Connected to MLflow
β OAuth token obtained
Found 45 Agent.Session traces
Downloading traces...
Downloaded 45 traces
=== Trace Analysis Report ===
Summary Statistics by Configuration:
βββββββββββββββ¬ββββββββββββ¬ββββββββββ¬βββββββββββ¬ββββββββ¬ββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ
β Agent β Benchmark β Model β Parallel β Count β Avg Create β Avg Agent β Avg Eval β Avg LLM β Avg Tool β
β β β β β β (ms) β Call (ms) β (ms) β (ms) β (ms) β
βββββββββββββββΌββββββββββββΌββββββββββΌβββββββββββΌββββββββΌββββββββββββββΌββββββββββββββΌββββββββββββββΌββββββββββββββΌββββββββββββββ€
β tool-callingβ gsm8k β gpt-4o β 1 β 45 β 125.3 β 8234.5 β 45.2 β 6543.2 β 1234.5 β
β β β β β β β β β (79.5%) β (15.0%) β
βββββββββββββββ΄ββββββββββββ΄ββββββββββ΄βββββββββββ΄ββββββββ΄ββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ
- jq: JSON processor for parsing API responses
# macOS brew install jq # Ubuntu/Debian apt-get install jq
- Python 3: For running the download and analysis scripts
- MLflow: Running and accessible (either locally or in kind cluster)
- kubectl: For port-forwarding and OAuth token retrieval
Connection refused:
- Ensure MLflow is running:
kubectl get pods -n kagenti-system -l app=mlflow - Use
--forwardflag to auto port-forward from kind cluster - Manually port-forward:
kubectl port-forward -n kagenti-system svc/mlflow 8080:5000
No traces found:
- Verify traces exist in MLflow UI: http://mlflow.localtest.me:8080
- Check that Agent.Session spans are being created by the runner
- MLflow tracing is enabled by default; pass
--disable-mlflowonly if you want to skip it
OAuth errors:
- Ensure the
mlflow-oauth-secretexists in thekagenti-systemnamespace - Verify the MLflow pod is running (token acquisition executes inside the pod)
When OTEL is enabled, you'll see:
- Session spans: Complete session lifecycle with timing
- MCP operations: create_session, evaluate_session, close_session
- A2A requests: Agent invocations with request/response sizes
- HTTP calls: Auto-instrumented outbound requests
- Errors: Failed operations with exception details
The runner can execute entirely inside the cluster as a Kubernetes Job β no local machine needed after the image is built. This is the recommended path for CI and automated evaluation runs.
k8s/job.yaml is the launch template. It references secrets for credentials and passes benchmark/agent flags as container args. The job container uses cluster-internal DNS to reach the Kagenti API, Keycloak, MCP server, and agent β no port-forwarding is required. MLflow tracing switches automatically to HTTP/protobuf when KUBERNETES_SERVICE_HOST is set.
cd exgentic_a2a_runner
docker build -t ghcr.io/exgentic/runner:latest .
docker push ghcr.io/exgentic/runner:latestReplace ghcr.io/exgentic/runner:latest with your own registry path if needed, and update k8s/job.yaml β image: to match.
Run this from your local machine (not inside the cluster) before submitting the job. update-secrets.sh creates/patches the API-key secrets via kubectl:
export OPENAI_API_KEY=sk-...
export HF_TOKEN=hf_... # optional; skip if not needed
./update-secrets.sh --namespace team1The kagenti-test-user secret (key: password) must already exist in team1 β it is created by the Kagenti cluster setup and holds the Keycloak password.
Note: The job container itself does not have kubectl RBAC, so
update-secrets.shwill print harmless warnings if it tries to run inside the cluster. The secrets just need to be present before the job starts.
Edit k8s/job.yaml to set:
| Field | Where | Example |
|---|---|---|
| Benchmark | args: ["--benchmark", "β¦"] |
gsm8k, tau2, appworld |
| Agent | args: ["--agent", "β¦"] |
tool_calling |
| Model | args: ["--model", "β¦"] |
openai/Azure/gpt-4.1 |
| LLM API base | env: OPENAI_API_BASE |
your LiteLLM proxy URL |
To disable MLflow tracing, add "--disable-mlflow" to args.
# Delete any previous run with the same name first
kubectl delete job exgentic-runner -n team1 --ignore-not-found
# Submit
kubectl apply -f k8s/job.yaml
# Stream logs (the runner prints a summary table at the end)
kubectl logs -f job/exgentic-runner -n team1To watch job status separately:
kubectl get job exgentic-runner -n team1 -wConsole output β the log stream ends with a run summary:
============================================================
RUN SUMMARY
============================================================
Max Parallel Sessions: 1
Sessions Attempted: 10
Sessions Succeeded: 9
Sessions With Error: 1
Evaluation Success: 90.0%
Total Wall Time: 312.4s
TIMING BREAKDOWN (average per session)
Session Creation: 0.05s
Agent Processing: 31.2s
Evaluation: 0.02s
AGENT PROCESSING LATENCY
Average: 31.20s
P50: 28.00s
P95: 58.00s
============================================================
MLflow UI β if MLflow tracing was enabled (the default), open http://mlflow.localtest.me:8080 to view traces grouped by experiment.
To test a local image change without pushing to a registry, sync it into the kind cluster first:
export REMOTE_IMAGE_NAME=ghcr.io/exgentic/runner:dev
export KIND_CLUSTER_NAME=kagenti
source ./sync-image-to-cluster.shThen set imagePullPolicy: IfNotPresent in k8s/job.yaml and submit as above.
e2e-test.sh runs deploy-and-evaluate.sh for every benchmark (or a chosen subset) and prints a consolidated results table.
# Run all three benchmarks sequentially (gsm8k β tau2 β appworld), 1 task each
./e2e-test.sh --agent tool_calling
# Run only a subset
./e2e-test.sh --agent tool_calling --benchmarks gsm8k,tau2
# Run with more tasks per benchmark
./e2e-test.sh --agent tool_calling --tasks 10
# Run all benchmarks in parallel (each benchmark gets its own port-forward slots)
./e2e-test.sh --agent tool_calling --parallel-jobs
# Run all benchmarks as Kubernetes Jobs (in-cluster mode)
./e2e-test.sh --agent tool_calling --in-cluster
# Dry run β prints all commands without executing them
./e2e-test.sh --agent tool_calling --dryAny flag not recognised by e2e-test.sh is forwarded verbatim to deploy-and-evaluate.sh (e.g. --model, --experiment, --disable-mlflow, --plugin-preset).
After all benchmarks finish, the script prints and writes e2e-results.md:
| Benchmark | Status | Tasks | Parallel Sessions | Eval Success Rate | Avg Latency (s) | Failures |
|-----------|--------|-------|-------------------|-------------------|-----------------|----------|
| gsm8k | PASS | 1 | -- | 100.0% | 4.2s | 0 |
| tau2 | PASS | 1 | -- | 100.0% | 12.8s | 0 |
| appworld | PASS | 1 | -- | 100.0% | 9.1s | 0 |
- Status is
PASS,FAIL, orSKIP(skipped benchmarks appear when a sequential run aborts early). When a step fails, the status includes the step name, e.g.FAIL(deploy-benchmark). - Parallel jobs mode (
--parallel-jobs): all benchmarks run concurrently; each gets unique OTEL-collector and Prometheus ports so local port-forwards don't collide. - In-cluster mode (
--in-cluster): one Kubernetes Job is created per benchmark fromk8s/job.yaml;e2e-test.shstreams the logs and checks the job's success status.
- No retry mechanism for failed operations
- No streaming response support
- Tested only with local kind Kagenti installation with Podman (not tested with Docker)
uv run pytestuv run black exgentic_a2a_runner/uv run mypy exgentic_a2a_runner/Contributions are welcome! Please ensure:
- Code follows the existing style
- Tests pass
- Documentation is updated
- Commit messages are clear
See LICENSE file in the repository root.
- Kagenti UI: Access at http://kagenti-ui.localtest.me:8080/ to monitor deployments
- GitHub Issues: https://github.com/kagenti/workload-harness/issues
- Related Issue: kagenti/kagenti#963
After successful test run:
- Increase
MAX_TASKSin.envfor longer runs - Adjust
MAX_PARALLEL_SESSIONSfor different concurrency levels - Enable OTLP exporter for telemetry collection
- Deploy different benchmarks (gsm8k, tau2, appworld)
- Test with various models via configure script
- Analyze results and agent performance in Kagenti UI