"""Compare OpenSquilla meta-skills against an OpenClaw gateway. The script defines seven fixed benchmark cases for the high-value meta-skill scenarios and can run them end-to-end through both gateways. """ from __future__ import annotations import argparse import asyncio import contextlib import json import os import re import textwrap import time import uuid from dataclasses import asdict, dataclass from datetime import UTC, datetime from pathlib import Path from typing import Any REPORT_DIR = Path( os.environ.get("OPENSQUILLA_COMPARE_REPORT_DIR", ".reports/meta-skill-comparison") ) JUDGE_SUBSCORE_RANGES: dict[str, tuple[int, int]] = { "final_artifact_quality": (0, 40), "task_completion": (0, 20), "evidence_traceability": (0, 15), "actionability": (0, 10), "risk_boundary_safety": (0, 10), "meta_skill_fit": (0, 5), } OPENCLAW_BASELINE_WARMUP = ( "本轮是任务评估会话。身份、称呼和初始化已经完成;后续请直接处理用户请求," "不要询问姓名、称呼、workspace/bootstrap/onboarding,也不要要求用户在 A/B " "路径中选择。" ) BENCHMARK_CONSTRAINTS = ( "Benchmark constraints: return the final deliverable inline in chat. " "Do not create, edit, or write local files. If you would normally create " "a PDF, DOCX, SKILL.md, patch, or other artifact, include artifact-ready " "content inline instead. You may name verification commands, but do not " "execute them.\n\n" ) @dataclass(frozen=True) class RubricCriterion: name: str description: str patterns: tuple[str, ...] weight: int = 1 @dataclass(frozen=True) class ComparisonCase: case_id: str skill_name: str prompt: str expected_advantage: str optimization_if_not_better: str scenario: str = "primary" rubric: tuple[RubricCriterion, ...] = () failure_modes: tuple[str, ...] = () @dataclass(frozen=True) class ResponseScore: total: int dimensions: dict[str, int] notes: list[str] @dataclass class EndpointResult: endpoint: str case_id: str ok: bool elapsed_s: float response_text: str score: dict[str, Any] error: str | None = None session_key: str | None = None model: str | None = None provider: str | None = None event_count: int = 0 @dataclass(frozen=True) class JudgeResult: winner: str scores: dict[str, int] confidence: float rationale: str risks: list[str] raw: dict[str, Any] model: str def criterion( name: str, description: str, *patterns: str, weight: int = 1, ) -> RubricCriterion: return RubricCriterion(name, description, tuple(patterns), weight) SKILL_RUBRICS: dict[str, tuple[RubricCriterion, ...]] = { "meta-paper-write": ( criterion( "paper_sections", "Includes canonical manuscript sections.", r"abstract", r"introduction", r"method", r"evaluation", ), criterion( "latex_ready", "Provides LaTeX or BibTeX-safe structure.", r"latex", r"\\begin", r"bib" ), criterion( "citation_integrity", "Avoids fabricated citations by marking placeholders.", r"placeholder", r"citation", r"reference", ), criterion( "length_plan", "Explains how the draft scales to a full paper.", r"page", r"expand", r"full version", ), criterion( "limitations", "Includes limitations and threats to validity.", r"limitation", r"threat" ), ), "meta-pdf-intelligence": ( criterion( "page_traceability", "Preserves page-level evidence.", r"page\s+\d+", r"p\.\s*\d+" ), criterion( "fact_digest", "Extracts facts rather than generic summary.", r"fact", r"key finding", r"digest", ), criterion( "open_questions", "Lists open questions or missing evidence.", r"open question", r"unknown", r"missing", ), criterion( "memory_index", "Builds a reusable memory/index structure.", r"memory index", r"index", r"tag", ), criterion( "no_hallucinated_pdf", "Acknowledges missing document limits.", r"provided excerpt", r"cannot verify", r"upload", ), ), "meta-stack-trace-investigator": ( criterion( "frame_parsing", "Identifies failing frame, exception, and data shape.", r"KeyError", r"frame", r"parse", ), criterion( "root_cause", "Provides ranked root-cause hypotheses.", r"root cause", r"hypothesis", r"likely", ), criterion("repo_search", "Gives concrete repo search targets.", r"rg ", r"grep", r"search"), criterion( "reproduction", "Gives a reproduction or focused check.", r"repro", r"fixture", r"minimal", ), criterion( "verification", "Gives exact verification commands.", r"pytest", r"command", r"verify" ), ), "meta-travel-planner": ( criterion( "constraint_capture", "Captures dates, party, pace, budget, and interests.", r"assumption", r"constraint", r"budget", ), criterion( "geo_grouping", "Groups activities by neighborhood/transit.", r"neighborhood", r"transit", r"route", ), criterion( "daily_schedule", "Produces day-by-day itinerary.", r"day\s+1", r"day\s+2", r"schedule" ), criterion( "weather_backup", "Includes rain or weather backup plan.", r"rain", r"weather", r"backup", ), criterion( "variants", "Includes variants or alternatives.", r"variant", r"alternative", r"swap" ), ), "meta-skill-creator": ( criterion("trigger_inputs", "Defines triggers and inputs.", r"trigger", r"input"), criterion( "step_graph", "Defines a workflow graph or ordered steps.", r"step", r"graph", r"workflow", ), criterion( "skill_preview", "Shows a SKILL.md-style preview.", r"SKILL\.md", r"```", r"name:" ), criterion( "collision_risk", "Checks collisions with existing skills.", r"collision", r"overlap", r"existing", ), criterion( "gates", "Defines lint, smoke, safety, or install gates.", r"gate", r"lint", r"smoke" ), ), "meta-migration-assistant": ( criterion( "migration_scope", "Identifies source and target migration states.", r"CommonJS", r"ESM", r"from", r"to", ), criterion( "breaking_changes", "Names breaking changes.", r"breaking", r"interop", r"compat" ), criterion( "grep_patterns", "Provides grep/search patterns.", r"rg ", r"grep", r"require\(", r"module\.exports", ), criterion( "validation_commands", "Provides validation commands.", r"test", r"build", r"command", r"verify", ), criterion( "rollout_risk", "Includes staged rollout risks.", r"rollout", r"risk", r"rollback" ), ), } COMPARISON_CASES: list[ComparisonCase] = [ ComparisonCase( case_id="paper_write", skill_name="meta-paper-write", prompt=( "I'm preparing to draft a paper on meta-skill orchestration for AI " "agents, but I only need the first pass today. Please produce an " "academic manuscript plan and a compact LaTeX-ready draft skeleton. " "Include abstract, introduction, method, evaluation design, expected " "results, limitations, and at least 20 reference placeholders. Also " "explain how the full version would reach 10+ pages." ), expected_advantage=( "OpenSquilla should preserve paper structure, citation planning, length " "gate, citation integrity gate, and LaTeX sanitization." ), optimization_if_not_better=( "Make the paper workflow expose a short benchmark mode while keeping " "the full 10-page gate for production paper requests." ), ), ComparisonCase( case_id="pdf_intelligence", skill_name="meta-pdf-intelligence", prompt=( "I don't have the PDF upload handy, but please treat this as a PDF " "intelligence task from `agent-observability.pdf`: page 3 says " "'Trace spans identify tool calls, model routing, and error recovery.' " "Page 4 says 'missing provenance makes evaluation unreliable.' Return " "a traceable digest with page evidence, key facts, open questions, " "and a reusable memory index." ), expected_advantage=( "OpenSquilla should classify the PDF task, preserve document/page " "references, synthesize traceably, and create a memory index." ), optimization_if_not_better=( "Add an inline-excerpt fallback path for PDF intelligence when the user " "provides page excerpts instead of a file upload." ), ), ComparisonCase( case_id="stack_trace_investigator", skill_name="meta-stack-trace-investigator", prompt=( "Can you investigate this stack trace from our agent runtime?\n" "Traceback (most recent call last):\n" ' File "src/agent/runtime.py", line 88, in run_step\n' " payload = parse_tool_result(raw)\n" ' File "src/agent/tools.py", line 41, in parse_tool_result\n' " return json.loads(raw)['result']\n" "KeyError: 'result'\n\n" "I need root-cause hypotheses, repo search targets, related checks, " "and exact verification commands I can run next." ), expected_advantage=( "OpenSquilla should classify the runtime, parse frames, search repo " "symbols, inspect history/issues, and synthesize verification commands." ), optimization_if_not_better=( "Improve degraded behavior when repo symbols are absent by producing " "language-specific reproduction snippets and patch targets." ), ), ComparisonCase( case_id="travel_planner", skill_name="meta-travel-planner", prompt=( "My partner and I are visiting Tokyo for the first time in late June. " "Could you build a 3-day travel plan with a balanced pace? We care " "about food, transit-friendly neighborhood grouping, rain backups, " "and a moderate budget. Please include your assumptions, a daily " "schedule, weather-aware risks, a few variants, and budget notes." ), expected_advantage=( "OpenSquilla should infer trip preferences, check weather/search results, " "extract constraints, and append variants plus bad-weather backup." ), optimization_if_not_better=( "Improve constraint extraction for dates, opening hours, neighborhood " "grouping, and budget notes before itinerary drafting." ), ), ComparisonCase( case_id="meta_skill_creator", skill_name="meta-skill-creator", prompt=( "I want to compose a meta-skill for our analyst workflow. It should " "combine web research, PDF intelligence, and a final docx export into " "a reusable due-diligence brief workflow. Please include triggers, " "inputs, the step graph, collision risks, gates, and a preview of the " "SKILL.md." ), expected_advantage=( "OpenSquilla should distinguish meta-skill vs normal skill, harvest " "patterns, assemble a candidate, run collision/risk/lint/smoke gates, " "and show a proposal preview." ), optimization_if_not_better=( "Expose clearer preview sections and make collision/risk findings more " "visible when the generated skill is only a draft." ), ), ComparisonCase( case_id="migration_assistant", skill_name="meta-migration-assistant", prompt=( "We're planning to migrate a small frontend package from CommonJS " "to native ESM next sprint. Please give me a practical migration " "checklist with breaking changes, grep patterns for files likely " "affected, validation commands, and rollout risks. Assume this is " "for the current repo, but don't make up files you cannot verify." ), expected_advantage=( "OpenSquilla should classify the migration kind, route to an " "authoritative guide source, optionally inspect current repo diff " "context, and produce a concrete validation checklist." ), optimization_if_not_better=( "Strengthen migration-kind classification, make repo-context use " "more selective, and require explicit source/command evidence in " "the final checklist." ), ), ] COMPARISON_CASES.extend( [ ComparisonCase( case_id="paper_write_citation_boundary", skill_name="meta-paper-write", scenario="degraded", prompt=( "Draft a LaTeX-ready extended abstract about agent meta-skill " "orchestration. Do not fabricate real citations; use BibTeX keys " "like TODO:smith2026 until I provide a library. Include a related " "work plan, evaluation table, limitations, and a path to a 10-page " "paper." ), expected_advantage=( "OpenSquilla should keep manuscript structure while enforcing " "citation integrity rather than inventing references." ), optimization_if_not_better=( "Make citation-integrity gating explicit for short paper drafts and " "mark unresolved references as TODO placeholders." ), failure_modes=( "Invents real-looking citations.", "Omits LaTeX-ready structure.", "Does not explain expansion to full paper length.", ), ), ComparisonCase( case_id="paper_write_scope_control", skill_name="meta-paper-write", scenario="boundary", prompt=( "I do not want the full paper yet. Produce a one-page manuscript " "brief for a future paper on meta-skill evaluation: thesis, section " "outline, evaluation design, risks to validity, and what evidence " "must be collected before writing." ), expected_advantage=( "OpenSquilla should respect the user's scope and produce a planning " "artifact instead of a long manuscript." ), optimization_if_not_better=( "Add scope-control checks so paper mode can return briefs, outlines, " "or full drafts intentionally." ), failure_modes=( "Overproduces a full paper despite the request.", "Omits evidence collection plan.", "No limitations or validity risks.", ), ), ComparisonCase( case_id="pdf_intelligence_missing_file_boundary", skill_name="meta-pdf-intelligence", scenario="boundary", prompt=( "I forgot to attach the PDF. The title is Observability for Agentic " "Systems, but I do not remember the contents. Give me the intake " "questions, extraction plan, evidence table schema, and what you " "must not claim until the file is available." ), expected_advantage=( "OpenSquilla should fail gracefully without hallucinating document " "content and should prepare a traceable extraction workflow." ), optimization_if_not_better=( "Improve missing-PDF handling with an intake template and explicit " "non-claims section." ), failure_modes=( "Summarizes a PDF that was not provided.", "No evidence table or page schema.", "Does not ask for the file or page excerpts.", ), ), ComparisonCase( case_id="pdf_intelligence_two_doc_compare", skill_name="meta-pdf-intelligence", scenario="degraded", prompt=( "Compare two provided PDF excerpts. Doc A page 2 says traces show " "tool calls and model routing. Doc A page 7 says missing spans hide " "retries. Doc B page 4 says cost attribution needs per-step usage. " "Return a cross-document evidence matrix, conflicts, open questions, " "and a memory index." ), expected_advantage=( "OpenSquilla should compare page-grounded evidence across documents " "and preserve provenance in the output." ), optimization_if_not_better=( "Add a multi-document excerpt mode that renders evidence matrices before synthesis." ), failure_modes=( "Merges Doc A and Doc B without provenance.", "Drops page numbers.", "Omits conflicts or open questions.", ), ), ComparisonCase( case_id="stack_trace_ambiguous_boundary", skill_name="meta-stack-trace-investigator", scenario="boundary", prompt=( "I only have this vague error: 'tool result parse failed after a " "provider retry'. No stack trace yet. Give me the minimum data to " "collect, repo search targets, likely failure classes, and commands " "to narrow it down without pretending you know the exact root cause." ), expected_advantage=( "OpenSquilla should avoid false certainty and produce a targeted " "diagnostic collection plan." ), optimization_if_not_better=( "Improve ambiguous-error mode with evidence requirements before root cause claims." ), failure_modes=( "Claims a single root cause without evidence.", "No data collection checklist.", "No concrete repo search commands.", ), ), ComparisonCase( case_id="stack_trace_js_async", skill_name="meta-stack-trace-investigator", scenario="degraded", prompt=( "Investigate this Node stack trace:\n" "TypeError: Cannot read properties of undefined (reading 'content')\n" " at parseAssistantMessage (src/stream/consumer.ts:77:21)\n" " at onDelta (src/stream/consumer.ts:141:9)\n" " at processTicksAndRejections (node:internal/process/task_queues:95:5)\n" "Include hypotheses, TypeScript grep targets, a minimal fixture, " "and verification commands." ), expected_advantage=( "OpenSquilla should adapt the stack-trace workflow to TypeScript " "and produce concrete reproduction and verification targets." ), optimization_if_not_better=( "Add language-aware stack parsing and fixture suggestions for " "JavaScript/TypeScript traces." ), failure_modes=( "Treats it as Python.", "No fixture or verification commands.", "Does not identify undefined content shape.", ), ), ComparisonCase( case_id="travel_planner_constraints", skill_name="meta-travel-planner", scenario="degraded", prompt=( "Plan 4 days in Kyoto for two adults and one parent with knee pain. " "We are vegetarian, prefer rail/bus over taxis, need one rest block " "per day, and have a tea ceremony booking at 15:00 on day 2 near " "Gion. Include assumptions, neighborhood grouping, rain backups, " "budget notes, and variants." ), expected_advantage=( "OpenSquilla should preserve mobility, dietary, fixed-booking, " "weather, budget, and transit constraints." ), optimization_if_not_better=( "Strengthen constraint extraction and schedule feasibility checks " "for accessibility and fixed events." ), failure_modes=( "Schedules high-walking days without rest blocks.", "Ignores vegetarian constraint.", "Misses fixed day-2 booking.", ), ), ComparisonCase( case_id="travel_planner_missing_dates_boundary", skill_name="meta-travel-planner", scenario="boundary", prompt=( "I might visit Seoul sometime next year but I have no dates, budget, " "or neighborhood preference yet. Give me a planning framework, the " "questions you need answered, seasonal tradeoffs, and a sample 2-day " "placeholder itinerary clearly marked as tentative." ), expected_advantage=( "OpenSquilla should ask for missing constraints and mark any sample " "itinerary as provisional." ), optimization_if_not_better=( "Improve missing-constraint mode so tentative plans are clearly " "separated from final itineraries." ), failure_modes=( "Presents a final itinerary despite missing dates.", "No clarifying questions.", "No seasonal tradeoffs.", ), ), ComparisonCase( case_id="meta_skill_creator_collision_boundary", skill_name="meta-skill-creator", scenario="boundary", prompt=( "Before creating anything, evaluate whether a new 'due-diligence " "brief' meta-skill would collide with existing web research, PDF " "intelligence, and doc export skills. Return a collision matrix, " "when not to create it, and a minimal SKILL.md preview only if it " "is still justified." ), expected_advantage=( "OpenSquilla should check whether composition is justified before " "drafting a new meta-skill." ), optimization_if_not_better=( "Add stronger no-new-skill and collision-first gates to the creator workflow." ), failure_modes=( "Creates a full skill without collision analysis.", "Does not mention existing skill overlap.", "No no-create criteria.", ), ), ComparisonCase( case_id="meta_skill_creator_safety_gates", skill_name="meta-skill-creator", scenario="degraded", prompt=( "Design a meta-skill that can run repo search and browser research " "but must never install dependencies, commit code, or publish files " "without explicit user approval. Include triggers, inputs, step " "graph, permission gates, failure modes, and a SKILL.md preview." ), expected_advantage=( "OpenSquilla should encode safety and permission gates as first-class " "workflow requirements." ), optimization_if_not_better=( "Make generated meta-skills include explicit authority boundaries " "and blocked actions by default." ), failure_modes=( "No explicit permission gates.", "Allows install/commit/publish without approval.", "No failure modes.", ), ), ComparisonCase( case_id="migration_assistant_repo_boundary", skill_name="meta-migration-assistant", scenario="boundary", prompt=( "Assess whether this repo is ready for a CommonJS to ESM migration, " "but do not assume files that you have not inspected. Give me a " "repo-discovery checklist, grep patterns, decision gates, and what " "would block migration." ), expected_advantage=( "OpenSquilla should distinguish actual repo evidence from a generic " "migration guide." ), optimization_if_not_better=( "Improve repo-evidence gating and make unsupported assumptions show up as blockers." ), failure_modes=( "Invents repository files.", "No discovery checklist.", "No migration blockers or decision gates.", ), ), ComparisonCase( case_id="migration_assistant_incremental_rollout", skill_name="meta-migration-assistant", scenario="degraded", prompt=( "We need an incremental CommonJS to ESM migration plan for a package " "used by downstream apps. Include dual-package hazards, package.json " "exports changes, test/build commands, grep patterns, rollback plan, " "and release sequencing." ), expected_advantage=( "OpenSquilla should produce a practical migration plan with rollout " "risk controls instead of only syntax changes." ), optimization_if_not_better=( "Strengthen rollout-risk handling for migrations that affect downstream consumers." ), failure_modes=( "Only describes import/export syntax.", "No rollback or release sequence.", "No dual-package hazard discussion.", ), ), ] ) def rubric_for_case(case: ComparisonCase) -> tuple[RubricCriterion, ...]: return case.rubric or SKILL_RUBRICS.get(case.skill_name, ()) def score_response(text: str, case: ComparisonCase | None = None) -> ResponseScore: if case is not None and rubric_for_case(case): return score_case_response(text, case) return score_generic_response(text) def score_case_response(text: str, case: ComparisonCase) -> ResponseScore: dimensions: dict[str, int] = {} notes: list[str] = [] for item in rubric_for_case(case): matched = any(re.search(pattern, text, flags=re.I | re.M) for pattern in item.patterns) dimensions[item.name] = item.weight if matched else 0 if not matched: notes.append(item.name) return ResponseScore(total=sum(dimensions.values()), dimensions=dimensions, notes=notes) def score_generic_response(text: str) -> ResponseScore: lowered = text.lower() dimensions = { "structure": min( 5, _count_matches(text, [r"^#", r"^\s*[-*]\s+", r"^\s*\d+[.)]\s+", r"\|.+\|"]) + (1 if len(text) > 800 else 0), ), "evidence": min( 5, _count_matches( text, [ r"https?://", r"\[[0-9]+\]", r"\bpage\s+\d+\b", r"\bsource\b", r"\bcitation\b", r"\bfile\s+\"?", ], ), ), "artifact_readiness": min( 5, _count_words( lowered, [ "artifact", "docx", "pptx", "latex", "bib", "html", "slide", "report", "manuscript", "migration", "checklist", ], ), ), "actionability": min( 5, _count_words( lowered, ["command", "verify", "check", "next step", "schedule", "itinerary", "risk"], ), ), "constraint_handling": min( 5, _count_words( lowered, ["assumption", "constraint", "budget", "audience", "limitation", "preference"], ), ), "traceability": min( 5, _count_words( lowered, ["evidence", "source", "page", "reference", "trace", "reproduce", "gate"], ), ), } notes = [name for name, value in dimensions.items() if value <= 1] return ResponseScore(total=sum(dimensions.values()), dimensions=dimensions, notes=notes) def _count_matches(text: str, patterns: list[str]) -> int: return sum(1 for pattern in patterns if re.search(pattern, text, flags=re.I | re.M)) def _count_words(text: str, words: list[str]) -> int: return sum(1 for word in words if word in text) def normalized_winner(value: str) -> str: lowered = value.strip().lower() if lowered in {"opensquilla", "openclaw", "tie"}: return lowered if lowered in {"a", "response_a", "candidate_a"}: return "opensquilla" if lowered in {"b", "response_b", "candidate_b"}: return "openclaw" return "tie" def response_excerpt(text: str, *, max_chars: int = 12000) -> str: stripped = text.strip() if len(stripped) <= max_chars: return stripped head = max_chars // 2 tail = max_chars - head return ( stripped[:head] + "\n\n[... middle truncated for judge prompt ...]\n\n" + stripped[-tail:] ) def blind_product_names(text: str) -> str: return ( text.replace("OpenSquilla", "the specialized meta-skill system") .replace("opensquilla", "the specialized meta-skill system") .replace("OpenClaw", "the baseline agent system") .replace("openclaw", "the baseline agent system") ) def build_judge_prompt( case: ComparisonCase, opensquilla: EndpointResult, openclaw: EndpointResult, ) -> str: rubric = "\n".join(f"- {item.name}: {item.description}" for item in rubric_for_case(case)) failure_modes = "\n".join(f"- {item}" for item in case.failure_modes) or "- None listed" return textwrap.dedent( f""" You are judging a product-quality benchmark between two anonymous AI agent systems. Judge only the two answers. Do not reward brand assumptions. The user asked: {case.prompt} Evaluation constraints: - Judge the final user-visible answer, not hidden orchestration traces. - Do not reward claims that tools, files, commands, or external checks were completed unless the answer provides visible evidence. - If the user asked for an artifact, reward artifact-ready content that can be pasted or used directly. Meta-skill under test: {case.skill_name} Scenario: {case.scenario} Expected advantage being tested: {blind_product_names(case.expected_advantage)} Rubric dimensions: {rubric} Known failure modes to penalize: {blind_product_names(failure_modes)} Endpoint health: - Candidate A ok={opensquilla.ok}, error={opensquilla.error} - Candidate B ok={openclaw.ok}, error={openclaw.error} JSON label mapping: - Use "opensquilla" for Candidate A. - Use "openclaw" for Candidate B. These are opaque output labels for the two anonymous candidates, not product facts. Do not infer quality from the labels. Candidate A answer: ```text {response_excerpt(opensquilla.response_text)} ``` Candidate B answer: ```text {response_excerpt(openclaw.response_text)} ``` Prioritize the quality of the final user-visible deliverable. Meta-skill orchestration is valuable only when it produces a better final artifact. Score each candidate from 0 to 100 using these weights: - final_artifact_quality: 40 points. The final deliverable is complete, coherent, polished, directly usable, appropriately structured for the requested artifact, and avoids distracting boilerplate. - task_completion: 20 points. It directly solves the user's concrete request, including all requested sub-parts. - evidence_traceability: 15 points. It maps important claims to pasted facts, source URLs, file/page evidence, or observed tool output where applicable. - actionability: 10 points. It gives concrete next steps, decisions, owners, checks, or schedules that the user can execute without another planning pass. - risk_boundary_safety: 10 points. It flags legal, finance, medical, security, privacy, or unknown-evidence boundaries instead of over-claiming. - meta_skill_fit: 5 points. It shows specialized workflow behavior expected for this meta-skill, beyond a generic high-end chat answer. The top-level scores MUST equal the sum of the six weighted subscores for each candidate. Do not invent an independent overall score. Use these cross-cutting checks while assigning the weighted score: - endpoint_validity: only compare usable, non-empty answers from healthy endpoints. Do not award a win because the other endpoint errored, timed out, or returned an empty response; treat that row as inconclusive unless both usable answers exist. - correctness_grounding: facts, constraints, citations, commands, and caveats are plausible, internally consistent, and not invented. - constraint_following: obeys inline-only/no-write/no-fake-execution constraints. - fairness_control: do not reward brand assumptions, model reputation, verbosity, or unrelated bootstrap/runtime/system commentary. Penalize unrelated bootstrap notes or tool/runtime chatter that distracts from the user's deliverable. - concision_efficiency: useful density without filler; do not reward verbosity alone. Hard caps: - timeout, empty response, or endpoint error: max 20 unless the other side also failed - answer is mostly off-task: max 30 - fabricated source/file/tool execution: max 50 - violates no-write/no-execute constraint: max 70 Return strict JSON only with this schema: {{ "winner": "opensquilla" | "openclaw" | "tie", "scores": {{"opensquilla": 0-100, "openclaw": 0-100}}, "subscores": {{ "opensquilla": {{ "final_artifact_quality": 0-40, "task_completion": 0-20, "evidence_traceability": 0-15, "actionability": 0-10, "risk_boundary_safety": 0-10, "meta_skill_fit": 0-5 }}, "openclaw": {{ "final_artifact_quality": 0-40, "task_completion": 0-20, "evidence_traceability": 0-15, "actionability": 0-10, "risk_boundary_safety": 0-10, "meta_skill_fit": 0-5 }} }}, "confidence": 0.0-1.0, "rationale": "one short paragraph", "risks": ["short risk or uncertainty"] }} """ ).strip() def parse_judge_response(text: str, model: str) -> JudgeResult: data = _load_json_object(text) raw_scores = data.get("scores") if isinstance(data.get("scores"), dict) else {} if not raw_scores and {"opensquilla", "openclaw"} <= set(data): raw_scores = data scores = { "opensquilla": int(raw_scores.get("opensquilla", 0)), "openclaw": int(raw_scores.get("openclaw", 0)), } winner = normalized_winner(str(data.get("winner", ""))) if winner not in {"opensquilla", "openclaw", "tie"}: winner = "tie" if winner == "tie" and scores["opensquilla"] != scores["openclaw"]: winner = "opensquilla" if scores["opensquilla"] > scores["openclaw"] else "openclaw" confidence_raw = data.get("confidence", 0) try: confidence = max(0.0, min(1.0, float(confidence_raw))) except (TypeError, ValueError): confidence = 0.0 risks_raw = data.get("risks") if isinstance(data.get("risks"), list) else [] risks = [str(item) for item in risks_raw[:5]] return JudgeResult( winner=winner, scores=scores, confidence=confidence, rationale=str(data.get("rationale", "")).strip(), risks=risks, raw=data, model=model, ) def _load_json_object(text: str) -> dict[str, Any]: candidates = [text.strip()] candidates.extend( match.group(1).strip() for match in re.finditer(r"```(?:json)?\s*(.*?)```", text, flags=re.S | re.I) ) match = re.search(r"\{.*\}", text, flags=re.S) if match: candidates.append(match.group(0)) decoder = json.JSONDecoder() errors: list[str] = [] for candidate in candidates: if not candidate: continue try: data = json.loads(candidate) except json.JSONDecodeError as exc: errors.append(str(exc)) else: if isinstance(data, dict): return data for index, char in enumerate(candidate): if char != "{": continue try: data, _ = decoder.raw_decode(candidate[index:]) except json.JSONDecodeError: continue if isinstance(data, dict): return data fallback = _load_json_object_from_fields(text) if fallback: return fallback excerpt = text.strip().replace("\n", " ")[:500] raise ValueError( f"judge response was not parseable JSON: {'; '.join(errors[:3])}; excerpt={excerpt!r}" ) def _load_json_object_from_fields(text: str) -> dict[str, Any] | None: winner_match = re.search(r'"winner"\s*:\s*"([^"]+)"', text, flags=re.I) sq_match = re.search(r'"opensquilla"\s*:\s*([0-9]{1,3})', text, flags=re.I) claw_match = re.search(r'"openclaw"\s*:\s*([0-9]{1,3})', text, flags=re.I) if not (winner_match and sq_match and claw_match): return None confidence_match = re.search(r'"confidence"\s*:\s*([0-9.]+)', text, flags=re.I) return { "winner": winner_match.group(1), "scores": { "opensquilla": int(sq_match.group(1)), "openclaw": int(claw_match.group(1)), }, "confidence": float(confidence_match.group(1)) if confidence_match else 0.0, "rationale": "", "risks": ["Judge response was recovered from malformed JSON fields."], } def _extract_payload_texts(payload: dict[str, Any], *, include_delta: bool) -> list[str]: texts: list[str] = [] message = payload.get("message") if isinstance(message, dict) and message.get("role") == "assistant": text = _content_to_text(message.get("content")) if text: texts.append(text) data = payload.get("data") if isinstance(data, dict) and isinstance(data.get("text"), str): texts.append(data["text"]) keys = ("text", "content", "final", "response") if include_delta: keys = (*keys, "delta") for key in keys: value = payload.get(key) if isinstance(value, str) and value.strip(): texts.append(value) return [text.strip() for text in texts if text and text.strip()] def _is_tool_or_meta_step_event(event: dict[str, Any], payload: dict[str, Any]) -> bool: event_name = event.get("event") if isinstance(event_name, str): lowered = event_name.lower() if "tool" in lowered or "meta.step" in lowered or "meta-step" in lowered: return True for key in ("tool_name", "tool_use_id", "tool_call_id", "toolResult", "tool_result"): if payload.get(key): return True data = payload.get("data") if isinstance(data, dict): for key in ("tool_name", "tool_use_id", "tool_call_id", "toolResult", "tool_result"): if data.get(key): return True role = data.get("role") if role in {"tool", "function"}: return True message = payload.get("message") if isinstance(message, dict) and message.get("role") in {"tool", "function"}: return True return False def extract_text_from_events(events: list[dict[str, Any]]) -> str: terminal_candidates: list[str] = [] assistant_candidates: list[str] = [] fallback_candidates: list[str] = [] delta_candidates: list[str] = [] for event in events: payload = event.get("payload") if isinstance(event.get("payload"), dict) else event if not isinstance(payload, dict): continue event_name = event.get("event") is_terminal = event_name == "session.event.done" texts = _extract_payload_texts(payload, include_delta=False) delta_texts = _extract_payload_texts(payload, include_delta=True) if is_terminal: terminal_candidates.extend(texts) continue is_toolish = _is_tool_or_meta_step_event(event, payload) message = payload.get("message") if isinstance(message, dict) and message.get("role") == "assistant" and not is_toolish: assistant_candidates.extend(texts) continue if not is_toolish: fallback_candidates.extend(texts) delta_candidates.extend(text for text in delta_texts if text not in texts) for candidates in (terminal_candidates, assistant_candidates, fallback_candidates): if candidates: return candidates[-1].strip() if delta_candidates: return max(delta_candidates, key=len).strip() return "" def extract_error_from_events(events: list[dict[str, Any]]) -> str | None: for event in reversed(events): payload = event.get("payload") if isinstance(event.get("payload"), dict) else event if not isinstance(payload, dict): continue event_name = event.get("event") is_error_event = isinstance(event_name, str) and event_name.endswith(".error") message = payload.get("message") if isinstance(message, dict) and isinstance(message.get("errorMessage"), str): return message["errorMessage"] data = payload.get("data") if isinstance(data, dict) and isinstance(data.get("error"), str): return data["error"] if isinstance(data, dict) and data.get("phase") == "error": value = data.get("error") if isinstance(value, str) and value.strip(): return value if payload.get("state") == "error" or is_error_event: for key in ("errorMessage", "error"): value = payload.get(key) if isinstance(value, str) and value.strip(): return value return None def _content_to_text(content: Any) -> str: if isinstance(content, str): return content if isinstance(content, list): parts: list[str] = [] for item in content: if isinstance(item, dict) and isinstance(item.get("text"), str): parts.append(item["text"]) elif isinstance(item, str): parts.append(item) return "".join(parts) return "" async def _send_application_pings(ws: Any, interval_s: float = 45.0) -> None: """Keep gateway app-level receive loops alive during long benchmark turns.""" while True: await asyncio.sleep(interval_s) await ws.send('{"type":"ping"}') def _slug_part(value: str, max_len: int = 64) -> str: slug = re.sub(r"[^a-z0-9_-]+", "-", str(value).strip().lower()).strip("-") return (slug or "default")[:max_len] class OpenSquillaRunner: def __init__( self, url: str, token: str | None, elevated: str | None = None, agent_id: str = "main", isolated_agent_per_case: bool = False, run_id: str | None = None, ) -> None: self.url = url self.token = token self.elevated = elevated self.agent_id = agent_id self.isolated_agent_per_case = isolated_agent_per_case self.run_id = _slug_part(run_id or uuid.uuid4().hex[:8]) def _agent_id_for_case(self, case: ComparisonCase) -> str: if not self.isolated_agent_per_case: return self.agent_id case_part = _slug_part(case.case_id.replace("_", "-"), max_len=32) prefix = _slug_part(self.agent_id if self.agent_id != "main" else "meta-compare") return _slug_part(f"{prefix}-{self.run_id}-{case_part}", max_len=64) async def run(self, case: ComparisonCase, timeout_s: float) -> EndpointResult: start = time.monotonic() try: result = await asyncio.wait_for(self._run(case), timeout=timeout_s) result.elapsed_s = round(time.monotonic() - start, 2) return result except SystemExit as exc: return _error_result("opensquilla", case.case_id, start, exc) except Exception as exc: return _error_result("opensquilla", case.case_id, start, exc) async def _run(self, case: ComparisonCase) -> EndpointResult: events: list[dict[str, Any]] = [] control_events: list[dict[str, Any]] = [] import websockets headers = {"Authorization": f"Bearer {self.token}"} if self.token else None async with websockets.connect( self.url, ping_interval=20, ping_timeout=20, additional_headers=headers, ) as ws: first = json.loads(await ws.recv()) if first.get("event") != "connect.challenge": raise RuntimeError(f"unexpected OpenSquilla handshake: {first}") auth_params = {"auth": {"token": self.token}} if self.token else {} await self._call( ws, "connect", { "minProtocol": 3, "maxProtocol": 3, "role": "operator", "scopes": ["operator.admin"], "client": {"name": "meta-skill-comparison"}, **auth_params, }, control_events, ) agent_id = self._agent_id_for_case(case) if agent_id != "main": try: await self._call( ws, "agents.create", { "id": agent_id, "name": f"Meta Compare {case.case_id}", "description": "Isolated agent for one meta-skill comparison case.", }, control_events, ) except RuntimeError as exc: if "agent.exists" not in str(exc): raise created = await self._call( ws, "sessions.create", { "agentId": agent_id, "kind": "cli", "displayName": f"meta compare {case.case_id}", }, control_events, ) session_key = str(created["key"]) await self._call( ws, "sessions.messages.subscribe", {"key": session_key}, control_events, ) source: dict[str, Any] = { "caller_kind": "cli", "channel_kind": "cli", "channel_id": "cli:meta-skill-comparison", "source_kind": "cli", "source_name": "meta-skill-comparison", } if self.elevated in ("on", "bypass", "full"): source["elevated"] = self.elevated await self._call( ws, "sessions.send", { "key": session_key, "message": benchmark_prompt(case), "attachments": [], "_source": source, }, events, session_key=session_key, ) await self._read_stream(ws, events, session_key) session_events = _events_for_session(events, session_key) text = extract_text_from_events(session_events) meta_final_text = _latest_opensquilla_meta_final_text(session_key) if meta_final_text: text = meta_final_text transcript_text = await _wait_for_opensquilla_transcript_text( session_key, minimum_len=len(text), ) if not meta_final_text and len(transcript_text) > len(text): text = transcript_text stream_error = extract_error_from_events(session_events) score = score_response(text, case) provider, model = _provider_model_from_events(session_events) return EndpointResult( endpoint="opensquilla", case_id=case.case_id, ok=bool(text) and stream_error is None, elapsed_s=0.0, response_text=text, score=asdict(score), error=stream_error, session_key=session_key, provider=provider, model=model, event_count=len(session_events), ) async def _call( self, ws: Any, method: str, params: dict[str, Any], events: list[dict[str, Any]], session_key: str | None = None, ) -> dict[str, Any]: req_id = str(uuid.uuid4()) await ws.send(json.dumps({"type": "req", "id": req_id, "method": method, "params": params})) while True: frame = json.loads(await ws.recv()) if frame.get("type") == "event": if session_key is None or _event_session_key(frame) == session_key: events.append(frame) continue if method == "connect" and frame.get("protocol") is not None: return frame if frame.get("type") == "res" and frame.get("id") == req_id: if not frame.get("ok"): raise RuntimeError(f"{method} failed: {frame.get('error')}") payload = frame.get("payload") return payload if isinstance(payload, dict) else {} async def _read_stream( self, ws: Any, events: list[dict[str, Any]], session_key: str, ) -> None: keepalive = asyncio.create_task(_send_application_pings(ws)) try: while True: frame = json.loads(await ws.recv()) if frame.get("type") != "event": continue if _event_session_key(frame) != session_key: continue events.append(frame) if frame.get("event") in ("session.event.done", "session.event.error"): return finally: keepalive.cancel() with contextlib.suppress(asyncio.CancelledError): await keepalive class OpenClawRunner: def __init__( self, url: str, token: str, idle_timeout_s: float = 90.0, state_dir: Path | None = None, ) -> None: self.url = url self.token = token self.idle_timeout_s = idle_timeout_s self.state_dir = state_dir async def run(self, case: ComparisonCase, timeout_s: float) -> EndpointResult: start = time.monotonic() try: result = await asyncio.wait_for(self._run(case), timeout=timeout_s) result.elapsed_s = round(time.monotonic() - start, 2) return result except Exception as exc: return _error_result("openclaw", case.case_id, start, exc) async def _run(self, case: ComparisonCase) -> EndpointResult: import websockets control_events: list[dict[str, Any]] = [] events: list[dict[str, Any]] = [] started_at = time.time() prompt = benchmark_prompt(case) headers = {"Authorization": f"Bearer {self.token}"} if self.token else None async with websockets.connect( self.url, ping_interval=None, additional_headers=headers, ) as ws: first = json.loads(await ws.recv()) if first.get("event") != "connect.challenge": raise RuntimeError(f"unexpected OpenClaw handshake: {first}") await self._call( ws, "connect", { "minProtocol": 1, "maxProtocol": 3, "role": "operator", "scopes": ["operator.admin"], "auth": {"token": self.token}, "client": { "id": "openclaw-tui", "mode": "cli", "version": "0", "platform": "linux", }, }, control_events, ) created = await self._call( ws, "sessions.create", {"agentId": "main"}, control_events, ) session_key = str(created["key"]) entry = created.get("entry") if isinstance(created.get("entry"), dict) else {} session_file = entry.get("sessionFile") await self._call( ws, "sessions.messages.subscribe", {"key": session_key}, control_events, ) warmup_events: list[dict[str, Any]] = [] await self._call( ws, "sessions.send", {"key": session_key, "message": OPENCLAW_BASELINE_WARMUP}, warmup_events, session_key=session_key, ) await self._read_openclaw_stream(ws, warmup_events, session_key) await self._call( ws, "sessions.send", {"key": session_key, "message": prompt}, events, session_key=session_key, ) await self._read_openclaw_stream(ws, events, session_key) session_paths: list[Path] = [] session_path = _resolve_openclaw_session_path(session_file, self.state_dir) if session_path is not None and _is_openclaw_session_jsonl(session_path): session_paths.append(session_path) if self.state_dir is not None: discovered = _discover_openclaw_session_file( self.state_dir, session_key=session_key, prompt=prompt, started_at=started_at, ) if discovered is not None and discovered not in session_paths: session_paths.append(discovered) file_events = await _wait_for_openclaw_session_file_events( session_paths, session_key=session_key, after_prompt=prompt, timeout_s=self.idle_timeout_s, ) events.extend(file_events) session_events = _events_for_session(events, session_key) text = extract_text_from_events(session_events) stream_error = extract_error_from_events(session_events) score = score_response(text, case) provider, model = _provider_model_from_events(session_events) return EndpointResult( endpoint="openclaw", case_id=case.case_id, ok=bool(text) and stream_error is None, elapsed_s=0.0, response_text=text, score=asdict(score), error=stream_error, session_key=session_key, provider=provider, model=model, event_count=len(session_events), ) async def _call( self, ws: Any, method: str, params: dict[str, Any], events: list[dict[str, Any]], session_key: str | None = None, ) -> dict[str, Any]: req_id = str(uuid.uuid4()) await ws.send(json.dumps({"type": "req", "id": req_id, "method": method, "params": params})) while True: frame = json.loads(await ws.recv()) if frame.get("type") == "event": if session_key is None or _event_session_key(frame) == session_key: events.append(frame) continue if frame.get("type") == "res" and frame.get("id") == req_id: if not frame.get("ok"): raise RuntimeError(f"{method} failed: {frame.get('error')}") payload = frame.get("payload") return payload if isinstance(payload, dict) else {} async def _read_openclaw_stream( self, ws: Any, events: list[dict[str, Any]], session_key: str, ) -> None: keepalive = asyncio.create_task(_send_application_pings(ws)) deadline = time.monotonic() + self.idle_timeout_s try: while True: timeout = max(0.1, deadline - time.monotonic()) try: frame = json.loads(await asyncio.wait_for(ws.recv(), timeout=timeout)) except TimeoutError: return if frame.get("type") != "event": continue if _event_session_key(frame) != session_key: continue events.append(frame) if frame.get("event") == "chat": payload = frame.get("payload") if isinstance(frame.get("payload"), dict) else {} if payload.get("state") == "final": return if payload.get("state") == "error": return if frame.get("event") == "agent": payload = frame.get("payload") if isinstance(frame.get("payload"), dict) else {} data = payload.get("data") if isinstance(payload.get("data"), dict) else {} if data.get("phase") == "error": return if frame.get("event") == "session.event.error": return finally: keepalive.cancel() with contextlib.suppress(asyncio.CancelledError): await keepalive class LLMJudge: def __init__( self, *, model: str, api_key: str | None, base_url: str = "https://openrouter.ai/api/v1", timeout_s: float = 120.0, ) -> None: self.model = model self.api_key = api_key self.base_url = base_url.rstrip("/") self.timeout_s = timeout_s async def judge( self, case: ComparisonCase, opensquilla: EndpointResult, openclaw: EndpointResult, ) -> JudgeResult: if not self.api_key: raise RuntimeError("LLM judge requires OPENROUTER_API_KEY or --judge-api-key") import httpx prompt = build_judge_prompt(case, opensquilla, openclaw) payload = { "model": self.model, "messages": [ { "role": "system", "content": ( "You are a strict benchmark judge. Return only valid JSON. " "Do not mention hidden chain-of-thought." ), }, {"role": "user", "content": prompt}, ], "temperature": 0, "max_tokens": 1200, "response_format": {"type": "json_object"}, } headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", "HTTP-Referer": "http://localhost/opensquilla-meta-skill-comparison", "X-Title": "OpenSquilla Meta Skill Comparison", } async with httpx.AsyncClient(timeout=self.timeout_s) as client: response = await client.post( f"{self.base_url}/chat/completions", headers=headers, json=payload, ) response.raise_for_status() data = response.json() choices = data.get("choices") if isinstance(data, dict) else None if not isinstance(choices, list) or not choices: raise RuntimeError("judge response has no choices") message = choices[0].get("message") if isinstance(choices[0], dict) else None content = message.get("content") if isinstance(message, dict) else None if not isinstance(content, str) or not content.strip(): raise RuntimeError("judge response has empty content") return parse_judge_response(content, self.model) def benchmark_prompt(case: ComparisonCase) -> str: return case.prompt def _event_session_key(event: dict[str, Any]) -> str | None: payload = event.get("payload") if isinstance(event.get("payload"), dict) else {} session_key = payload.get("sessionKey") if isinstance(session_key, str): return session_key session = payload.get("session") if isinstance(session, dict) and isinstance(session.get("key"), str): return session["key"] return None def _provider_model_from_events(events: list[dict[str, Any]]) -> tuple[str | None, str | None]: for event in reversed(events): payload = event.get("payload") if isinstance(event.get("payload"), dict) else event message = payload.get("message") if isinstance(payload, dict) else None if isinstance(message, dict): provider = message.get("provider") or message.get("modelProvider") model = message.get("model") if provider or model: return ( str(provider) if provider else None, str(model) if model else None, ) provider = payload.get("provider") if isinstance(payload, dict) else None model = payload.get("model") if isinstance(payload, dict) else None if provider or model: return (str(provider) if provider else None, str(model) if model else None) return None, None def _events_for_session(events: list[dict[str, Any]], session_key: str) -> list[dict[str, Any]]: filtered = [ event for event in events if (key := _event_session_key(event)) is None or key == session_key ] return filtered or events def _latest_opensquilla_transcript_text(session_key: str) -> str: """Return the persisted final assistant text for a local gateway session. Some gateway streams emit only the assistant preface before a long-running ``meta_invoke`` while the complete final text is persisted to the transcript after the DAG finishes. Prefer the persisted transcript when available so the benchmark judges the actual user-visible final assistant message. """ if not session_key: return "" state_db = Path(os.environ.get("OPENSQUILLA_STATE_DB", "/root/.opensquilla/state/sessions.db")) if not state_db.exists(): return "" try: import sqlite3 with sqlite3.connect(state_db) as conn: rows = conn.execute( "SELECT content FROM transcript_entries " "WHERE session_key=? AND role='assistant' " "ORDER BY id ASC", (session_key,), ).fetchall() except Exception: return "" texts = [str(row[0]).strip() for row in rows if row and row[0] and str(row[0]).strip()] return texts[-1] if texts else "" def _latest_opensquilla_meta_final_text(session_key: str) -> str: if not session_key: return "" state_db = Path(os.environ.get("OPENSQUILLA_STATE_DB", "/root/.opensquilla/state/sessions.db")) if not state_db.exists(): return "" try: import sqlite3 with sqlite3.connect(state_db) as conn: rows = conn.execute( "SELECT final_text FROM meta_skill_runs " "WHERE session_key=? AND status='ok' " "ORDER BY started_at_ms ASC", (session_key,), ).fetchall() except Exception: return "" texts = [str(row[0]).strip() for row in rows if row and row[0] and str(row[0]).strip()] return texts[-1] if texts else "" async def _wait_for_opensquilla_transcript_text( session_key: str, *, minimum_len: int, timeout_s: float = 5.0, interval_s: float = 0.25, ) -> str: """Poll briefly for the final assistant transcript after stream completion.""" deadline = time.monotonic() + timeout_s best = "" while True: text = _latest_opensquilla_transcript_text(session_key) if len(text) > len(best): best = text if len(best) > minimum_len or time.monotonic() >= deadline: return best await asyncio.sleep(interval_s) def _openclaw_session_file_events( path: Path, session_key: str, *, after_prompt: str | None = None, ) -> list[dict[str, Any]]: if not path.exists(): return [] events: list[dict[str, Any]] = [] prompt_seen = after_prompt is None for line in path.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue try: item = json.loads(line) except json.JSONDecodeError: continue message = item.get("message") if isinstance(item, dict) else None if item.get("type") == "message" and isinstance(message, dict): if not prompt_seen: if ( message.get("role") == "user" and after_prompt and after_prompt in _content_to_text(message.get("content")) ): prompt_seen = True continue events.append( { "type": "event", "event": "session.message", "payload": {"sessionKey": session_key, "message": message}, } ) return events async def _wait_for_openclaw_session_file_events( paths: list[Path], *, session_key: str, after_prompt: str, timeout_s: float = 90.0, interval_s: float = 0.5, stable_s: float = 5.0, ) -> list[dict[str, Any]]: """Poll OpenClaw's JSONL file because the WS stream may end before persistence.""" deadline = time.monotonic() + timeout_s best: list[dict[str, Any]] = [] best_text = "" last_change = time.monotonic() while True: for path in paths: events = _openclaw_session_file_events( path, session_key, after_prompt=after_prompt, ) text = extract_text_from_events(events) if text != best_text: best = events best_text = text last_change = time.monotonic() now = time.monotonic() if best_text and now - last_change >= stable_s: return best if now >= deadline: return best await asyncio.sleep(interval_s) def _resolve_openclaw_session_path(session_file: Any, state_dir: Path | None) -> Path | None: if not isinstance(session_file, str) or not session_file.strip(): return None if state_dir is not None and session_file.startswith("$OPENCLAW_STATE_DIR/"): return state_dir / session_file.removeprefix("$OPENCLAW_STATE_DIR/") path = Path(session_file) if path.is_absolute() or state_dir is None: return path candidate = state_dir / path return candidate if candidate.exists() else path def _is_openclaw_session_jsonl(path: Path) -> bool: return path.exists() and path.suffix == ".jsonl" and ".trajectory" not in path.name def _discover_openclaw_session_file( state_dir: Path, *, session_key: str, prompt: str, started_at: float, ) -> Path | None: sessions_dir = state_dir / "agents" / "main" / "sessions" if not sessions_dir.exists(): return None candidates: list[tuple[float, Path]] = [] for path in sessions_dir.glob("*.jsonl"): if ".trajectory" in path.name: continue try: stat = path.stat() if stat.st_mtime < started_at - 5: continue text = path.read_text(encoding="utf-8") except OSError: continue trajectory_path = path.with_name(f"{path.stem}.trajectory.jsonl") trajectory_text = "" if trajectory_path.exists(): try: trajectory_text = trajectory_path.read_text(encoding="utf-8") except OSError: trajectory_text = "" if ( prompt in text or _openclaw_session_file_contains_prompt(path, prompt) or session_key in trajectory_text ): candidates.append((stat.st_mtime, path)) if not candidates: return None return max(candidates, key=lambda item: item[0])[1] def _openclaw_session_file_contains_prompt(path: Path, prompt: str) -> bool: try: lines = path.read_text(encoding="utf-8").splitlines() except OSError: return False for line in lines: try: item = json.loads(line) except json.JSONDecodeError: continue message = item.get("message") if isinstance(item, dict) else None if not isinstance(message, dict) or message.get("role") != "user": continue text = _content_to_text(message.get("content")) if prompt in text: return True return False def _event_session_key(event: dict[str, Any]) -> str | None: payload = event.get("payload") if isinstance(event.get("payload"), dict) else event if not isinstance(payload, dict): return None for key_name in ("sessionKey", "session_key", "key"): value = payload.get(key_name) if isinstance(value, str): return value session = payload.get("session") if isinstance(session, dict): value = session.get("key") if isinstance(value, str): return value return None def _error_result(endpoint: str, case_id: str, start: float, exc: Exception) -> EndpointResult: return EndpointResult( endpoint=endpoint, case_id=case_id, ok=False, elapsed_s=round(time.monotonic() - start, 2), response_text="", score=asdict(score_response("")), error=f"{type(exc).__name__}: {exc}", ) def read_openclaw_token(config_path: Path) -> str: data = json.loads(config_path.read_text(encoding="utf-8")) token = data.get("gateway", {}).get("auth", {}).get("token") if not isinstance(token, str) or not token: raise RuntimeError(f"OpenClaw token missing in {config_path}") return token def read_opensquilla_token() -> str | None: for env_name in ("OPENSQUILLA_GATEWAY_TOKEN", "OPENSQUILLA_TOKEN"): value = os.environ.get(env_name) if value: return value token_file = os.environ.get("OPENSQUILLA_GATEWAY_TOKEN_FILE") if token_file: path = Path(token_file) match = re.search(r'^TOKEN\s*=\s*"([^"]+)"', path.read_text(encoding="utf-8"), re.M) if match: return match.group(1) return None def read_judge_api_key() -> str | None: for env_name in ("OPENSQUILLA_JUDGE_API_KEY", "OPENROUTER_API_KEY"): value = os.environ.get(env_name) if value: return value return None async def run_live(args: argparse.Namespace) -> list[dict[str, Any]]: selected = _select_cases(args.case, scenario=args.scenario, skill=args.skill) if not args.openclaw_config: raise SystemExit("Pass --openclaw-config or set OPENCLAW_CONFIG.") openclaw_token = read_openclaw_token(Path(args.openclaw_config)) opensquilla = OpenSquillaRunner( args.opensquilla_url, args.opensquilla_token, elevated=args.opensquilla_elevated, ) openclaw = OpenClawRunner( args.openclaw_url, openclaw_token, args.openclaw_idle_timeout, state_dir=Path(args.openclaw_config).parent, ) judge = None if args.judge_llm: if not args.judge_model: raise SystemExit("Pass --judge-model or set OPENSQUILLA_JUDGE_MODEL.") judge = LLMJudge( model=args.judge_model, api_key=args.judge_api_key, base_url=args.judge_base_url, timeout_s=args.judge_timeout, ) rows: list[dict[str, Any]] = [] stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ") for case in selected: print(f"running {case.case_id} ...", flush=True) sq_result, claw_result = await asyncio.gather( opensquilla.run(case, args.timeout), openclaw.run(case, args.timeout), ) row = compare_results(case, sq_result, claw_result) if judge is not None: try: judge_result = await judge_with_retries(judge, case, sq_result, claw_result) row = apply_judge_result(row, judge_result, case) except Exception as exc: row["judge_error"] = f"{type(exc).__name__}: {exc}" rows.append(row) print( f"{case.case_id}: opensquilla={sq_result.score['total']} " f"openclaw={claw_result.score['total']} winner={row['winner']}", flush=True, ) write_reports(rows, stamp=stamp) write_reports(rows, stamp=stamp) return rows async def judge_existing(args: argparse.Namespace) -> list[dict[str, Any]]: if not args.judge_jsonl: raise SystemExit("Pass --judge-jsonl.") if not args.judge_model: raise SystemExit("Pass --judge-model or set OPENSQUILLA_JUDGE_MODEL.") judge = LLMJudge( model=args.judge_model, api_key=args.judge_api_key, base_url=args.judge_base_url, timeout_s=args.judge_timeout, ) rows = [ json.loads(line) for line in Path(args.judge_jsonl).read_text(encoding="utf-8").splitlines() if line.strip() ] judged_rows: list[dict[str, Any]] = [] stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ") for row in rows: case = case_from_row(row) opensquilla = endpoint_from_row(row, "opensquilla") openclaw = endpoint_from_row(row, "openclaw") row.setdefault("baseline_winner", row.get("winner", "tie")) row.setdefault("score_basis", "deterministic") try: judge_result = await judge_with_retries(judge, case, opensquilla, openclaw) judged = apply_judge_result(row, judge_result, case) except Exception as exc: judged = dict(row) judged["judge_error"] = f"{type(exc).__name__}: {exc}" judged_rows.append(judged) print(f"judged {case.case_id}: winner={judged.get('winner')}", flush=True) write_reports(judged_rows, stamp=stamp) write_reports(judged_rows, stamp=stamp) return judged_rows def compare_results( case: ComparisonCase, opensquilla: EndpointResult, openclaw: EndpointResult, ) -> dict[str, Any]: sq_total = int(opensquilla.score["total"]) claw_total = int(openclaw.score["total"]) if not opensquilla.ok and openclaw.ok: baseline_winner = "openclaw" elif opensquilla.ok and not openclaw.ok: baseline_winner = "opensquilla" elif sq_total > claw_total: baseline_winner = "opensquilla" elif claw_total > sq_total: baseline_winner = "openclaw" else: baseline_winner = "tie" return { "case": case_to_dict(case), "opensquilla": asdict(opensquilla), "openclaw": asdict(openclaw), "baseline_winner": baseline_winner, "winner": baseline_winner, "score_basis": "deterministic", "opensquilla_better": baseline_winner == "opensquilla", "recommended_optimization": None if baseline_winner == "opensquilla" else case.optimization_if_not_better, } def apply_judge_result( row: dict[str, Any], judge_result: JudgeResult, case: ComparisonCase, ) -> dict[str, Any]: judge_result = normalize_weighted_judge_result(judge_result) winner = judge_result.winner updated = dict(row) updated["judge"] = asdict(judge_result) updated["winner"] = winner updated["score_basis"] = "llm_judge" updated["opensquilla_better"] = winner == "opensquilla" updated["recommended_optimization"] = ( None if winner == "opensquilla" else case.optimization_if_not_better ) updated.pop("judge_error", None) return updated async def judge_with_retries( judge: LLMJudge, case: ComparisonCase, opensquilla: EndpointResult, openclaw: EndpointResult, *, attempts: int = 3, ) -> JudgeResult: errors: list[str] = [] for attempt in range(1, attempts + 1): try: result = await judge.judge(case, opensquilla, openclaw) except Exception as exc: errors.append(f"attempt {attempt}: {type(exc).__name__}: {exc}") continue try: return normalize_weighted_judge_result(result) except ValueError as exc: errors.append(f"attempt {attempt}: {exc}") raise RuntimeError("; ".join(errors)) def normalize_weighted_judge_result(judge_result: JudgeResult) -> JudgeResult: if not judge_result.rationale.strip(): raise ValueError("judge response missing rationale") raw = judge_result.raw if isinstance(judge_result.raw, dict) else {} totals = weighted_judge_totals(raw) if totals is None: raise ValueError("judge response missing complete weighted subscores") winner = "tie" if totals["opensquilla"] > totals["openclaw"]: winner = "opensquilla" elif totals["openclaw"] > totals["opensquilla"]: winner = "openclaw" normalized_raw = dict(raw) normalized_raw["scores"] = totals normalized_raw["winner"] = winner normalized_raw["score_source"] = "weighted_subscores" return JudgeResult( winner=winner, scores=totals, confidence=judge_result.confidence, rationale=judge_result.rationale, risks=judge_result.risks, raw=normalized_raw, model=judge_result.model, ) def weighted_judge_totals(raw: dict[str, Any]) -> dict[str, int] | None: subscores = raw.get("subscores") if isinstance(raw.get("subscores"), dict) else {} totals: dict[str, int] = {} for label in ("opensquilla", "openclaw"): candidate = subscores.get(label) if not isinstance(candidate, dict): return None total = 0 for name, (low, high) in JUDGE_SUBSCORE_RANGES.items(): if name not in candidate: return None try: value = int(candidate[name]) except (TypeError, ValueError): return None if value < low or value > high: return None total += value totals[label] = total return totals def case_from_row(row: dict[str, Any]) -> ComparisonCase: case_data = row.get("case") if isinstance(row.get("case"), dict) else {} case_id = str(case_data.get("case_id", "")) for case in COMPARISON_CASES: if case.case_id == case_id: return case rubric_data = case_data.get("rubric") if isinstance(case_data.get("rubric"), list) else [] rubric = tuple( RubricCriterion( name=str(item.get("name", "")), description=str(item.get("description", "")), patterns=tuple(str(pattern) for pattern in item.get("patterns", ())), weight=int(item.get("weight", 1)), ) for item in rubric_data if isinstance(item, dict) ) return ComparisonCase( case_id=case_id, skill_name=str(case_data.get("skill_name", "")), prompt=str(case_data.get("prompt", "")), expected_advantage=str(case_data.get("expected_advantage", "")), optimization_if_not_better=str(case_data.get("optimization_if_not_better", "")), scenario=str(case_data.get("scenario", "primary")), rubric=rubric, failure_modes=tuple(str(item) for item in case_data.get("failure_modes", ())), ) def endpoint_from_row(row: dict[str, Any], endpoint: str) -> EndpointResult: data = row.get(endpoint) if isinstance(row.get(endpoint), dict) else {} return EndpointResult( endpoint=endpoint, case_id=str(data.get("case_id", row.get("case", {}).get("case_id", ""))), ok=bool(data.get("ok")), elapsed_s=float(data.get("elapsed_s", 0.0)), response_text=str(data.get("response_text", "")), score=data.get("score") if isinstance(data.get("score"), dict) else {"total": 0}, error=str(data["error"]) if data.get("error") is not None else None, session_key=str(data["session_key"]) if data.get("session_key") is not None else None, model=str(data["model"]) if data.get("model") is not None else None, provider=str(data["provider"]) if data.get("provider") is not None else None, event_count=int(data.get("event_count", 0)), ) def case_to_dict(case: ComparisonCase) -> dict[str, Any]: data = asdict(case) if not data["rubric"]: data["rubric"] = [asdict(item) for item in rubric_for_case(case)] return data def write_reports(rows: list[dict[str, Any]], stamp: str | None = None) -> None: REPORT_DIR.mkdir(parents=True, exist_ok=True) if stamp is None: stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ") jsonl_path = REPORT_DIR / f"openclaw_vs_opensquilla_meta_skill_{stamp}.jsonl" md_path = REPORT_DIR / f"openclaw_vs_opensquilla_meta_skill_{stamp}.md" prompts_path = REPORT_DIR / f"openclaw_vs_opensquilla_meta_skill_prompts_{stamp}.md" with jsonl_path.open("w", encoding="utf-8") as fh: for row in rows: fh.write(json.dumps(row, ensure_ascii=False) + "\n") md_path.write_text(render_markdown(rows, jsonl_path), encoding="utf-8") prompts_path.write_text(render_prompts_markdown(rows, jsonl_path), encoding="utf-8") print(f"wrote {jsonl_path}") print(f"wrote {md_path}") print(f"wrote {prompts_path}") def render_markdown(rows: list[dict[str, Any]], jsonl_path: Path) -> str: total = len(rows) sq_wins = sum(1 for row in rows if row["winner"] == "opensquilla") claw_wins = sum(1 for row in rows if row["winner"] == "openclaw") ties = sum(1 for row in rows if row["winner"] == "tie") judged = [row for row in rows if row.get("score_basis") == "llm_judge"] failed = [ row["case"]["case_id"] for row in rows if not row["opensquilla"]["ok"] or not row["openclaw"]["ok"] ] lines = [ "# OpenClaw vs OpenSquilla Meta-Skill Comparison", "", f"Raw JSONL: `{jsonl_path}`", "", "## Conclusion", "", ( f"OpenSquilla won {sq_wins}/{total} cases; OpenClaw won " f"{claw_wins}/{total}; ties: {ties}." ), ] if judged: lines.append(f"Final winner uses LLM judge for {len(judged)}/{total} rows.") else: lines.append( "Final winner uses deterministic rubric scoring; no LLM judge rows are present." ) if failed: lines.append(f"Cases with endpoint errors/timeouts: {', '.join(failed)}.") else: lines.append("No endpoint errors or timeouts were recorded.") if claw_wins or ties or failed: lines.append("Rows that do not show an OpenSquilla win include an optimization note.") else: lines.append("All completed cases favored OpenSquilla under this rubric.") lines.extend( [ "", "## Score Table", "", "| Case | OpenSquilla | OpenClaw | Baseline | Judge | Winner | Optimization |", "| --- | ---: | ---: | --- | --- | --- | --- |", ] ) for row in rows: opt = row["recommended_optimization"] or "" judge = row.get("judge") if isinstance(row.get("judge"), dict) else None judge_cell = "" if judge: scores = judge.get("scores") if isinstance(judge.get("scores"), dict) else {} judge_cell = ( f"{scores.get('opensquilla', '')}-{scores.get('openclaw', '')} " f"{judge.get('winner', '')}" ).strip() lines.append( "| {case} | {sq} | {claw} | {baseline} | {judge} | {winner} | {opt} |".format( case=row["case"]["case_id"], sq=row["opensquilla"]["score"]["total"], claw=row["openclaw"]["score"]["total"], baseline=row.get("baseline_winner", row.get("winner", "")), judge=judge_cell, winner=row["winner"], opt=opt.replace("|", "/"), ) ) lines.extend(["", "## Notes", ""]) for row in rows: lines.append(f"### {row['case']['case_id']}") lines.append("") lines.append("Prompt:") lines.append("") lines.append("```text") lines.append(row["case"]["prompt"]) lines.append("```") lines.append(f"- Expected advantage: {row['case']['expected_advantage']}") scenario = row["case"].get("scenario") if scenario: lines.append(f"- Scenario: {scenario}") rubric = row["case"].get("rubric") or [] if rubric: lines.append( "- Rubric: " + ", ".join( f"{item['name']}({item.get('weight', 1)})" for item in rubric if isinstance(item, dict) ) ) failure_modes = row["case"].get("failure_modes") or [] if failure_modes: lines.append("- Failure modes: " + "; ".join(failure_modes)) if row["recommended_optimization"]: lines.append(f"- Optimize: {row['recommended_optimization']}") lines.append(f"- Score basis: {row.get('score_basis', 'deterministic')}") if row.get("baseline_winner") and row.get("baseline_winner") != row.get("winner"): lines.append(f"- Baseline winner: {row['baseline_winner']}") judge = row.get("judge") if isinstance(row.get("judge"), dict) else None if judge: scores = judge.get("scores") if isinstance(judge.get("scores"), dict) else {} risks = judge.get("risks") if isinstance(judge.get("risks"), list) else [] lines.append( ( "- Judge: winner={winner}, scores={sq}-{claw}, " "confidence={confidence}, model={model}" ).format( winner=judge.get("winner"), sq=scores.get("opensquilla"), claw=scores.get("openclaw"), confidence=judge.get("confidence"), model=judge.get("model"), ) ) if judge.get("rationale"): lines.append(f"- Judge rationale: {judge['rationale']}") if risks: lines.append("- Judge risks: " + "; ".join(str(item) for item in risks)) if row.get("judge_error"): lines.append(f"- Judge error: {row['judge_error']}") for endpoint in ("opensquilla", "openclaw"): result = row[endpoint] error = f", error={result['error']}" if result["error"] else "" lines.append( f"- {endpoint}: ok={result['ok']}, elapsed={result['elapsed_s']}s, " f"events={result['event_count']}, provider={result['provider']}, " f"model={result['model']}{error}" ) lines.append("") return "\n".join(lines) def render_prompts_markdown(rows: list[dict[str, Any]], jsonl_path: Path) -> str: lines = [ "# OpenClaw vs OpenSquilla Meta-Skill Benchmark Prompts", "", f"Raw JSONL: `{jsonl_path}`", "", ] for row in rows: case = row["case"] lines.append(f"## {case['case_id']}") lines.append("") lines.append(f"- Meta-skill: `{case['skill_name']}`") lines.append(f"- Expected advantage: {case['expected_advantage']}") lines.append("") lines.append("```text") lines.append(case["prompt"]) lines.append("```") lines.append("") return "\n".join(lines) def _select_cases( case_arg: str, *, scenario: str | None = None, skill: str | None = None, ) -> list[ComparisonCase]: if case_arg == "all": selected = COMPARISON_CASES else: selected = [case for case in COMPARISON_CASES if case.case_id == case_arg] if not selected: valid = ", ".join(case.case_id for case in COMPARISON_CASES) raise SystemExit(f"Unknown case {case_arg!r}. Valid: {valid}") if scenario: selected = [case for case in selected if case.scenario == scenario] if skill: selected = [case for case in selected if case.skill_name == skill] if not selected: raise SystemExit("No comparison cases matched the requested filters.") return selected def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--run-live", action="store_true", help="Run both gateways.") parser.add_argument( "--judge-jsonl", help="Judge an existing comparison JSONL without rerunning both gateways.", ) parser.add_argument("--case", default="all", help="Case id or 'all'.") parser.add_argument( "--scenario", choices=["primary", "degraded", "boundary"], help="Optional scenario filter for case='all'.", ) parser.add_argument("--skill", help="Optional meta-skill name filter for case='all'.") parser.add_argument("--timeout", type=float, default=240.0) parser.add_argument("--opensquilla-url", default="ws://127.0.0.1:8081/ws") parser.add_argument("--opensquilla-token", default=read_opensquilla_token()) parser.add_argument( "--opensquilla-elevated", default="bypass", choices=["off", "on", "bypass", "full"], help="Gateway elevated mode for OpenSquilla tool calls.", ) parser.add_argument("--openclaw-url", default="ws://127.0.0.1:18789/ws") parser.add_argument("--openclaw-config", default=os.environ.get("OPENCLAW_CONFIG")) parser.add_argument("--openclaw-idle-timeout", type=float, default=90.0) parser.add_argument( "--judge-llm", action="store_true", help="Use an LLM judge for the final winner after deterministic scoring.", ) parser.add_argument( "--judge-model", default=os.environ.get("OPENSQUILLA_JUDGE_MODEL"), help="OpenRouter model id for --judge-llm.", ) parser.add_argument("--judge-api-key", default=read_judge_api_key()) parser.add_argument( "--judge-base-url", default=os.environ.get("OPENSQUILLA_JUDGE_BASE_URL", "https://openrouter.ai/api/v1"), ) parser.add_argument("--judge-timeout", type=float, default=120.0) return parser.parse_args() def main() -> None: args = parse_args() if args.judge_jsonl: asyncio.run(judge_existing(args)) return if not args.run_live: for case in _select_cases(args.case, scenario=args.scenario, skill=args.skill): print(json.dumps(case_to_dict(case), indent=2)) return asyncio.run(run_live(args)) if __name__ == "__main__": main()