# ============================================================================= # EXAMPLE — NoETL as the orchestration harness around an agentic circuit search # for the Google Quantum AI "Verifiable Quantum Circuit Compilation" competition # on Kaggle. # # What this file is: # A sketch of how a team could run its own circuit search under NoETL: an AI # model proposes generator settings, the team's own scripts build circuits with # the open-source kickmix library, the competition's standalone verifiers check # them, and every candidate's inputs, artifact hash and verifier result land on # the execution's event log and in a results table. # # What this file is NOT: # - NoETL does not compile, optimise or verify circuits. kickmix and the # competition's verifiers do that; NoETL runs them, retries them, records # what happened and decides what runs next. # - It does not upload anything to Kaggle. The last step marks a bundle as # ready for a person to review and submit. # - It has not been run against the competition. Script paths, generator # knobs, the verifier wrapper's output and the cost proxy are placeholders # for a team to replace with its own. # # Schema: the v2 playbook format the NoETL server parses # (noetl/server orchestrate-core/src/playbook.rs). Every tool kind below is in the # server's ToolKind set: python, http, postgres. Retry, jump and continue are # `spec.policy.rules` on pipeline tasks, which the worker's task_sequence tool # evaluates (noetl/tools src/tools/task_sequence.rs). # # There is no `kind: agent` or `kind: mcp` step here: the server refuses those # kinds at registration. The model is called with an `http` task, and its answer # is validated by a python task before anything runs. # ============================================================================= apiVersion: noetl.io/v2 kind: Playbook metadata: name: vqcc_circuit_search_example path: examples/quantum/vqcc_circuit_search description: >- EXAMPLE harness. An AI model proposes generator settings, team-owned kickmix scripts build circuits, the official verifiers check them, and NoETL records, retries, scores and gates each round. NoETL does not compile circuits. labels: example: "true" domain: quantum competition: google-verifiable-quantum-circuit-compilation # Credentials are named by reference. The server refuses a playbook with inline # secret material at registration, so the API key lives in Secret Manager and is # resolved by keychain alias when the step runs. keychain: - name: model_api kind: secrets provider: gcp scope: global auth: "{{ gcp_auth }}" map: api_key: "{{ model_api_secret_path }}" workload: # --- infrastructure (replace) --------------------------------------------- gcp_auth: gcp_workload_identity # credential alias model_api_secret_path: projects/YOUR_PROJECT/secrets/model-api-key/versions/latest pg_auth: pg_vqcc # credential alias for the results DB kickmix_python: /opt/vqcc/venv/bin/python # interpreter with kickmix installed artifact_root: /mnt/vqcc # a path every worker in the pool mounts verify_wrapper: /opt/vqcc/scripts/verify_and_report.py # --- search bounds (the agent cannot exceed these) ------------------------ max_rounds: 4 candidates_per_round: 6 model: claude-sonnet-5-5 generate_timeout_s: 900 verify_timeout_s: 3600 # the official verifiers can run for a long time # Team-owned generator scripts that call kickmix, one per benchmark, with the # knobs the agent may set and the inclusive range each knob may take. The # knob names are illustrative; they are not kickmix or competition parameters. generators: invsqrt: script: /opt/vqcc/generators/invsqrt.py params: { poly_degree: [1, 4], precision_bits: [8, 32] } dqi: script: /opt/vqcc/generators/dqi.py params: { decoder_degree: [4, 16], lanes: [1, 8] } dfthcsos: script: /opt/vqcc/generators/dfthcsos.py params: { rank: [8, 64], givens_depth: [2, 12] } # Local cost of each benchmark's reference baseline, as reported by your # verifier wrapper. Used only for the local proxy score below; Kaggle computes # the real score. baseline_cost: { invsqrt: 1.0, dqi: 1.0, dfthcsos: 1.0 } # The local proxy score of the last bundle you submitted, and how much better # a new bundle must be before it is worth a person's review. last_submitted_score: 1.0 min_improvement: 0.02 # Arrays the dfthcsos/submission.npz must contain. Confirm against the # competition's own materials before relying on this list. npz_required_keys: [U, W, H2_I, H1_I, Hsym_lower, tree_edges] workflow: # --------------------------------------------------------------------------- # 1. Set up the results table and the round counter. # --------------------------------------------------------------------------- - step: start desc: Create the candidate and bundle tables, then open round 1 tool: - name: ensure_tables kind: postgres auth: "{{ pg_auth }}" command: | CREATE TABLE IF NOT EXISTS vqcc_candidate ( execution_id TEXT NOT NULL, candidate_id TEXT NOT NULL, round_no INT NOT NULL, benchmark TEXT NOT NULL, params JSONB NOT NULL, rationale TEXT, status TEXT NOT NULL, -- verified | rejected | generator_failed | verifier_crashed passed BOOLEAN NOT NULL, qubits BIGINT, gate_cost DOUBLE PRECISION, cost DOUBLE PRECISION, artifact_dir TEXT, artifact_sha256 TEXT, verifier_tail TEXT, recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (execution_id, candidate_id) ); CREATE TABLE IF NOT EXISTS vqcc_bundle ( execution_id TEXT NOT NULL, round_no INT NOT NULL, bundle_path TEXT NOT NULL, bundle_sha256 TEXT NOT NULL, local_score DOUBLE PRECISION NOT NULL, decision TEXT NOT NULL, -- ready_for_review | not_better members JSONB NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (execution_id, round_no) ); spec: policy: rules: - when: "{{ output.status == 'error' }}" then: { do: retry, attempts: 3, backoff: exponential, delay: 2.0 } - else: then: do: continue set: ctx.round: 1 ctx.leaderboard: [] ctx.local_score: 0 next: spec: mode: exclusive arcs: - step: propose_candidates # --------------------------------------------------------------------------- # 2. Candidate generation. The model proposes settings; it does not run code. # A python task then throws away anything outside the declared generators # and ranges, and caps the count. Both tasks are recorded on the log. # --------------------------------------------------------------------------- - step: propose_candidates desc: Ask the model for generator settings, then keep only what the bounds allow tool: - name: ask_model kind: http method: POST url: https://api.anthropic.com/v1/messages headers: x-api-key: "{{ keychain.model_api.api_key }}" anthropic-version: "2023-06-01" content-type: application/json json: model: "{{ model }}" max_tokens: 2048 system: | You propose settings for circuit generator scripts. Reply with a JSON array only. Each item is {"benchmark", "params", "rationale"}. benchmark is one of the keys of the generators object. params may only use that generator's knobs, each an integer inside its range. messages: - role: user content: | Round {{ ctx.round }} of {{ max_rounds }}. Propose up to {{ candidates_per_round }} candidates. Generators and allowed ranges: {{ generators | tojson }} Best verified candidates so far: {{ ctx.leaderboard | tojson }} spec: policy: rules: - when: "{{ output.status == 'error' }}" then: { do: retry, attempts: 4, backoff: exponential, delay: 5.0 } - else: then: { do: continue } - name: bound_proposals kind: python input: reply: "{{ ask_model.content }}" generators: "{{ generators }}" limit: "{{ candidates_per_round }}" round_no: "{{ ctx.round }}" code: | import json, re # The reply is a list of content blocks; take the text ones. blocks = reply if isinstance(reply, list) else [reply] text = "".join(b.get("text", "") for b in blocks if isinstance(b, dict)) match = re.search(r"\[.*\]", text, re.S) proposals = json.loads(match.group(0)) if match else [] kept, dropped = [], [] for p in proposals if isinstance(proposals, list) else []: bench = p.get("benchmark") if isinstance(p, dict) else None gen = generators.get(bench) if bench else None params = p.get("params", {}) if gen else {} ok = ( gen is not None and isinstance(params, dict) and set(params) <= set(gen["params"]) and all( isinstance(v, int) and gen["params"][k][0] <= v <= gen["params"][k][1] for k, v in params.items() ) ) (kept if ok else dropped).append(p) kept = kept[: int(limit)] candidates = [ { "id": f"r{round_no}-c{i}", "benchmark": c["benchmark"], "params": c["params"], "rationale": str(c.get("rationale", ""))[:500], } for i, c in enumerate(kept) ] result = {"candidates": candidates, "dropped": len(dropped)} spec: policy: rules: - when: "{{ output.status == 'error' or not output.data.candidates }}" then: { do: fail } - else: then: do: continue set: ctx.candidates: "{{ output.data.candidates }}" next: spec: mode: exclusive arcs: - step: build_and_verify # --------------------------------------------------------------------------- # 3. Build and verify every candidate, in parallel across the worker pool. # Each iteration is one command a worker claims. A generator or verifier # crash is retried; a circuit that fails verification is recorded, not # retried. The row is written with ON CONFLICT DO NOTHING so a redelivered # iteration does not record the candidate twice. # --------------------------------------------------------------------------- - step: build_and_verify desc: Generate each candidate with kickmix, run the verifier, record the result loop: in: "{{ ctx.candidates }}" iterator: cand spec: mode: parallel max_in_flight: 8 tool: - name: generate kind: python python: "{{ kickmix_python }}" timeout_seconds: 1200 input: cand: "{{ iter.cand }}" generators: "{{ generators }}" artifact_root: "{{ artifact_root }}" execution_id: "{{ execution_id }}" round_no: "{{ ctx.round }}" timeout_s: "{{ generate_timeout_s }}" code: | import hashlib, pathlib, subprocess, sys out_dir = pathlib.Path(artifact_root) / str(execution_id) / f"r{round_no}" / cand["id"] out_dir.mkdir(parents=True, exist_ok=True) argv = [sys.executable, generators[cand["benchmark"]]["script"], "--out", str(out_dir)] for k, v in sorted(cand["params"].items()): argv += [f"--{k}", str(v)] proc = subprocess.run(argv, capture_output=True, text=True, timeout=int(timeout_s)) if proc.returncode != 0: raise RuntimeError(f"generator exited {proc.returncode}: {proc.stderr[-2000:]}") # Content hash of everything the generator wrote, so the record names # exactly which artifact was verified. h = hashlib.sha256() for f in sorted(p for p in out_dir.rglob("*") if p.is_file()): h.update(f.relative_to(out_dir).as_posix().encode()) h.update(f.read_bytes()) result = {"artifact_dir": str(out_dir), "sha256": h.hexdigest()} spec: policy: rules: - when: "{{ output.status == 'error' and _attempt < 2 }}" then: { do: retry, attempts: 3, backoff: linear, delay: 30.0 } - when: "{{ output.status == 'error' }}" then: do: jump to: record set: verify_row: candidate_id: "{{ cand.id }}" benchmark: "{{ cand.benchmark }}" params: "{{ cand.params }}" rationale: "{{ cand.rationale }}" status: generator_failed passed: false - else: then: { do: continue } - name: verify kind: python python: "{{ kickmix_python }}" timeout_seconds: 4200 input: cand: "{{ iter.cand }}" artifact_dir: "{{ generate.artifact_dir }}" artifact_sha256: "{{ generate.sha256 }}" wrapper: "{{ verify_wrapper }}" timeout_s: "{{ verify_timeout_s }}" code: | import json, subprocess, sys # verify_and_report.py is the team's thin wrapper around the official # standalone verifier for this benchmark. It prints one JSON line: # {"passed": bool, "qubits": int, "gate_cost": float, "cost": float}. proc = subprocess.run( [sys.executable, wrapper, "--benchmark", cand["benchmark"], "--artifact", artifact_dir], capture_output=True, text=True, timeout=int(timeout_s), ) lines = [l for l in proc.stdout.splitlines() if l.strip().startswith("{")] if not lines: # No verdict at all: the verifier crashed. Raise so the policy retries. raise RuntimeError(f"verifier gave no verdict (exit {proc.returncode}): {proc.stderr[-2000:]}") verdict = json.loads(lines[-1]) passed = bool(verdict.get("passed")) result = { "row": { "candidate_id": cand["id"], "benchmark": cand["benchmark"], "params": cand["params"], "rationale": cand.get("rationale", ""), "status": "verified" if passed else "rejected", "passed": passed, "qubits": verdict.get("qubits"), "gate_cost": verdict.get("gate_cost"), "cost": verdict.get("cost"), "artifact_dir": artifact_dir, "artifact_sha256": artifact_sha256, "verifier_tail": proc.stdout[-4000:], } } spec: policy: rules: - when: "{{ output.status == 'error' and _attempt < 2 }}" then: { do: retry, attempts: 3, backoff: linear, delay: 120.0 } - when: "{{ output.status == 'error' }}" then: do: continue set: verify_row: candidate_id: "{{ cand.id }}" benchmark: "{{ cand.benchmark }}" params: "{{ cand.params }}" rationale: "{{ cand.rationale }}" status: verifier_crashed passed: false - else: then: do: continue set: verify_row: "{{ output.data.row }}" - name: record kind: postgres auth: "{{ pg_auth }}" # One JSON parameter: the postgres tool binds an object as its JSON text, # and the SQL unpacks it, so no value depends on how a template renders. command: | INSERT INTO vqcc_candidate ( execution_id, candidate_id, round_no, benchmark, params, rationale, status, passed, qubits, gate_cost, cost, artifact_dir, artifact_sha256, verifier_tail ) SELECT j->>'execution_id', r->>'candidate_id', (j->>'round_no')::int, r->>'benchmark', r->'params', r->>'rationale', r->>'status', (r->>'passed')::boolean, (r->>'qubits')::bigint, (r->>'gate_cost')::double precision, (r->>'cost')::double precision, r->>'artifact_dir', r->>'artifact_sha256', r->>'verifier_tail' FROM (SELECT j, j->'row' AS r FROM (SELECT $1::text::jsonb AS j) AS a) AS s ON CONFLICT (execution_id, candidate_id) DO NOTHING; params: - execution_id: "{{ execution_id }}" round_no: "{{ ctx.round }}" row: "{{ verify_row }}" spec: policy: rules: - when: "{{ output.status == 'error' }}" then: { do: retry, attempts: 5, backoff: exponential, delay: 2.0 } - else: then: { do: continue } next: spec: mode: exclusive arcs: - step: score_round when: "{{ event.name == 'loop.done' }}" # --------------------------------------------------------------------------- # 4. Cost score. Read back the best verified candidate per benchmark across # every round of this execution, and compute a local proxy score. # --------------------------------------------------------------------------- - step: score_round desc: Pick the cheapest verified candidate per benchmark and score the set tool: - name: best kind: postgres auth: "{{ pg_auth }}" command: | SELECT DISTINCT ON (benchmark) benchmark, candidate_id, round_no, qubits, gate_cost, cost, artifact_dir, artifact_sha256 FROM vqcc_candidate WHERE execution_id = ($1::text::jsonb)->>'execution_id' AND passed AND cost IS NOT NULL ORDER BY benchmark, cost ASC, candidate_id ASC; params: - execution_id: "{{ execution_id }}" spec: policy: rules: - when: "{{ output.status == 'error' }}" then: { do: retry, attempts: 5, backoff: exponential, delay: 2.0 } - else: then: { do: continue } - name: score kind: python input: rows: "{{ best.rows }}" baseline_cost: "{{ baseline_cost }}" code: | import math # Local proxy only: geometric mean of baseline_cost / cost over the # benchmarks that have a verified candidate. Kaggle computes the # real score; this exists so the gate has something to compare. rows = rows or [] ratios = [baseline_cost[r["benchmark"]] / r["cost"] for r in rows if r["cost"]] local = math.exp(sum(math.log(x) for x in ratios) / len(ratios)) if ratios else 0.0 covered = sorted(r["benchmark"] for r in rows) result = { "leaderboard": rows, "local_score": local, "complete": covered == sorted(baseline_cost), } spec: policy: rules: - else: then: do: continue set: ctx.leaderboard: "{{ output.data.leaderboard }}" ctx.local_score: "{{ output.data.local_score }}" ctx.complete: "{{ output.data.complete }}" next: spec: mode: exclusive arcs: - step: package_submission when: "{{ ctx.complete }}" - step: propose_candidates when: "{{ ctx.round < max_rounds }}" set: ctx.round: "{{ ctx.round + 1 }}" - step: finish # --------------------------------------------------------------------------- # 5. Package the best verified set into the submission layout: # invsqrt.kmb|kmx, dqi.kmb|kmx, dfthcsos/angle_k_0..n-1, dfthcsos/submission.npz # Checks the angle files are numbered without gaps and the npz has the # required arrays, then zips and hashes the bundle. # --------------------------------------------------------------------------- - step: package_submission desc: Assemble and hash the submission bundle from the verified artifacts tool: - name: package kind: python python: "{{ kickmix_python }}" input: leaderboard: "{{ ctx.leaderboard }}" artifact_root: "{{ artifact_root }}" execution_id: "{{ execution_id }}" round_no: "{{ ctx.round }}" npz_required_keys: "{{ npz_required_keys }}" code: | import hashlib, pathlib, re, shutil, zipfile import numpy as np best = {r["benchmark"]: r for r in leaderboard} stage = pathlib.Path(artifact_root) / str(execution_id) / f"bundle-r{round_no}" if stage.exists(): shutil.rmtree(stage) # a retried package starts from clean (stage / "dfthcsos").mkdir(parents=True) def one_circuit(src_dir, name): found = sorted(pathlib.Path(src_dir).glob("*.kmb")) + sorted(pathlib.Path(src_dir).glob("*.kmx")) if len(found) != 1: raise RuntimeError(f"{name}: expected one .kmb/.kmx in {src_dir}, found {len(found)}") dst = stage / f"{name}{found[0].suffix}" shutil.copyfile(found[0], dst) return dst.name members = [one_circuit(best["invsqrt"]["artifact_dir"], "invsqrt"), one_circuit(best["dqi"]["artifact_dir"], "dqi")] src = pathlib.Path(best["dfthcsos"]["artifact_dir"]) angles = sorted(src.glob("angle_k_*.km[bx]"), key=lambda p: int(re.search(r"angle_k_(\d+)", p.name).group(1))) idx = [int(re.search(r"angle_k_(\d+)", p.name).group(1)) for p in angles] if not angles or idx != list(range(len(angles))): raise RuntimeError(f"dfthcsos: angle_k files must be numbered 0..n-1 with no gaps, got {idx}") for p in angles: shutil.copyfile(p, stage / "dfthcsos" / p.name) members.append(f"dfthcsos/{p.name}") npz = np.load(src / "submission.npz") missing = [k for k in npz_required_keys if k not in npz.files] if missing: raise RuntimeError(f"dfthcsos/submission.npz is missing {missing}") shutil.copyfile(src / "submission.npz", stage / "dfthcsos" / "submission.npz") members.append("dfthcsos/submission.npz") bundle = stage.with_suffix(".zip") with zipfile.ZipFile(bundle, "w", zipfile.ZIP_DEFLATED) as z: for m in members: # files at the zip root, no wrapping folder z.write(stage / m, m) digest = hashlib.sha256(bundle.read_bytes()).hexdigest() result = {"bundle_path": str(bundle), "bundle_sha256": digest, "members": members} spec: policy: rules: - when: "{{ output.status == 'error' }}" then: { do: fail } - else: then: do: continue set: ctx.bundle: "{{ output.data }}" next: spec: mode: exclusive arcs: - step: leaderboard_gate # --------------------------------------------------------------------------- # 6. Leaderboard gate. Compare the bundle's local proxy score with the last # submitted one. A better bundle is marked ready for a person to review and # submit; NoETL never uploads it. Otherwise search again while rounds remain. # --------------------------------------------------------------------------- - step: leaderboard_gate desc: Decide whether this bundle is worth a person's review tool: - name: decide kind: python input: local_score: "{{ ctx.local_score }}" last_submitted_score: "{{ last_submitted_score }}" min_improvement: "{{ min_improvement }}" code: | better = float(local_score) >= float(last_submitted_score) * (1 + float(min_improvement)) result = {"decision": "ready_for_review" if better else "not_better"} spec: policy: rules: - else: then: do: continue set: ctx.decision: "{{ output.data.decision }}" - name: record_bundle kind: postgres auth: "{{ pg_auth }}" command: | INSERT INTO vqcc_bundle (execution_id, round_no, bundle_path, bundle_sha256, local_score, decision, members) SELECT j->>'execution_id', (j->>'round_no')::int, j->'bundle'->>'bundle_path', j->'bundle'->>'bundle_sha256', (j->>'local_score')::double precision, j->>'decision', j->'bundle'->'members' FROM (SELECT $1::text::jsonb AS j) AS s ON CONFLICT (execution_id, round_no) DO NOTHING; params: - execution_id: "{{ execution_id }}" round_no: "{{ ctx.round }}" local_score: "{{ ctx.local_score }}" decision: "{{ decide.decision }}" bundle: "{{ ctx.bundle }}" spec: policy: rules: - when: "{{ output.status == 'error' }}" then: { do: retry, attempts: 5, backoff: exponential, delay: 2.0 } - else: then: { do: continue } next: spec: mode: exclusive arcs: - step: finish when: "{{ ctx.decision == 'ready_for_review' }}" - step: propose_candidates when: "{{ ctx.round < max_rounds }}" set: ctx.round: "{{ ctx.round + 1 }}" - step: finish # --------------------------------------------------------------------------- # 7. Summary for the execution record. # --------------------------------------------------------------------------- - step: finish desc: Summarise the search; any upload to Kaggle is done by a person tool: kind: python input: rounds: "{{ ctx.round }}" leaderboard: "{{ ctx.leaderboard }}" local_score: "{{ ctx.local_score }}" decision: "{{ ctx.decision }}" bundle: "{{ ctx.bundle }}" code: | result = { "rounds": rounds, "local_score": local_score, "decision": decision or "no_complete_bundle", "bundle": bundle or None, "leaderboard": leaderboard, "note": "Review the bundle and run the official verifiers before any Kaggle upload.", }