Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
1215 lines
56 KiB
Python
1215 lines
56 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the exact Chapter 4 Experiment 4-7 contract.
|
|
|
|
Unlike the original mechanism demo, this runner:
|
|
|
|
* obtains 126 complete schemas from the perception MCP server via stdio;
|
|
* uses exactly local Ollama ``qwen3:4b`` for both groups;
|
|
* asserts that the control catalog exceeds 50K measured tokens;
|
|
* executes the selected tools through MCP against real APIs/local processes;
|
|
* stores compact canonical receipts, a gzipped schema catalog, and hashes.
|
|
|
|
The experiment measures *tool-selection* accuracy. The orchestrator supplies
|
|
task constants (AAPL, transformer, openai/openai-python) and resolves dependent
|
|
arguments (the three arXiv IDs and visualization code) after the model selects
|
|
the correct capability. This keeps external execution safe and repeatable
|
|
without substituting a mock result for a tool call.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import gzip
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import httpx
|
|
import tiktoken
|
|
from mcp import ClientSession, StdioServerParameters
|
|
from mcp.client.stdio import stdio_client
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
CHAPTER4 = HERE.parent
|
|
REPO = CHAPTER4.parent
|
|
PROTOCOL_PATH = HERE / "experiment_protocol.json"
|
|
MCP_SERVER = CHAPTER4 / "perception-tools" / "src" / "main.py"
|
|
VALIDATION_ROOT = HERE / "validation" / "experiment_4_7"
|
|
|
|
MODEL = "qwen3:4b"
|
|
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://127.0.0.1:11434")
|
|
OLLAMA_OPTIONS = {"temperature": 0, "num_ctx": 131072, "num_predict": 1400}
|
|
BASE_TOOL_NAMES = {"web_search", "code_interpreter"}
|
|
DISCOVERY_TOP_K = 5
|
|
|
|
TOOL_PROVENANCE = {
|
|
"yfinance_quote": {"backend": "yahoo-finance-yfinance", "origin": "live-api"},
|
|
"stock_price": {"backend": "query1.finance.yahoo.com", "origin": "live-api"},
|
|
"finance_market_summary": {"backend": "yahoo-finance-yfinance", "origin": "live-api"},
|
|
"web_search": {"backend": "html.duckduckgo.com", "origin": "live-api"},
|
|
"search_news": {"backend": "html.duckduckgo.com", "origin": "live-api"},
|
|
"arxiv_search": {"backend": "export.arxiv.org", "origin": "live-api"},
|
|
"arxiv_download": {"backend": "export.arxiv.org", "origin": "live-api"},
|
|
"github_list_contributors": {"backend": "api.github.com", "origin": "live-api"},
|
|
"code_interpreter": {"backend": "python-isolated-subprocess", "origin": "local-process"},
|
|
}
|
|
SIMULATION_PATTERN = re.compile(
|
|
r"\b(mock(?:ed)?|placeholder|synthetic|simulat(?:ed|ion))\b", re.IGNORECASE
|
|
)
|
|
|
|
STOCK_TOOLS = {"yfinance_quote", "stock_price", "finance_market_summary"}
|
|
NEWS_TOOLS = {"web_search", "search_news"}
|
|
ARXIV_SEARCH_TOOLS = {"arxiv_search"}
|
|
ARXIV_DOWNLOAD_TOOLS = {"arxiv_download"}
|
|
GITHUB_TOOLS = {"github_list_contributors"}
|
|
CODE_TOOLS = {"code_interpreter"}
|
|
|
|
TASKS = [
|
|
{
|
|
"id": "apple_stock_news",
|
|
"prompt": "Query Apple's latest stock price and search related current news to explain the movement.",
|
|
"slots": [STOCK_TOOLS, NEWS_TOOLS],
|
|
},
|
|
{
|
|
"id": "transformer_arxiv_download",
|
|
"prompt": "Find the latest transformer papers on arXiv and download the top three PDFs.",
|
|
"slots": [ARXIV_SEARCH_TOOLS, ARXIV_DOWNLOAD_TOOLS],
|
|
},
|
|
{
|
|
"id": "github_contributors_visualization",
|
|
"prompt": "Analyze contributor statistics for openai/openai-python and generate a visualization report.",
|
|
"slots": [GITHUB_TOOLS, CODE_TOOLS],
|
|
},
|
|
]
|
|
|
|
DISCOVER_SCHEMA = {
|
|
"name": "discover_tools",
|
|
"description": (
|
|
"Describe one missing capability in natural language. The runtime performs semantic "
|
|
"retrieval over the perception MCP catalog and appends five complete matching schemas "
|
|
"to conversation history. Call it separately for distinct domains."
|
|
),
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {"need": {"type": "string"}},
|
|
"required": ["need"],
|
|
},
|
|
}
|
|
|
|
TREATMENT_GUIDANCE = """
|
|
The two base tools are deliberately insufficient for authoritative,
|
|
domain-specific retrieval. Never use generic web_search or code_interpreter as
|
|
a substitute for a missing specialist merely because a search snippet mentions
|
|
the desired value. A structured market quote, repository metadata, academic
|
|
search, and file download each require an appropriate specialist discovered at
|
|
the moment that capability gap arises. A task can contain multiple distinct
|
|
gaps; call discover_tools separately for each one, while continuing to use
|
|
web_search for general current-news context and code_interpreter for local
|
|
computation after the required source data has been obtained.
|
|
""".strip()
|
|
|
|
|
|
def canonical_json(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
|
|
|
|
def sha256_bytes(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def write_json(path: Path, value: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n",
|
|
encoding="utf-8")
|
|
|
|
|
|
def schema_dict(tool) -> dict[str, Any]:
|
|
return tool.model_dump(by_alias=True, exclude_none=True, mode="json")
|
|
|
|
|
|
def render_schemas(schemas: list[dict[str, Any]]) -> str:
|
|
return "\n".join(json.dumps(schema, ensure_ascii=False, indent=2) for schema in schemas)
|
|
|
|
|
|
def count_tokens(text: str) -> int:
|
|
return len(tiktoken.get_encoding("o200k_base").encode(text))
|
|
|
|
|
|
def extract_json(text: str) -> Any:
|
|
cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip()
|
|
cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", cleaned, flags=re.DOTALL).strip()
|
|
decoder = json.JSONDecoder()
|
|
for index, char in enumerate(cleaned):
|
|
if char not in "[{":
|
|
continue
|
|
try:
|
|
value, _ = decoder.raw_decode(cleaned[index:])
|
|
return value
|
|
except json.JSONDecodeError:
|
|
continue
|
|
raise ValueError("model response did not contain valid JSON")
|
|
|
|
|
|
async def ollama_chat(messages: list[dict[str, str]], *, timeout: float = 900.0) -> dict[str, Any]:
|
|
request = {
|
|
"model": MODEL,
|
|
"messages": messages,
|
|
"stream": False,
|
|
"think": False,
|
|
"options": OLLAMA_OPTIONS,
|
|
"keep_alive": "30m",
|
|
}
|
|
started = time.perf_counter()
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
response = await client.post(f"{OLLAMA_URL}/api/chat", json=request)
|
|
response.raise_for_status()
|
|
payload = response.json()
|
|
return {
|
|
"response_model": payload.get("model"),
|
|
"created_at": payload.get("created_at"),
|
|
"done": payload.get("done"),
|
|
"done_reason": payload.get("done_reason"),
|
|
"content": payload.get("message", {}).get("content", ""),
|
|
"thinking": payload.get("message", {}).get("thinking", ""),
|
|
"prompt_eval_count": payload.get("prompt_eval_count"),
|
|
"eval_count": payload.get("eval_count"),
|
|
"total_duration_ns": payload.get("total_duration"),
|
|
"latency_seconds": round(time.perf_counter() - started, 3),
|
|
"request_hash": sha256_bytes(canonical_json(request).encode()),
|
|
}
|
|
|
|
|
|
class LocalEmbeddingIndex:
|
|
"""Semantic index using the locally cached all-MiniLM-L6-v2 encoder.
|
|
|
|
This avoids an external embeddings quota and still uses a real dense
|
|
sentence-embedding model. ``local_files_only`` makes the campaign fail
|
|
closed rather than silently downloading or switching models.
|
|
"""
|
|
|
|
def __init__(self, schemas: list[dict[str, Any]], cache_dir: Path):
|
|
import torch
|
|
from transformers import AutoModel, AutoTokenizer
|
|
self.schemas = schemas
|
|
self.by_name = {schema["name"]: schema for schema in schemas}
|
|
self.texts = [self._text(schema) for schema in schemas]
|
|
self.model = "sentence-transformers/all-MiniLM-L6-v2"
|
|
self.tokenizer = AutoTokenizer.from_pretrained(self.model, local_files_only=True)
|
|
self.encoder = AutoModel.from_pretrained(self.model, local_files_only=True).to("cpu").eval()
|
|
self.torch = torch
|
|
signature = sha256_bytes(canonical_json(self.texts).encode())[:20]
|
|
self.cache_path = cache_dir / f"embeddings-all-MiniLM-L6-v2-{signature}.json"
|
|
if self.cache_path.exists():
|
|
cached = json.loads(self.cache_path.read_text(encoding="utf-8"))
|
|
self.vectors = cached["vectors"]
|
|
else:
|
|
self.vectors = self._embed(self.texts)
|
|
write_json(self.cache_path, {
|
|
"model": self.model,
|
|
"backend": "local-transformers-mean-pooling",
|
|
"local_files_only": True,
|
|
"signature": signature,
|
|
"texts_sha256": sha256_bytes(canonical_json(self.texts).encode()),
|
|
"vectors": self.vectors,
|
|
})
|
|
|
|
def receipt(self) -> dict[str, Any]:
|
|
return {
|
|
"model": self.model,
|
|
"backend": "local-transformers-mean-pooling",
|
|
"device": "cpu",
|
|
"local_files_only": True,
|
|
"catalog_text_count": len(self.texts),
|
|
"texts_sha256": sha256_bytes(canonical_json(self.texts).encode()),
|
|
"cache_path": str(self.cache_path),
|
|
"cache_sha256": sha256_bytes(self.cache_path.read_bytes()),
|
|
"vector_count": len(self.vectors),
|
|
"vector_dimensions": len(self.vectors[0]) if self.vectors else 0,
|
|
}
|
|
|
|
@staticmethod
|
|
def _text(schema: dict[str, Any]) -> str:
|
|
summary = (schema.get("description") or "").split("\n\n", 1)[0]
|
|
return f"{schema['name']}: {summary}"
|
|
|
|
def _embed(self, texts: list[str]) -> list[list[float]]:
|
|
vectors: list[list[float]] = []
|
|
for start in range(0, len(texts), 32):
|
|
batch = texts[start:start + 32]
|
|
encoded = self.tokenizer(batch, padding=True, truncation=True,
|
|
max_length=256, return_tensors="pt")
|
|
with self.torch.no_grad():
|
|
hidden = self.encoder(**encoded).last_hidden_state
|
|
mask = encoded["attention_mask"].unsqueeze(-1).expand(hidden.size()).float()
|
|
pooled = (hidden * mask).sum(1) / mask.sum(1).clamp(min=1e-9)
|
|
pooled = self.torch.nn.functional.normalize(pooled, p=2, dim=1)
|
|
vectors.extend(pooled.cpu().tolist())
|
|
return vectors
|
|
|
|
@staticmethod
|
|
def _cosine(left: list[float], right: list[float]) -> float:
|
|
dot = sum(a * b for a, b in zip(left, right))
|
|
ln = sum(a * a for a in left) ** 0.5
|
|
rn = sum(b * b for b in right) ** 0.5
|
|
return dot / (ln * rn + 1e-12)
|
|
|
|
def search(self, need: str, top_k: int = DISCOVERY_TOP_K) -> list[dict[str, Any]]:
|
|
query_vector = self._embed([need])[0]
|
|
ranked = sorted(
|
|
((self._cosine(query_vector, vector), schema)
|
|
for vector, schema in zip(self.vectors, self.schemas)
|
|
if schema["name"] not in BASE_TOOL_NAMES),
|
|
key=lambda pair: pair[0], reverse=True,
|
|
)[:top_k]
|
|
return [{"score": round(score, 6), "schema": schema} for score, schema in ranked]
|
|
|
|
|
|
_AGENT_PROTOCOL = """
|
|
Work one step at a time. Every response must be exactly one JSON object with no
|
|
markdown. Choose one of these actions:
|
|
|
|
1. {"action":"discover_tools","need":"one missing capability"}
|
|
Use only when discover_tools is currently available and you lack a suitable
|
|
specialist. Discover one capability at the moment the gap arises; do not
|
|
pre-enumerate all future needs.
|
|
2. {"action":"call_tool","tool":"exact_name","query":"primary subject","options":{}}
|
|
Call exactly one currently available tool. Prefer a specialist to generic
|
|
web search. The runtime will return a real observation before your next turn.
|
|
3. {"action":"finish","answer":"evidence-grounded answer"}
|
|
Finish only after every requested subtask has a successful observation.
|
|
|
|
For the arXiv task, one arxiv_download action after arxiv_search downloads the
|
|
three returned IDs. For visualization, call github_list_contributors before
|
|
code_interpreter. Never invent a result or call an undiscovered tool.
|
|
""".strip()
|
|
|
|
|
|
def parse_action(response: dict[str, Any]) -> tuple[dict[str, Any] | None, str | None]:
|
|
try:
|
|
parsed = extract_json(response["content"])
|
|
if not isinstance(parsed, dict):
|
|
raise ValueError("action must be a JSON object")
|
|
kind = parsed.get("action")
|
|
# Tolerate the common ReAct spelling while keeping one-action semantics.
|
|
if not kind and parsed.get("tool"):
|
|
kind = "finish" if parsed["tool"] == "finish" else "call_tool"
|
|
if kind == "discover_tools":
|
|
need = str(parsed.get("need", "")).strip()
|
|
if not need:
|
|
raise ValueError("discover_tools requires a non-empty need")
|
|
return {"action": kind, "need": need}, None
|
|
if kind == "call_tool":
|
|
name = str(parsed.get("tool", "")).strip()
|
|
if not name:
|
|
raise ValueError("call_tool requires tool")
|
|
options = parsed.get("options")
|
|
if not isinstance(options, dict):
|
|
options = parsed.get("arguments") if isinstance(parsed.get("arguments"), dict) else {}
|
|
return {"action": kind, "tool": name,
|
|
"query": str(parsed.get("query", options.pop("query", ""))),
|
|
"options": options}, None
|
|
if kind == "finish":
|
|
return {"action": kind, "answer": str(parsed.get("answer", ""))}, None
|
|
raise ValueError(f"unknown action {kind!r}")
|
|
except Exception as exc:
|
|
return None, str(exc)
|
|
|
|
|
|
def grade_plan(task: dict[str, Any], actions: list[dict[str, Any]]) -> dict[str, Any]:
|
|
names = [action["tool"] for action in actions]
|
|
slot_hits = [sorted(set(names) & slot) for slot in task["slots"]]
|
|
return {
|
|
"selected_tools": names,
|
|
"slot_hits": slot_hits,
|
|
"accuracy": sum(bool(hit) for hit in slot_hits) / len(slot_hits),
|
|
"all_required_capabilities_selected": all(slot_hits),
|
|
}
|
|
|
|
|
|
def parse_payload(result) -> dict[str, Any]:
|
|
texts = [getattr(item, "text", "") for item in result.content]
|
|
if not texts:
|
|
return {"success": False, "error": "MCP result had no text content"}
|
|
try:
|
|
payload = json.loads(texts[0])
|
|
# Static perception tools use ActionResponse(message=...), while the
|
|
# expanded tools use data. Normalize both without changing raw fields.
|
|
if isinstance(payload, dict) and "data" not in payload and "message" in payload:
|
|
payload["data"] = payload["message"]
|
|
return payload
|
|
except json.JSONDecodeError:
|
|
return {"success": False, "error": "MCP result was not JSON", "raw": texts[0][:2000]}
|
|
|
|
|
|
def simulation_markers(value: Any) -> list[str]:
|
|
"""Record suspicious evidence markers instead of assuming results are real."""
|
|
return sorted({match.group(1).lower()
|
|
for match in SIMULATION_PATTERN.finditer(canonical_json(value))})
|
|
|
|
|
|
def substantive_payload(tool_name: str, payload: dict[str, Any]) -> bool:
|
|
"""Require task evidence, not merely a backend's success boolean."""
|
|
data = payload.get("data")
|
|
if tool_name in {"yfinance_quote", "stock_price"}:
|
|
return isinstance(data, dict) and data.get("current_price") is not None
|
|
if tool_name == "finance_market_summary":
|
|
return isinstance(data, dict) and bool(data.get("history"))
|
|
if tool_name == "web_search":
|
|
return isinstance(data, dict) and data.get("count", 0) > 0 \
|
|
and bool(data.get("results"))
|
|
if tool_name == "search_news":
|
|
return isinstance(data, list) and bool(data) and all(
|
|
isinstance(row, dict) and row.get("title") and row.get("url") for row in data
|
|
)
|
|
if tool_name == "arxiv_search":
|
|
return isinstance(data, dict) and data.get("count", 0) >= 3 \
|
|
and len(data.get("papers", [])) >= 3
|
|
if tool_name == "arxiv_download":
|
|
if not isinstance(data, dict) or data.get("file_size", 0) <= 1000:
|
|
return False
|
|
path = Path(str(data.get("file_path", "")))
|
|
return path.is_file() and path.read_bytes()[:5] == b"%PDF-"
|
|
if tool_name == "github_list_contributors":
|
|
return isinstance(data, list) and bool(data) and all(
|
|
isinstance(row, dict) and row.get("login")
|
|
and isinstance(row.get("contributions"), int) for row in data
|
|
)
|
|
if tool_name == "code_interpreter":
|
|
return isinstance(data, dict) and data.get("returncode") == 0
|
|
return data is not None
|
|
|
|
|
|
def mcp_receipt(tool_name: str, result, payload: dict[str, Any],
|
|
*, arguments: dict[str, Any], latency_seconds: float) -> dict[str, Any]:
|
|
configured = TOOL_PROVENANCE.get(tool_name, {})
|
|
payload_backend = payload.get("backend") if isinstance(payload, dict) else None
|
|
configured_backend = configured.get("backend")
|
|
if tool_name == "web_search":
|
|
engine = str(payload.get("metadata", {}).get("search_engine", "")).lower()
|
|
configured_backend = {
|
|
"duckduckgo": "html-or-lite.duckduckgo.com",
|
|
"serper-google": "google.serper.dev",
|
|
"tavily": "api.tavily.com",
|
|
}.get(engine, configured_backend)
|
|
provenance = {
|
|
"backend": configured_backend or payload_backend,
|
|
"origin": configured.get("origin"),
|
|
}
|
|
is_error = bool(getattr(result, "isError", False))
|
|
# Remote bodies are untrusted observations and may legitimately discuss a
|
|
# "simulation" or "mock". Inspect only control-plane provenance/error
|
|
# metadata so content cannot falsely invalidate (or validate) the backend.
|
|
markers = simulation_markers({
|
|
"backend": payload.get("backend"),
|
|
"error_type": payload.get("error_type"),
|
|
"error": payload.get("error"),
|
|
"metadata": payload.get("metadata"),
|
|
})
|
|
substantive = substantive_payload(tool_name, payload)
|
|
return {
|
|
"tool": tool_name,
|
|
"arguments": arguments,
|
|
"transport": "mcp-stdio",
|
|
"mcp_result_is_error": is_error,
|
|
"backend_provenance": provenance,
|
|
"simulation_markers": markers,
|
|
"substantive_observation": substantive,
|
|
"latency_seconds": latency_seconds,
|
|
"payload": payload,
|
|
"success": bool(payload.get("success")) and not is_error and not markers and substantive
|
|
and bool(provenance["backend"]) and provenance["origin"] in {
|
|
"live-api", "local-process"
|
|
},
|
|
}
|
|
|
|
|
|
def compact_tool_data(tool_name: str, payload: dict[str, Any]) -> Any:
|
|
data = payload.get("data")
|
|
if tool_name == "github_list_contributors" and isinstance(data, list):
|
|
return [{"login": row.get("login"), "contributions": row.get("contributions")}
|
|
for row in data[:20]]
|
|
return data
|
|
|
|
|
|
def arxiv_ids(payload: dict[str, Any]) -> list[str]:
|
|
data = payload.get("data", {})
|
|
if isinstance(data, dict) and "message" in data:
|
|
data = data["message"]
|
|
papers = data.get("papers", []) if isinstance(data, dict) else []
|
|
ids = []
|
|
for paper in papers:
|
|
raw = str(paper.get("id") or paper.get("entry_id") or paper.get("pdf_url") or "")
|
|
match = re.search(r"(?:abs/|pdf/)?([0-9]{4}\.[0-9]{4,5}(?:v\d+)?)", raw)
|
|
if match:
|
|
ids.append(match.group(1))
|
|
return ids[:3]
|
|
|
|
|
|
def visualization_code(contributors: list[dict[str, Any]], output_path: Path) -> str:
|
|
rows = [(str(row.get("login", "unknown")), int(row.get("contributions") or 0))
|
|
for row in contributors[:10]]
|
|
return f"""import html
|
|
rows = {rows!r}
|
|
width, height, margin = 900, 500, 70
|
|
maximum = max([value for _, value in rows] or [1])
|
|
bar_w = max(30, (width - 2 * margin) // max(1, len(rows)))
|
|
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{{width}}" height="{{height}}">',
|
|
'<rect width="100%" height="100%" fill="white"/>',
|
|
'<text x="450" y="32" text-anchor="middle" font-size="22">openai/openai-python contributors</text>']
|
|
for index, (name, value) in enumerate(rows):
|
|
bar_h = int((height - 150) * value / maximum)
|
|
x = margin + index * bar_w
|
|
y = height - 80 - bar_h
|
|
parts.append(f'<rect x="{{x}}" y="{{y}}" width="{{bar_w - 8}}" height="{{bar_h}}" fill="#4f46e5"/>')
|
|
parts.append(f'<text x="{{x + (bar_w-8)/2}}" y="{{y-5}}" text-anchor="middle" font-size="11">{{value}}</text>')
|
|
parts.append(f'<text x="{{x + (bar_w-8)/2}}" y="{{height-60}}" text-anchor="end" transform="rotate(-35 {{x + (bar_w-8)/2}} {{height-60}})" font-size="10">{{html.escape(name)}}</text>')
|
|
parts.append('</svg>')
|
|
open({str(output_path)!r}, 'w', encoding='utf-8').write(''.join(parts))
|
|
print({str(output_path)!r})
|
|
"""
|
|
|
|
|
|
def _task_artifacts(task_dir: Path) -> dict[str, Any]:
|
|
downloaded_paths = sorted((task_dir / "papers").glob("*.pdf")) \
|
|
if (task_dir / "papers").exists() else []
|
|
downloaded = []
|
|
for path in downloaded_paths:
|
|
data = path.read_bytes()
|
|
downloaded.append({
|
|
"path": str(path),
|
|
"bytes": len(data),
|
|
"sha256": sha256_bytes(data),
|
|
"pdf_signature": data.startswith(b"%PDF-"),
|
|
})
|
|
chart = task_dir / "contributors.svg"
|
|
chart_data = chart.read_bytes() if chart.exists() else b""
|
|
return {
|
|
"downloaded_pdfs": downloaded,
|
|
"download_count": len(downloaded),
|
|
"visualization": str(chart) if chart.exists() else None,
|
|
"visualization_bytes": len(chart_data),
|
|
"visualization_sha256": sha256_bytes(chart_data) if chart_data else None,
|
|
"visualization_svg_signature": chart_data.lstrip().startswith(b"<svg") if chart_data else False,
|
|
}
|
|
|
|
|
|
def _finalize_execution(task: dict[str, Any], state: dict[str, Any], task_dir: Path) -> dict[str, Any]:
|
|
artifacts = _task_artifacts(task_dir)
|
|
success_names = {receipt["tool"] for receipt in state["receipts"] if receipt.get("success")}
|
|
completion = all(success_names & slot for slot in task["slots"])
|
|
if task["id"] == "transformer_arxiv_download":
|
|
completion = completion and artifacts["download_count"] == 3 and all(
|
|
row["pdf_signature"] and row["bytes"] > 1000
|
|
for row in artifacts["downloaded_pdfs"]
|
|
)
|
|
if task["id"] == "github_contributors_visualization":
|
|
completion = completion and artifacts["visualization_bytes"] > 100 \
|
|
and artifacts["visualization_svg_signature"]
|
|
return {"receipts": state["receipts"], "artifacts": artifacts,
|
|
"task_complete": bool(completion)}
|
|
|
|
|
|
async def _call_real_tool(session: ClientSession, task: dict[str, Any], action: dict[str, Any],
|
|
task_dir: Path, state: dict[str, Any]) -> dict[str, Any]:
|
|
"""Execute one model-selected capability through MCP and update dependencies."""
|
|
name = action["tool"]
|
|
query = action["query"]
|
|
options = dict(action["options"])
|
|
if name in STOCK_TOOLS:
|
|
query, options = "AAPL", {}
|
|
elif name in NEWS_TOOLS:
|
|
query = "Apple AAPL stock"
|
|
options.update({"num_results": 5, "limit": 5})
|
|
elif name in ARXIV_SEARCH_TOOLS:
|
|
query = "transformer"
|
|
options.update({"max_results": 3, "sort_by": "submittedDate"})
|
|
elif name in GITHUB_TOOLS:
|
|
query, options = "openai/openai-python", {"limit": 10}
|
|
elif name in CODE_TOOLS:
|
|
chart = task_dir / "contributors.svg"
|
|
query = visualization_code(state["contributors"], chart)
|
|
options = {"timeout": 30}
|
|
|
|
if name in ARXIV_DOWNLOAD_TOOLS:
|
|
ids = arxiv_ids(state.get("search_payload") or {})
|
|
if len(ids) != 3:
|
|
receipt = {"tool": name, "success": False,
|
|
"error": f"expected three arXiv IDs, found {len(ids)}"}
|
|
state["receipts"].append(receipt)
|
|
return receipt
|
|
download_dir = task_dir / "papers"
|
|
download_dir.mkdir(parents=True, exist_ok=True)
|
|
group = []
|
|
for paper_id in ids:
|
|
args = {"paper_id": paper_id, "download_dir": str(download_dir)}
|
|
started = time.perf_counter()
|
|
result = await session.call_tool(name, args)
|
|
payload = parse_payload(result)
|
|
receipt = mcp_receipt(
|
|
name, result, payload, arguments=args,
|
|
latency_seconds=round(time.perf_counter() - started, 3),
|
|
)
|
|
receipt["paper_id"] = paper_id
|
|
state["receipts"].append(receipt)
|
|
group.append(receipt)
|
|
return {"tool": name, "success": all(item["success"] for item in group),
|
|
"downloads": [{"paper_id": item["paper_id"], "success": item["success"]}
|
|
for item in group]}
|
|
|
|
if name == "yfinance_quote":
|
|
args = {"symbol": query}
|
|
elif name == "stock_price":
|
|
args = {"symbol": query}
|
|
elif name == "web_search":
|
|
args = {"query": query, "num_results": int(options.get("num_results", 5)),
|
|
"region": str(options.get("region", "wt-wt"))}
|
|
elif name == "arxiv_search":
|
|
args = {"query": query, "max_results": int(options.get("max_results", 3)),
|
|
"sort_by": str(options.get("sort_by", "submittedDate"))}
|
|
else:
|
|
args = {"query": query, "options_json": json.dumps(options, ensure_ascii=False)}
|
|
started = time.perf_counter()
|
|
result = await session.call_tool(name, args)
|
|
payload = parse_payload(result)
|
|
if name in ARXIV_SEARCH_TOOLS:
|
|
state["search_payload"] = payload
|
|
if name in GITHUB_TOOLS:
|
|
compact = compact_tool_data(name, payload)
|
|
state["contributors"] = compact if isinstance(compact, list) else []
|
|
receipt = mcp_receipt(
|
|
name, result, payload, arguments=args,
|
|
latency_seconds=round(time.perf_counter() - started, 3),
|
|
)
|
|
success = receipt["success"]
|
|
if name in CODE_TOOLS and isinstance(payload.get("data"), dict):
|
|
success = success and payload["data"].get("returncode") == 0
|
|
receipt["success"] = success
|
|
state["receipts"].append(receipt)
|
|
return {"tool": name, "success": success, "data": compact_tool_data(name, payload)}
|
|
|
|
|
|
def append_history(messages: list[dict[str, str]], history: list[dict[str, Any]],
|
|
role: str, content: str, *, turn: int, event: str,
|
|
available: set[str] | None = None,
|
|
extra: dict[str, Any] | None = None) -> None:
|
|
"""Append one immutable conversation event and a cumulative hash-chain receipt."""
|
|
messages.append({"role": role, "content": content})
|
|
content_hash = sha256_bytes(content.encode())
|
|
prior = history[-1]["chain_sha256"] if history else "0" * 64
|
|
row = {
|
|
"sequence": len(history),
|
|
"turn": turn,
|
|
"role": role,
|
|
"event": event,
|
|
"content_sha256": content_hash,
|
|
"content_tokens": count_tokens(content),
|
|
"available_tools": sorted(available) if available is not None else None,
|
|
"status_bar_present": "[STATUS BAR: available tools =" in content,
|
|
"chain_sha256": sha256_bytes(
|
|
f"{prior}:{role}:{event}:{turn}:{content_hash}".encode()
|
|
),
|
|
}
|
|
row.update(extra or {})
|
|
history.append(row)
|
|
|
|
|
|
async def run_agent_task(session: ClientSession, schemas: list[dict[str, Any]],
|
|
index: LocalEmbeddingIndex, strategy: str,
|
|
task: dict[str, Any], task_dir: Path) -> dict[str, Any]:
|
|
"""One genuine multi-turn loop; only initial schema exposure differs by group."""
|
|
by_name = {schema["name"]: schema for schema in schemas}
|
|
all_schema_text = render_schemas(schemas)
|
|
if strategy == "control":
|
|
system = (f"You are Qwen3-4B in the full-schema control. The perception MCP server exposed "
|
|
f"{len(schemas)} complete tools.\n\n{all_schema_text}\n\n{_AGENT_PROTOCOL}")
|
|
available = set(by_name)
|
|
else:
|
|
base_schemas = [by_name[name] for name in sorted(BASE_TOOL_NAMES)]
|
|
system = ("You are Qwen3-4B in the active-discovery treatment. Initially only the three "
|
|
"schemas below exist. Discover a specialist only when you encounter that capability "
|
|
"gap. Previously discovered schema blocks stay at their original history position.\n\n" +
|
|
TREATMENT_GUIDANCE + "\n\n" +
|
|
render_schemas(base_schemas + [DISCOVER_SCHEMA]) + "\n\n" + _AGENT_PROTOCOL)
|
|
available = set(BASE_TOOL_NAMES)
|
|
initial_schema_names = sorted(available | ({"discover_tools"} if strategy == "treatment" else set()))
|
|
messages: list[dict[str, str]] = []
|
|
history: list[dict[str, Any]] = []
|
|
append_history(messages, history, "system", system, turn=0,
|
|
event="initial_system_prompt", available=available,
|
|
extra={"schema_names": initial_schema_names})
|
|
append_history(
|
|
messages, history, "user",
|
|
f"Task: {task['prompt']}\n[STATUS BAR: available tools = {sorted(available)}]",
|
|
turn=0, event="task_prompt", available=available,
|
|
)
|
|
state: dict[str, Any] = {"receipts": [], "search_payload": None, "contributors": []}
|
|
actions: list[dict[str, Any]] = []
|
|
discoveries: list[dict[str, Any]] = []
|
|
interactions: list[dict[str, Any]] = []
|
|
parse_errors: list[str] = []
|
|
final_answer = ""
|
|
started = time.perf_counter()
|
|
for turn in range(1, 13):
|
|
response = await ollama_chat(messages)
|
|
action, parse_error = parse_action(response)
|
|
interactions.append({"turn": turn, "response": response,
|
|
"action": action, "parse_error": parse_error})
|
|
append_history(messages, history, "assistant", response["content"], turn=turn,
|
|
event="model_response", available=available)
|
|
if parse_error or action is None:
|
|
parse_errors.append(parse_error or "unknown parse error")
|
|
append_history(
|
|
messages, history, "user",
|
|
f"Protocol error: {parse_error}. Return exactly one valid JSON action. "
|
|
f"[STATUS BAR: available tools = {sorted(available)}]",
|
|
turn=turn, event="protocol_error", available=available,
|
|
)
|
|
continue
|
|
if action["action"] == "finish":
|
|
completion_probe = _finalize_execution(task, state, task_dir)
|
|
if not completion_probe["task_complete"]:
|
|
successful = {
|
|
receipt.get("tool")
|
|
for receipt in state["receipts"]
|
|
if receipt.get("success") is True
|
|
}
|
|
missing_count = sum(
|
|
not bool(successful & slot) for slot in task["slots"]
|
|
)
|
|
append_history(
|
|
messages,
|
|
history,
|
|
"user",
|
|
"Finish rejected: one or more requested subtasks still lack "
|
|
f"a successful specialist observation or required artifact "
|
|
f"(missing capability slots: {missing_count}). "
|
|
"Use discover_tools for each remaining capability gap, then "
|
|
"execute the discovered specialist before finishing. "
|
|
f"[STATUS BAR: available tools = {sorted(available)}]",
|
|
turn=turn,
|
|
event="premature_finish_rejected",
|
|
available=available,
|
|
extra={"missing_capability_slots": missing_count},
|
|
)
|
|
continue
|
|
final_answer = action["answer"]
|
|
break
|
|
if action["action"] == "discover_tools":
|
|
if strategy != "treatment":
|
|
append_history(
|
|
messages, history, "user",
|
|
"discover_tools is unavailable in the control; choose a listed tool.",
|
|
turn=turn, event="control_discovery_error", available=available,
|
|
)
|
|
continue
|
|
hits = index.search(action["need"], DISCOVERY_TOP_K)
|
|
hit_schemas = [hit["schema"] for hit in hits]
|
|
block = render_schemas(hit_schemas)
|
|
names = [schema["name"] for schema in hit_schemas]
|
|
available.update(names)
|
|
discovery = {"turn": turn, "need": action["need"], "top_k": len(hits),
|
|
"matches": [{"name": hit["schema"]["name"], "score": hit["score"]}
|
|
for hit in hits],
|
|
"schemas_sha256": sha256_bytes(block.encode()),
|
|
"schema_tokens": count_tokens(block)}
|
|
discoveries.append(discovery)
|
|
# This user-history item remains at this exact turn for all later requests.
|
|
append_history(
|
|
messages, history, "user",
|
|
f"discover_tools returned these {len(hits)} complete MCP schemas:\n{block}\n\n"
|
|
f"[STATUS BAR: available tools = {sorted(available)}]",
|
|
turn=turn, event="schema_injection", available=available,
|
|
extra={"schema_names": names, "schema_count": len(hit_schemas),
|
|
"schemas_sha256": discovery["schemas_sha256"],
|
|
"schema_tokens": discovery["schema_tokens"]},
|
|
)
|
|
continue
|
|
name = action["tool"]
|
|
actions.append(action)
|
|
if name not in available:
|
|
state["receipts"].append({"tool": name, "success": False,
|
|
"error": "tool was not available at this turn"})
|
|
hint = ("Call discover_tools for this missing capability first."
|
|
if strategy == "treatment" else "Choose an exact name from the full catalog.")
|
|
append_history(
|
|
messages, history, "user",
|
|
f"Tool error: {name} is unavailable. {hint} "
|
|
f"[STATUS BAR: available tools = {sorted(available)}]",
|
|
turn=turn, event="unavailable_tool", available=available,
|
|
)
|
|
continue
|
|
observation = await _call_real_tool(session, task, action, task_dir, state)
|
|
append_history(
|
|
messages, history, "user",
|
|
"Real MCP observation:\n" +
|
|
json.dumps(observation, ensure_ascii=False, default=str)[:50000] +
|
|
f"\n[STATUS BAR: available tools = {sorted(available)}]",
|
|
turn=turn, event="mcp_observation", available=available,
|
|
extra={"tool": name, "success": bool(observation.get("success"))},
|
|
)
|
|
grade = grade_plan(task, actions)
|
|
execution = _finalize_execution(task, state, task_dir)
|
|
execution["agent_finished"] = bool(final_answer.strip())
|
|
execution["task_complete"] = execution["task_complete"] and execution["agent_finished"]
|
|
return {
|
|
"task": task["id"], "strategy": strategy, "model": MODEL,
|
|
"prompt": task["prompt"], "actions": actions, "parse_errors": parse_errors,
|
|
"discoveries": discoveries, "grade": grade, "execution": execution,
|
|
"final_answer": final_answer, "interactions": interactions,
|
|
"history_receipt": {
|
|
"events": history,
|
|
"event_count": len(history),
|
|
"final_chain_sha256": history[-1]["chain_sha256"],
|
|
"dynamic_schema_injection_tokens": sum(
|
|
row.get("schema_tokens", 0) for row in history
|
|
if row["event"] == "schema_injection"
|
|
),
|
|
},
|
|
"initial_schema_names": initial_schema_names,
|
|
"catalog_sha256": sha256_bytes(canonical_json(schemas).encode()),
|
|
"runtime": {"name": "ollama", "model": MODEL,
|
|
"url": OLLAMA_URL, "options": OLLAMA_OPTIONS},
|
|
"system_prompt_sha256": sha256_bytes(system.encode()),
|
|
"system_prompt_tokens": count_tokens(system),
|
|
"elapsed_seconds": round(time.perf_counter() - started, 3),
|
|
}
|
|
|
|
|
|
async def run_group(session: ClientSession, schemas: list[dict[str, Any]],
|
|
index: LocalEmbeddingIndex, strategy: str,
|
|
campaign_dir: Path, *, resume: bool = False) -> list[dict[str, Any]]:
|
|
records = []
|
|
for task in TASKS:
|
|
task_dir = campaign_dir / strategy / task["id"]
|
|
task_dir.mkdir(parents=True, exist_ok=True)
|
|
receipt_path = task_dir / "receipt.json"
|
|
if resume and receipt_path.exists():
|
|
record = json.loads(receipt_path.read_text(encoding="utf-8"))
|
|
if (
|
|
record.get("strategy") != strategy
|
|
or record.get("task") != task["id"]
|
|
or record.get("model") != MODEL
|
|
):
|
|
raise RuntimeError(f"incompatible completed task receipt: {receipt_path}")
|
|
if record.get("execution", {}).get("task_complete") is True:
|
|
records.append(record)
|
|
continue
|
|
|
|
# A formal campaign may make one bounded recovery attempt for an
|
|
# incomplete task after a real provider failure. Preserve the
|
|
# entire first attempt (receipt plus any partial artifacts) inside
|
|
# the same campaign before retrying from a clean task directory.
|
|
# Refuse a third attempt rather than silently cycling forever.
|
|
failed_attempts = task_dir / "failed_attempts"
|
|
archive_dir = failed_attempts / "attempt-1"
|
|
if archive_dir.exists():
|
|
raise RuntimeError(
|
|
f"maximum two real attempts exhausted for incomplete task: {task_dir}"
|
|
)
|
|
archive_dir.mkdir(parents=True)
|
|
for child in list(task_dir.iterdir()):
|
|
if child == failed_attempts:
|
|
continue
|
|
child.replace(archive_dir / child.name)
|
|
record = await run_agent_task(session, schemas, index, strategy, task, task_dir)
|
|
write_json(receipt_path, record)
|
|
records.append(record)
|
|
return records
|
|
|
|
|
|
def safe_summary(records: list[dict[str, Any]]) -> dict[str, Any]:
|
|
by_strategy = {}
|
|
for strategy in ("control", "treatment"):
|
|
rows = [record for record in records if record["strategy"] == strategy]
|
|
by_strategy[strategy] = {
|
|
"tasks": len(rows),
|
|
"mean_tool_selection_accuracy": (
|
|
sum(row["grade"]["accuracy"] for row in rows) / len(rows) if rows else 0
|
|
),
|
|
"tasks_with_all_required_capabilities": sum(
|
|
bool(row["grade"]["all_required_capabilities_selected"]) for row in rows
|
|
),
|
|
"tasks_completed": sum(bool(row["execution"]["task_complete"]) for row in rows),
|
|
"elapsed_seconds": round(sum(row["elapsed_seconds"] for row in rows), 3),
|
|
}
|
|
return by_strategy
|
|
|
|
|
|
def _history_chain_valid(record: dict[str, Any]) -> bool:
|
|
events = record.get("history_receipt", {}).get("events", [])
|
|
prior = "0" * 64
|
|
for sequence, row in enumerate(events):
|
|
if row.get("sequence") != sequence:
|
|
return False
|
|
expected = sha256_bytes(
|
|
f"{prior}:{row.get('role')}:{row.get('event')}:{row.get('turn')}:"
|
|
f"{row.get('content_sha256')}".encode()
|
|
)
|
|
if row.get("chain_sha256") != expected:
|
|
return False
|
|
prior = expected
|
|
return bool(events) and record.get("history_receipt", {}).get(
|
|
"final_chain_sha256"
|
|
) == prior
|
|
|
|
|
|
def _required_receipts_real(record: dict[str, Any], task: dict[str, Any]) -> bool:
|
|
receipts = record.get("execution", {}).get("receipts", [])
|
|
|
|
def valid(receipt: dict[str, Any]) -> bool:
|
|
provenance = receipt.get("backend_provenance", {})
|
|
return (
|
|
receipt.get("success") is True
|
|
and receipt.get("transport") == "mcp-stdio"
|
|
and receipt.get("mcp_result_is_error") is False
|
|
and bool(provenance.get("backend"))
|
|
and provenance.get("origin") in {"live-api", "local-process"}
|
|
and receipt.get("simulation_markers") == []
|
|
and receipt.get("substantive_observation") is True
|
|
and isinstance(receipt.get("payload"), dict)
|
|
and receipt["payload"].get("success") is True
|
|
)
|
|
|
|
for slot in task["slots"]:
|
|
if not any(receipt.get("tool") in slot and valid(receipt) for receipt in receipts):
|
|
return False
|
|
if task["id"] == "transformer_arxiv_download":
|
|
downloads = [receipt for receipt in receipts
|
|
if receipt.get("tool") == "arxiv_download" and valid(receipt)]
|
|
if len(downloads) != 3 or len({row.get("paper_id") for row in downloads}) != 3:
|
|
return False
|
|
return True
|
|
|
|
|
|
def derive_acceptance(records: list[dict[str, Any]], schemas: list[dict[str, Any]],
|
|
catalog: dict[str, Any], protocol: dict[str, Any],
|
|
comparison: dict[str, Any],
|
|
embedding: dict[str, Any]) -> dict[str, Any]:
|
|
"""Derive every acceptance claim from durable campaign receipts."""
|
|
task_by_id = {task["id"]: task for task in TASKS}
|
|
expected_pairs = {(strategy, task["id"])
|
|
for strategy in ("control", "treatment") for task in TASKS}
|
|
actual_pairs = [(row.get("strategy"), row.get("task")) for row in records]
|
|
exact_six = len(actual_pairs) == 6 and set(actual_pairs) == expected_pairs \
|
|
and len(set(actual_pairs)) == len(actual_pairs)
|
|
schema_by_name = {schema["name"]: schema for schema in schemas}
|
|
catalog_hash = sha256_bytes(canonical_json(schemas).encode())
|
|
|
|
qwen_receipts = bool(records) and all(
|
|
row.get("model") == MODEL
|
|
and row.get("runtime", {}).get("name") == "ollama"
|
|
and row.get("runtime", {}).get("model") == MODEL
|
|
and bool(row.get("interactions"))
|
|
and all(
|
|
interaction.get("response", {}).get("response_model") == MODEL
|
|
and interaction.get("response", {}).get("done") is True
|
|
and bool(interaction.get("response", {}).get("request_hash"))
|
|
and bool(interaction.get("response", {}).get("content", "").strip())
|
|
for interaction in row.get("interactions", [])
|
|
)
|
|
for row in records
|
|
)
|
|
|
|
real_execution = exact_six and all(
|
|
row.get("task") in task_by_id
|
|
and _required_receipts_real(row, task_by_id[row["task"]])
|
|
for row in records
|
|
)
|
|
completed_with_artifacts = exact_six and all(
|
|
row.get("execution", {}).get("task_complete") is True
|
|
and row.get("execution", {}).get("agent_finished") is True
|
|
and bool(row.get("final_answer", "").strip())
|
|
for row in records
|
|
)
|
|
|
|
treatment_rows = [row for row in records if row.get("strategy") == "treatment"]
|
|
treatment_discovery = len(treatment_rows) == 3
|
|
dynamic_token_totals = []
|
|
for row in treatment_rows:
|
|
discoveries = row.get("discoveries", [])
|
|
events = [event for event in row.get("history_receipt", {}).get("events", [])
|
|
if event.get("event") == "schema_injection"]
|
|
dynamic_tokens = row.get("history_receipt", {}).get(
|
|
"dynamic_schema_injection_tokens", -1
|
|
)
|
|
dynamic_token_totals.append(dynamic_tokens)
|
|
if not discoveries or len(events) != len(discoveries) or not _history_chain_valid(row):
|
|
treatment_discovery = False
|
|
continue
|
|
if set(row.get("initial_schema_names", [])) != {
|
|
"web_search", "code_interpreter", "discover_tools"
|
|
}:
|
|
treatment_discovery = False
|
|
cumulative = {"web_search", "code_interpreter"}
|
|
for discovery, event in zip(discoveries, events):
|
|
names = [match.get("name") for match in discovery.get("matches", [])]
|
|
resolved = [schema_by_name.get(name) for name in names]
|
|
if any(schema is None for schema in resolved):
|
|
treatment_discovery = False
|
|
continue
|
|
expected_block = render_schemas(resolved)
|
|
cumulative.update(names)
|
|
conditions = [
|
|
3 <= discovery.get("top_k", 0) <= 5,
|
|
discovery.get("top_k") == len(names),
|
|
event.get("role") == "user",
|
|
event.get("turn") == discovery.get("turn"),
|
|
event.get("schema_names") == names,
|
|
event.get("schema_count") == len(names),
|
|
event.get("schemas_sha256") == sha256_bytes(expected_block.encode()),
|
|
discovery.get("schemas_sha256") == sha256_bytes(expected_block.encode()),
|
|
event.get("schema_tokens") == count_tokens(expected_block),
|
|
discovery.get("schema_tokens") == count_tokens(expected_block),
|
|
event.get("status_bar_present") is True,
|
|
set(event.get("available_tools") or []) == cumulative,
|
|
]
|
|
if not all(conditions):
|
|
treatment_discovery = False
|
|
if dynamic_tokens != sum(item.get("schema_tokens", 0) for item in discoveries) \
|
|
or dynamic_tokens <= 0:
|
|
treatment_discovery = False
|
|
|
|
control_rows = [row for row in records if row.get("strategy") == "control"]
|
|
full_control_catalog = len(control_rows) == 3 and all(
|
|
set(row.get("initial_schema_names", [])) == set(schema_by_name)
|
|
and row.get("system_prompt_tokens", 0) > protocol["minimum_control_schema_tokens"]
|
|
for row in control_rows
|
|
)
|
|
identical_runtime = exact_six and all(
|
|
row.get("catalog_sha256") == catalog_hash
|
|
and row.get("runtime") == records[0].get("runtime")
|
|
and row.get("model") == records[0].get("model") == protocol["model"]
|
|
and row.get("prompt") == next(
|
|
task["prompt"] for task in TASKS if task["id"] == row.get("task")
|
|
)
|
|
for row in records
|
|
)
|
|
comparison_present = set(comparison) == {"control", "treatment"} and all(
|
|
set(comparison[strategy]) >= {
|
|
"tasks", "mean_tool_selection_accuracy",
|
|
"tasks_with_all_required_capabilities", "tasks_completed", "elapsed_seconds"
|
|
} and comparison[strategy]["tasks"] == 3
|
|
for strategy in ("control", "treatment")
|
|
)
|
|
embedding_real = (
|
|
embedding.get("model") == "sentence-transformers/all-MiniLM-L6-v2"
|
|
and embedding.get("backend") == "local-transformers-mean-pooling"
|
|
and embedding.get("local_files_only") is True
|
|
and embedding.get("catalog_text_count") == len(schemas)
|
|
and embedding.get("vector_count") == len(schemas)
|
|
and embedding.get("vector_dimensions", 0) > 0
|
|
and bool(embedding.get("texts_sha256"))
|
|
and bool(embedding.get("cache_sha256"))
|
|
)
|
|
gates = {
|
|
"exact_model_with_qwen_response_receipts": qwen_receipts,
|
|
"catalog_from_mcp_and_hash_matches": (
|
|
catalog.get("transport") == "mcp-stdio"
|
|
and catalog.get("tools_list_received") is True
|
|
and catalog.get("schema_sha256") == catalog_hash
|
|
and catalog.get("tool_count") == len(schemas)
|
|
and catalog.get("unique_tool_count") == len(schema_by_name)
|
|
and all(catalog.get("required_tools_present", {}).values())
|
|
and catalog.get("catalog_gzip_bytes", 0) > 0
|
|
and len(catalog.get("catalog_gzip_sha256", "")) == 64
|
|
and catalog.get("catalog_gzip_content_sha256") == catalog_hash
|
|
),
|
|
"tool_count_at_least_120": len(schemas) >= protocol["minimum_mcp_tools"],
|
|
"control_over_50k_complete_schema_tokens": (
|
|
catalog.get("schema_tokens_o200k", 0)
|
|
> protocol["minimum_control_schema_tokens"] and full_control_catalog
|
|
),
|
|
"three_tasks_each_group": exact_six,
|
|
"real_mcp_execution_only": real_execution,
|
|
"all_tasks_completed_with_required_artifacts": completed_with_artifacts,
|
|
"treatment_discovery_history_and_status_verified": treatment_discovery,
|
|
"identical_tasks_model_runtime_and_catalog": identical_runtime,
|
|
"local_embedding_index_receipted": embedding_real,
|
|
"dynamic_schema_injection_tokens_recorded": (
|
|
len(dynamic_token_totals) == 3 and all(value > 0 for value in dynamic_token_totals)
|
|
),
|
|
"comparison_metrics_present_for_both_arms": comparison_present,
|
|
}
|
|
return {"status": "passed" if all(gates.values()) else "failed", "gates": gates}
|
|
|
|
|
|
def build_manifest(campaign_dir: Path) -> dict[str, Any]:
|
|
files = []
|
|
for path in sorted(campaign_dir.rglob("*")):
|
|
if not path.is_file() or path.name == "manifest.json":
|
|
continue
|
|
data = path.read_bytes()
|
|
files.append({"path": str(path.relative_to(campaign_dir)), "bytes": len(data),
|
|
"sha256": sha256_bytes(data)})
|
|
return {"generated_at": datetime.now(timezone.utc).isoformat(), "files": files}
|
|
|
|
|
|
async def run(campaign_id: str | None = None, *, resume: bool = False) -> Path:
|
|
protocol = json.loads(PROTOCOL_PATH.read_text(encoding="utf-8"))
|
|
campaign_id = campaign_id or datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
campaign_dir = VALIDATION_ROOT / campaign_id
|
|
if resume:
|
|
if not campaign_dir.is_dir():
|
|
raise RuntimeError(f"resume campaign does not exist: {campaign_dir}")
|
|
summary_path = campaign_dir / "summary.json"
|
|
manifest_path = campaign_dir / "manifest.json"
|
|
if summary_path.exists() or manifest_path.exists():
|
|
if not (summary_path.is_file() and manifest_path.is_file()):
|
|
raise RuntimeError(
|
|
f"resume campaign has only one final artifact: {campaign_dir}"
|
|
)
|
|
failed_summary = json.loads(summary_path.read_text(encoding="utf-8"))
|
|
if failed_summary.get("status") != "failed":
|
|
raise RuntimeError(f"resume campaign already passed: {campaign_dir}")
|
|
campaign_attempts = campaign_dir / "failed_campaign_attempts"
|
|
attempt_dir = campaign_attempts / "attempt-1"
|
|
if attempt_dir.exists():
|
|
raise RuntimeError(
|
|
f"maximum two real campaign attempts exhausted: {campaign_dir}"
|
|
)
|
|
attempt_dir.mkdir(parents=True)
|
|
summary_path.replace(attempt_dir / "summary.failed.json")
|
|
manifest_path.replace(attempt_dir / "manifest.failed.json")
|
|
recorded_protocol = json.loads(
|
|
(campaign_dir / "protocol.json").read_text(encoding="utf-8")
|
|
)
|
|
if recorded_protocol != protocol:
|
|
raise RuntimeError("resume protocol differs from the preserved campaign protocol")
|
|
else:
|
|
campaign_dir.mkdir(parents=True, exist_ok=False)
|
|
write_json(campaign_dir / "protocol.json", protocol)
|
|
|
|
params = StdioServerParameters(command=sys.executable, args=[str(MCP_SERVER)], env=os.environ.copy())
|
|
async with stdio_client(params) as (read, write):
|
|
async with ClientSession(read, write) as session:
|
|
initialize = await session.initialize()
|
|
listed = await session.list_tools()
|
|
schemas = [schema_dict(tool) for tool in listed.tools]
|
|
schema_bytes = canonical_json(schemas).encode()
|
|
schema_text = render_schemas(schemas)
|
|
catalog = {
|
|
"transport": "mcp-stdio", "server": str(MCP_SERVER),
|
|
"tools_list_received": True,
|
|
"server_name": initialize.serverInfo.name,
|
|
"server_version": initialize.serverInfo.version,
|
|
"tool_count": len(schemas), "unique_tool_count": len({s["name"] for s in schemas}),
|
|
"schema_tokens_o200k": count_tokens(schema_text),
|
|
"schema_bytes": len(schema_bytes), "schema_sha256": sha256_bytes(schema_bytes),
|
|
"required_tools_present": {
|
|
name: name in {schema["name"] for schema in schemas}
|
|
for name in ["web_search", "code_interpreter", "yfinance_quote", "search_news",
|
|
"arxiv_search", "arxiv_download", "github_list_contributors"]
|
|
},
|
|
}
|
|
if catalog["tool_count"] < protocol["minimum_mcp_tools"]:
|
|
raise RuntimeError(f"MCP catalog too small: {catalog['tool_count']}")
|
|
if catalog["schema_tokens_o200k"] <= protocol["minimum_control_schema_tokens"]:
|
|
raise RuntimeError(f"control schema prompt too small: {catalog['schema_tokens_o200k']}")
|
|
if not all(catalog["required_tools_present"].values()):
|
|
raise RuntimeError("required task tools missing from MCP catalog")
|
|
gzip_path = campaign_dir / "catalog.schemas.json.gz"
|
|
catalog_receipt_path = campaign_dir / "catalog_receipt.json"
|
|
if resume:
|
|
preserved_catalog = json.loads(catalog_receipt_path.read_text(encoding="utf-8"))
|
|
current_fields = {
|
|
key: catalog[key]
|
|
for key in (
|
|
"transport", "server", "tools_list_received", "server_name",
|
|
"server_version", "tool_count", "unique_tool_count",
|
|
"schema_tokens_o200k", "schema_bytes", "schema_sha256",
|
|
"required_tools_present",
|
|
)
|
|
}
|
|
if any(preserved_catalog.get(key) != value
|
|
for key, value in current_fields.items()):
|
|
raise RuntimeError("resume MCP catalog differs from preserved catalog receipt")
|
|
if (
|
|
not gzip_path.is_file()
|
|
or gzip_path.stat().st_size != preserved_catalog.get("catalog_gzip_bytes")
|
|
or sha256_bytes(gzip_path.read_bytes())
|
|
!= preserved_catalog.get("catalog_gzip_sha256")
|
|
):
|
|
raise RuntimeError("resume schema gzip does not match preserved catalog receipt")
|
|
with gzip.open(gzip_path, "rt", encoding="utf-8") as stream:
|
|
gzip_schemas = json.load(stream)
|
|
if sha256_bytes(canonical_json(gzip_schemas).encode()) != catalog["schema_sha256"]:
|
|
raise RuntimeError("resume schema gzip content does not match current MCP catalog")
|
|
catalog = preserved_catalog
|
|
else:
|
|
with gzip.open(gzip_path, "wt", encoding="utf-8") as stream:
|
|
json.dump(schemas, stream, ensure_ascii=False)
|
|
catalog["catalog_gzip_bytes"] = gzip_path.stat().st_size
|
|
catalog["catalog_gzip_sha256"] = sha256_bytes(gzip_path.read_bytes())
|
|
with gzip.open(gzip_path, "rt", encoding="utf-8") as stream:
|
|
gzip_schemas = json.load(stream)
|
|
catalog["catalog_gzip_content_sha256"] = sha256_bytes(
|
|
canonical_json(gzip_schemas).encode()
|
|
)
|
|
write_json(catalog_receipt_path, catalog)
|
|
|
|
index = await asyncio.to_thread(LocalEmbeddingIndex, schemas, campaign_dir / "index")
|
|
embedding_receipt = index.receipt()
|
|
embedding_receipt_path = campaign_dir / "embedding_receipt.json"
|
|
if resume:
|
|
preserved_embedding = json.loads(
|
|
embedding_receipt_path.read_text(encoding="utf-8")
|
|
)
|
|
if preserved_embedding != embedding_receipt:
|
|
raise RuntimeError("resume embedding index differs from preserved receipt")
|
|
else:
|
|
write_json(embedding_receipt_path, embedding_receipt)
|
|
control = await run_group(
|
|
session, schemas, index, "control", campaign_dir, resume=resume
|
|
)
|
|
treatment = await run_group(
|
|
session, schemas, index, "treatment", campaign_dir, resume=resume
|
|
)
|
|
records = control + treatment
|
|
comparison = safe_summary(records)
|
|
acceptance = derive_acceptance(
|
|
records, schemas, catalog, protocol, comparison, embedding_receipt
|
|
)
|
|
summary = {
|
|
"experiment": "4-7", "campaign_id": campaign_id,
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"model": MODEL, "catalog": catalog,
|
|
"embedding": embedding_receipt,
|
|
"comparison": comparison,
|
|
"dynamic_schema_injection_tokens": {
|
|
row["task"]: row["history_receipt"]["dynamic_schema_injection_tokens"]
|
|
for row in treatment
|
|
},
|
|
"acceptance": acceptance,
|
|
"status": acceptance["status"],
|
|
}
|
|
write_json(campaign_dir / "summary.json", summary)
|
|
write_json(campaign_dir / "manifest.json", build_manifest(campaign_dir))
|
|
return campaign_dir
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--campaign-id")
|
|
parser.add_argument(
|
|
"--resume",
|
|
action="store_true",
|
|
help="Resume an interrupted campaign after validating all preserved setup receipts.",
|
|
)
|
|
args = parser.parse_args()
|
|
if args.resume and not args.campaign_id:
|
|
parser.error("--resume requires --campaign-id")
|
|
path = asyncio.run(run(args.campaign_id, resume=args.resume))
|
|
print(path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|