Skip to content

[rig-decomposition-bench] Daily rig decomposition benchmark — 2026-10-19 — no result (task picker failed) #678

Closed as not planned

Description

@github-actions

Result: benchmark did not produce data

The run completed (exit 0) but the task-picker agent returned null, so the workflow returned early. The task, timings, solutions, attempts and grading are all empty. Nothing below is invented.

Captured stdout (bench_output.json):

{"status":"failed: task picker returned null","task":null,"attempts":[]}

Captured stderr: empty (0 bytes).

Notes

  • The skill dir had no node_modules, so the first --typecheck failed on every program (even a trivial one) with missing node:* and @types/node errors. I ran npm install in .github/skills/rig (8 packages) and the typecheck then passed. The skill needs its deps installed for this workflow to work without a checkout.
  • The runner's Node is v22.23.3, but the skill requires Node >=24 (EBADENGINE warning). This may be related to the failure, but I did not debug it, as instructed.
  • The picker used small, maxTurns: 2 and repair(). The cause of the null is unknown, because stderr was empty.
  • The program was run exactly once, as instructed.
single-call decomposed
duration (ms) n/a n/a
status n/a not run

Decomposition attempts: none (no task was picked).
Grading: none.

Benchmark program (bench.ts)
import { agent, workflow, s, repair, configureAgent, copilotEngine } from "rig";

configureAgent(copilotEngine());

const DEADLINE = Date.now() + 25 * 60_000;
const left = () => DEADLINE - Date.now();

const SKELETON = `import { agent, workflow, s } from "rig";

// Agent role: <role>
const analyze = agent({ model: "small", instructions: "...", output: s.string });

// Workflow role: combines the agents' outputs into the final solution
export default workflow({
  meta: { name: "decomposed-solution", description: "..." },
  body: async ({ call }) => {
    const analysis = await call(analyze, "...");
    return { solution: analysis ?? "" };
  },
});`;

const CONTRACT = `You write a self-contained TypeScript program for the "rig" library. Rules:
- Import ONLY from "rig" (agent, workflow, s). No other imports, no console.log.
- Split the task across AT LEAST TWO agents, each preceded by a "// Agent role: ..." comment. Each agent sets model: "small" for simple sub-steps, "medium" or "large" for harder ones.
- Coordinate them so the final answer combines the agents' outputs rather than one agent solving everything.
- Root must be exported with "export default" and must NOT be invoked. It takes no required input.
- A workflow() spec accepts only meta, an optional input, and body. It has NO output or execute field; the final { solution } object is simply what body returns. body receives { call } (also pipeline, parallel, phase, log); call(agent, input) returns the agent output or null. An agent() root instead declares output: s.object({ solution: s.string }).
- Use s.string as output for every agent whose result is source text or free-form solution. Do not make any agent re-emit a large solution inside an object wrapper.
- Required shape (abridged to one agent; you need at least two):
${SKELETON}
Return ONLY the complete TypeScript source, with no commentary.`;

// Agent role: pick a concrete benchmark task
const picker = agent({
  model: "small",
  maxTurns: 2,
  addons: [repair()],
  instructions: `Pick ONE concrete, complicated task a person or team might face in a single day that would naturally benefit from being split across sub-agents running different models: multi-source research and synthesis, a multi-file coding task with design/implementation/review phases, a structured report combining independent analyses, or a multi-step data transformation pipeline. It must be self-contained (solvable from its description alone, no external files or live web) and concrete enough to grade. Vary the domain; today is ${new Date().toISOString().slice(0, 10)} so pick something unusual. Return a title, domain, description, and 3-6 concrete checkable success criteria.`,
  output: s.object({
    title: s.string,
    domain: s.string,
    description: s.string,
    criteria: s.array(s.string),
  }),
});

// Agent role: solve the whole task in one turn
const solver = agent({
  model: "medium",
  maxTurns: 1,
  instructions: "Solve the whole task yourself in a single response, addressing every success criterion. No delegation, no tools. Return the complete solution.",
  output: s.string,
});

// Agent role: write or fix the decomposed rig program
const writer = agent({
  model: "medium",
  maxTurns: 1,
  instructions: CONTRACT,
  output: s.string,
});

// Agent role: grade both solutions
const grader = agent({
  model: "large",
  maxTurns: 1,
  instructions: "Score each solution 0-10 on how completely and correctly it satisfies the success criteria. Judge only correctness and completeness of content, not length and not which approach produced it. Pick winner single-call, decomposed, or tie, and explain the comparison.",
  output: s.object({
    singleScore: s.number,
    decomposedScore: s.number,
    winner: s.enum("single-call", "decomposed", "tie"),
    rationale: s.string,
  }),
});

type RunResult = { code: number | null; stdout: string; stderr: string; timedOut: boolean };

function runCli(args: string[], input: string, timeoutMs: number): Promise<RunResult> {
  const cp = process.getBuiltinModule("node:child_process");
  return new Promise((resolve) => {
    let stdout = "";
    let stderr = "";
    let timedOut = false;
    const child = cp.spawn(process.execPath, ["rig.ts", ...args], { cwd: process.cwd(), stdio: ["pipe", "pipe", "pipe"] });
    const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, timeoutMs);
    child.stdout.on("data", (d: Buffer) => { stdout += d.toString(); });
    child.stderr.on("data", (d: Buffer) => { stderr += d.toString(); });
    child.on("error", (e: Error) => { stderr += String(e); });
    child.on("close", (code: number | null) => { clearTimeout(timer); resolve({ code, stdout, stderr, timedOut }); });
    child.stdin.on("error", () => {});
    child.stdin.end(input);
  });
}

