426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
315 lines
11 KiB
Python
315 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Run framework benchmark scenarios through real benchmark harness clients.
|
|
|
|
The TypeScript framework benchmark measures local elizaOS runtime overhead with
|
|
mock LLM handlers. This runner is the cross-harness counterpart: it exercises
|
|
the real Eliza, Hermes, or OpenClaw client surface on the same framework
|
|
scenario fixtures and writes a framework-results.json compatible summary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import platform
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from statistics import mean, median, pstdev
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
BENCH_DIR = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "eliza-adapter"))
|
|
sys.path.insert(0, str(ROOT / "hermes-adapter"))
|
|
sys.path.insert(0, str(ROOT / "openclaw-adapter"))
|
|
|
|
|
|
SYSTEM_PROMPT = "\n".join(
|
|
[
|
|
"You are BenchmarkAgent, a concise assistant in a framework benchmark.",
|
|
"Reply to the user's benchmark message in one short sentence.",
|
|
"Do not call external tools unless the benchmark context explicitly provides them.",
|
|
]
|
|
)
|
|
|
|
|
|
def _load_scenarios(selected: set[str]) -> list[dict[str, Any]]:
|
|
data = json.loads((BENCH_DIR / "shared" / "scenarios.json").read_text(encoding="utf-8"))
|
|
scenarios = [item for item in data.get("scenarios", []) if isinstance(item, dict)]
|
|
return [item for item in scenarios if str(item.get("id")) in selected]
|
|
|
|
|
|
def _messages(raw: object, *, generated_limit: int) -> list[dict[str, str]]:
|
|
if isinstance(raw, str) and raw.startswith("_generate:"):
|
|
count = min(generated_limit, max(1, int(raw.split(":", 1)[1])))
|
|
return [
|
|
{
|
|
"role": "user",
|
|
"content": f"BenchmarkAgent, benchmark message number {index + 1}.",
|
|
}
|
|
for index in range(count)
|
|
]
|
|
if not isinstance(raw, list):
|
|
return []
|
|
out: list[dict[str, str]] = []
|
|
for item in raw:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
content = item.get("content")
|
|
if isinstance(content, str) and content.strip():
|
|
out.append({"role": str(item.get("role") or "user"), "content": content})
|
|
return out
|
|
|
|
|
|
def _build_client(harness: str, provider: str, model: str):
|
|
timeout_s = float(os.environ.get("FRAMEWORK_HARNESS_TIMEOUT_S", "180"))
|
|
if harness == "hermes":
|
|
from hermes_adapter.client import HermesClient
|
|
|
|
return HermesClient(provider=provider, model=model, timeout_s=timeout_s), None
|
|
if harness == "openclaw":
|
|
from openclaw_adapter.client import OpenClawClient
|
|
|
|
return (
|
|
OpenClawClient(
|
|
provider=provider,
|
|
model=model,
|
|
timeout_s=timeout_s,
|
|
direct_openai_compatible=True,
|
|
reasoning_effort=os.environ.get("FRAMEWORK_OPENCLAW_THINKING", "low"),
|
|
),
|
|
None,
|
|
)
|
|
if harness == "eliza":
|
|
from eliza_adapter import ElizaClient, ElizaServerManager
|
|
|
|
if os.environ.get("ELIZA_BENCH_URL") and os.environ.get("ELIZA_BENCH_TOKEN"):
|
|
return (
|
|
ElizaClient(
|
|
os.environ["ELIZA_BENCH_URL"],
|
|
token=os.environ.get("ELIZA_BENCH_TOKEN"),
|
|
),
|
|
None,
|
|
)
|
|
manager = ElizaServerManager()
|
|
manager.start()
|
|
return manager.client, manager
|
|
raise ValueError(f"unsupported harness: {harness}")
|
|
|
|
|
|
def _latency_stats(values: list[float]) -> dict[str, Any]:
|
|
if not values:
|
|
return {
|
|
"min_ms": 0,
|
|
"max_ms": 0,
|
|
"avg_ms": 0,
|
|
"median_ms": 0,
|
|
"p95_ms": 0,
|
|
"p99_ms": 0,
|
|
"stddev_ms": 0,
|
|
"raw_ms": [],
|
|
}
|
|
sorted_values = sorted(values)
|
|
|
|
def percentile(p: float) -> float:
|
|
index = min(len(sorted_values) - 1, int(round((len(sorted_values) - 1) * p)))
|
|
return sorted_values[index]
|
|
|
|
return {
|
|
"min_ms": sorted_values[0],
|
|
"max_ms": sorted_values[-1],
|
|
"avg_ms": mean(sorted_values),
|
|
"median_ms": median(sorted_values),
|
|
"p95_ms": percentile(0.95),
|
|
"p99_ms": percentile(0.99),
|
|
"stddev_ms": pstdev(sorted_values) if len(sorted_values) > 1 else 0,
|
|
"raw_ms": sorted_values,
|
|
}
|
|
|
|
|
|
def _empty_pipeline(total_ms: float) -> dict[str, float]:
|
|
return {
|
|
"compose_state_avg_ms": 0,
|
|
"provider_execution_avg_ms": 0,
|
|
"should_respond_avg_ms": 0,
|
|
"model_call_avg_ms": total_ms,
|
|
"action_dispatch_avg_ms": 0,
|
|
"evaluator_avg_ms": 0,
|
|
"memory_create_avg_ms": 0,
|
|
"memory_get_avg_ms": 0,
|
|
"model_time_total_ms": total_ms,
|
|
"framework_time_total_ms": 0,
|
|
}
|
|
|
|
|
|
def _resources() -> dict[str, float]:
|
|
return {
|
|
"memory_rss_start_mb": 0,
|
|
"memory_rss_peak_mb": 0,
|
|
"memory_rss_end_mb": 0,
|
|
"memory_delta_mb": 0,
|
|
"heap_used_start_mb": 0,
|
|
"heap_used_peak_mb": 0,
|
|
"heap_used_end_mb": 0,
|
|
}
|
|
|
|
|
|
def _send(client: Any, scenario_id: str, text: str, model: str) -> tuple[bool, float, str]:
|
|
context = {
|
|
"benchmark": "framework",
|
|
"task_id": scenario_id,
|
|
"system_prompt": SYSTEM_PROMPT,
|
|
"messages": [
|
|
{"role": "system", "content": SYSTEM_PROMPT},
|
|
{"role": "user", "content": text},
|
|
],
|
|
"temperature": 0.0,
|
|
"max_tokens": int(os.environ.get("FRAMEWORK_HARNESS_MAX_TOKENS", "128")),
|
|
"model": model,
|
|
}
|
|
started = time.perf_counter()
|
|
response = client.send_message(text, context=context)
|
|
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
|
output = str(getattr(response, "text", "") or "").strip()
|
|
actions = getattr(response, "actions", [])
|
|
params = getattr(response, "params", {})
|
|
tool_calls = params.get("tool_calls") if isinstance(params, dict) else None
|
|
ok = bool(output or actions or tool_calls)
|
|
return ok, elapsed_ms, output
|
|
|
|
|
|
def _run_scenario(
|
|
client: Any,
|
|
scenario: dict[str, Any],
|
|
*,
|
|
model: str,
|
|
iterations: int,
|
|
generated_limit: int,
|
|
) -> tuple[dict[str, Any], int, int]:
|
|
scenario_id = str(scenario.get("id") or "scenario")
|
|
config = scenario.get("config") if isinstance(scenario.get("config"), dict) else {}
|
|
if config.get("startupOnly") is True:
|
|
latencies: list[float] = []
|
|
successes = 0
|
|
for _ in range(iterations):
|
|
started = time.perf_counter()
|
|
if hasattr(client, "reset"):
|
|
client.reset(f"framework-{scenario_id}", "framework")
|
|
latencies.append((time.perf_counter() - started) * 1000.0)
|
|
successes += 1
|
|
total_time = sum(latencies)
|
|
return _scenario_result(iterations, 0, latencies, iterations, total_time, successes, iterations), successes, iterations
|
|
|
|
msgs = _messages(scenario.get("messages"), generated_limit=generated_limit)
|
|
latencies: list[float] = []
|
|
successes = 0
|
|
total = 0
|
|
for iteration in range(iterations):
|
|
if hasattr(client, "reset"):
|
|
client.reset(f"framework-{scenario_id}-{iteration}", "framework")
|
|
for message in msgs:
|
|
total += 1
|
|
ok, elapsed_ms, _output = _send(client, scenario_id, message["content"], model)
|
|
latencies.append(elapsed_ms)
|
|
if ok:
|
|
successes += 1
|
|
total_time = sum(latencies)
|
|
return _scenario_result(iterations, 0, latencies, total, total_time, successes, total), successes, total
|
|
|
|
|
|
def _scenario_result(
|
|
iterations: int,
|
|
warmup: int,
|
|
latencies: list[float],
|
|
total_messages: int,
|
|
total_time_ms: float,
|
|
successes: int,
|
|
total_checks: int,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"iterations": iterations,
|
|
"warmup": warmup,
|
|
"latency": _latency_stats(latencies),
|
|
"throughput": {
|
|
"messages_per_second": (total_messages / total_time_ms) * 1000.0
|
|
if total_time_ms > 0
|
|
else 0,
|
|
"total_messages": total_messages,
|
|
"total_time_ms": total_time_ms,
|
|
},
|
|
"pipeline": _empty_pipeline(total_time_ms),
|
|
"resources": _resources(),
|
|
"success_rate": successes / total_checks if total_checks else 1.0,
|
|
"successful_messages": successes,
|
|
"total_checks": total_checks,
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--harness", choices=["eliza", "hermes", "openclaw"], required=True)
|
|
parser.add_argument("--provider", default=os.environ.get("BENCHMARK_MODEL_PROVIDER", "cerebras"))
|
|
parser.add_argument("--model", default=os.environ.get("BENCHMARK_MODEL_NAME", "gemma-4-31b"))
|
|
parser.add_argument("--scenarios", default="single-message")
|
|
parser.add_argument("--iterations", type=int, default=1)
|
|
parser.add_argument("--generated-limit", type=int, default=3)
|
|
parser.add_argument("--output", required=True)
|
|
args = parser.parse_args(argv)
|
|
|
|
selected = {item.strip() for item in args.scenarios.split(",") if item.strip()}
|
|
scenarios = _load_scenarios(selected)
|
|
if not scenarios:
|
|
raise SystemExit(f"no framework scenarios selected from: {sorted(selected)}")
|
|
|
|
client, manager = _build_client(args.harness, args.provider, args.model)
|
|
results: dict[str, Any] = {}
|
|
successes = 0
|
|
total = 0
|
|
try:
|
|
for scenario in scenarios:
|
|
result, scenario_successes, scenario_total = _run_scenario(
|
|
client,
|
|
scenario,
|
|
model=args.model,
|
|
iterations=max(1, args.iterations),
|
|
generated_limit=max(1, args.generated_limit),
|
|
)
|
|
results[str(scenario.get("id"))] = result
|
|
successes += scenario_successes
|
|
total += scenario_total
|
|
finally:
|
|
if manager is not None:
|
|
manager.stop()
|
|
|
|
report = {
|
|
"runtime": "framework-harness",
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"harness": args.harness,
|
|
"provider": args.provider,
|
|
"model": args.model,
|
|
"system": {
|
|
"os": platform.system().lower(),
|
|
"arch": platform.machine(),
|
|
"cpus": os.cpu_count() or 1,
|
|
"memory_gb": 0,
|
|
"runtime_version": platform.python_version(),
|
|
"platform": "python",
|
|
},
|
|
"scenarios": results,
|
|
"overall_score": successes / total if total else 1.0,
|
|
"score_basis": "real harness non-empty/action response rate",
|
|
}
|
|
out = Path(args.output)
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
print(str(out))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|