type Attempt = { attempt: number; generation: string; typecheck: string; execute: string; error: string; note: string };

function stripFence(src: string): string {
  const m = src.match(/```(?:ts|typescript|rig)?\s*\n([\s\S]*?)```/);
  return (m ? m[1]! : src).trim();
}

// Workflow role: pick a task, solve it two ways, grade, and return the full report
export default workflow({
  meta: { name: "rig-decomposition-bench", description: "Compare single-call vs decomposed solving", phases: ["Pick", "Single", "Decompose", "Grade"] },
  body: async ({ call, phase, log }) => {
    phase("Pick");
    const task = await call(picker, "Pick the task now.", { timeout: Math.max(1000, Math.min(180_000, left())) });
    if (!task) return { status: "failed: task picker returned null", task: null, attempts: [] as Attempt[] };
    const taskText = `Title: ${task.title}\nDomain: ${task.domain}\nDescription: ${task.description}\nSuccess criteria:\n${task.criteria.map((c: string) => `- ${c}`).join("\n")}`;

    phase("Single");
    const t0 = Date.now();
    const single = await call(solver, taskText, { timeout: Math.max(1000, Math.min(480_000, left())) });
    const singleMs = Date.now() - t0;

    phase("Decompose");
    const d0 = Date.now();
    const attempts: Attempt[] = [];
    let source = "";
    let decomposed = "";
    let status = "fail";
    let prevSource = "";
    let prevError = "";
    for (let n = 1; n <= 2; n++) {
      if (left() < 120_000) { log("budget exhausted"); break; }
      let prompt: string;
      if (n === 1) prompt = `Write the program for this task:\n${taskText}`;
      else if (prevSource.trim()) prompt = `Fix the program below. Fix precisely what the error names and preserve what already worked.\n\nTask:\n${taskText}\n\nRequired shape:\n${SKELETON}\n\nSource:\n${prevSource}\n\nError:\n${prevError}`;
      else prompt = `There is no source to fix: the previous attempt produced nothing. Generate the complete program from scratch.\n\nTask:\n${taskText}\n\nRequired shape:\n${SKELETON}`;
      const raw = await call(writer, prompt, { timeout: Math.max(1000, Math.min(300_000, left())) });
      const src = raw ? stripFence(raw) : "";
      const rec: Attempt = { attempt: n, generation: "", typecheck: "skipped", execute: "skipped", error: "", note: "" };
      attempts.push(rec);
      if (!src.trim()) {
        rec.generation = "empty/null";
        rec.error = "writer returned null or whitespace-only source";
        prevSource = ""; prevError = rec.error;
        rec.note = n < 2 ? "Next attempt generates the program from scratch (no source to fix)." : "";
        continue;
      }
      rec.generation = "non-empty";
      source = src;
      prevSource = src;
      const tc = await runCli(["--typecheck"], src, Math.max(1000, Math.min(120_000, left())));
      if (tc.code !== 0) {
        rec.typecheck = tc.timedOut ? "timeout" : "fail";
        rec.error = `${tc.stdout}\n${tc.stderr}`.trim();
        prevError = rec.error;
        rec.note = n < 2 ? "Next attempt passes source and typecheck error to the fixer." : "";
        continue;
      }
      rec.typecheck = "pass";
      if (left() < 30_000) { rec.execute = "skipped (budget)"; break; }
      const ex = await runCli(["--server"], src, Math.max(1000, Math.min(300_000, left())));
      if (ex.code !== 0 || ex.timedOut) {
        rec.execute = ex.timedOut ? "timeout" : "fail";
        rec.error = `exit=${ex.code}\n${ex.stdout}\n${ex.stderr}`.trim();
        prevError = rec.error;
        rec.note = n < 2 ? "Next attempt passes source and execution error to the fixer." : "";
        continue;
      }
      let parsed: { solution?: unknown } | null = null;
      try { parsed = JSON.parse(ex.stdout); } catch { parsed = null; }
      if (parsed && typeof parsed.solution === "string" && parsed.solution.trim()) {
        rec.execute = "pass";
        decomposed = parsed.solution;
        status = "pass";
        break;
      }
      rec.execute = "fail";
      rec.error = `stdout was not JSON with non-empty solution:\n${ex.stdout}\n${ex.stderr}`.trim();
      prevError = rec.error;
      rec.note = n < 2 ? "Next attempt passes source and the output error to the fixer." : "";
    }
    const decomposedMs = Date.now() - d0;

    phase("Grade");
    const placeholder = "[Decomposition produced no solution: the generated program failed to produce output.]";
    let grade = null;
    if (left() > 20_000) {
      grade = await call(grader, `${taskText}\n\n=== SOLUTION A (single-call) ===\n${single ?? "[no solution]"}\n\n=== SOLUTION B (decomposed) ===\n${decomposed.trim() ? decomposed : placeholder}`, { timeout: Math.max(1000, Math.min(300_000, left())) });
    }
    return { task, singleMs, decomposedMs, singleSolution: single ?? "", decomposedSolution: decomposed, attempts, status, grade, decomposedSource: source };
  },
});

Verdict: no winner — the benchmark produced no data because the task picker returned null (empty stderr), so no solutions were generated or graded.

Generated by Daily Rig Decomposition Benchmark · copilot · auto · 34.9 AIC · ⌖ 0.679 AIC · ⊞ 10.1K · ◷

  • expires on Oct 26, 2026, 1:12 PM UTC

Activity

  1. github-actions commented on Oct 20, 2026

    @github-actions
    ContributorAuthor

    This issue is being closed as outdated. A newer issue has been created: #684

    View newer issue


    This action was performed automatically by the Daily Rig Decomposition Benchmark workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions