Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

[rig-decomposition-bench] Daily rig decomposition benchmark — 2026-10-20 — no result (task picker returned null)

未关闭
#684 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
2/5
预计耗时
1-3 小时
新手友好度
58/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
领域
devtools, testing

调研方向

Start with the benchmark program in the issue body and the rig skill entry point at .github/skills/rig/rig.ts. Check how the task picker call handles a null result and whether the reported Node version requirement affects that path; the issue says Node 24 or later is required and the run used Node 22 by default. Done means identifying why the picker returned null and providing a repeatable fix or setup step so the benchmark can proceed.

由索引模型根据 Issue 内容生成。

描述

ai-agent automation

Result

The benchmark ran once and failed at step 1. The task picker returned null, so nothing else executed.

  • Task: none chosen
  • Timing: single-call n/a, decomposed n/a, decomposed status fail
  • Decomposition attempts: none, because the task picker failed
  • Grading: none
  • Single-call solution: none
  • Decomposed program source and solution: none

Captured stdout (bench_output.json):

{"status":"fail","reason":"task picker returned null","task":null,"attempts":[],"source":""}

Captured stderr (bench_stderr.txt) was empty. The process exited with code 0.

Setup notes

  • The first --typecheck failed with missing Node type definitions (node:child_process, process, Buffer and others). I ran npm install in .github/skills/rig to fetch the skill's dependencies, and the typecheck then passed. The skill does not ship its node_modules, so a fresh install needs this step.
  • The default node on PATH was v22.23.3, although the skill requires Node 24 or later. Node 24.22.0 was also installed. This may be why the picker returned null, but I did not investigate. The run was not repeated, as instructed.
Benchmark program (Step 1)
import { agent, workflow, s, repair, configureAgent, copilotEngine } from "rig";

configureAgent(copilotEngine());

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

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

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

const cap = (t: string, n = 6000) => (t.length > n ? t.slice(0, n) + "\n...[truncated]" : t);

// Agent role: pick a concrete, gradeable, complicated daily task suited to multi-agent decomposition.
const pickTask = agent({
  name: "pickTask",
  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 several 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 access) and concrete enough to grade. Prefer variety across domains; choose a random domain. 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 entire task alone in a single turn.
const solveSingle = agent({
  name: "solveSingle",
  model: "medium",
  maxTurns: 1,
  instructions:
    "Solve the whole task yourself in one response, addressing every success criterion. No delegation, no tools. Return the complete solution.",
  output: s.string,
});

// Agent role: write a decomposed rig program for the task.
const writeProgram = agent({
  name: "writeProgram",
  model: "medium",
  maxTurns: 1,
  instructions:
    "You write rig TypeScript programs. Output ONLY the complete program source (no prose; a single ts code fence is allowed). Rules: import only from \"rig\"; split the given task across at least two agents, each with a `// Agent role: ...` comment above it, choosing model \"small\" for simple sub-steps and \"medium\" or \"large\" for harder ones; coordinate them so the final answer combines their outputs rather than one agent solving everything; the root takes no required input and is exported with `export default` and never invoked. A workflow() spec accepts only `meta`, an optional `input`, and `body`; it has no `output` or `execute` field, and the final { solution } object is simply what body returns. An agent() root instead declares `output: s.object({ solution: s.string })`. Use `output: s.string` for every agent whose result is source text or free-form solution; do not make agents re-emit large solutions in object wrappers. Required shape (abridged to one agent; you need at least two):\n\n" +
    "```ts\nimport { agent, workflow, s } from \"rig\";\n\n// Agent role: <role>\nconst analyze = agent({ model: \"small\", instructions: \"...\", output: s.string });\n\n// Workflow role: combines the agents' outputs into the final solution\nexport default workflow({\n  meta: { name: \"decomposed-solution\", description: \"...\" },\n  body: async ({ call }) => {\n    const analysis = await call(analyze, \"...\");\n    return { solution: analysis ?? \"\" };\n  },\n});\n```\n\nThe `call(agent, promptString)` form passes a string input; agents may use `call(worker, input, { label })`. Agent results may be null on failure; handle with `?? \"\"`.",
  output: s.string,
});

// Agent role: fix a failing rig program, or write one from scratch when no source exists.
const fixProgram = agent({
  name: "fixProgram",
  model: "medium",
  maxTurns: 1,
  instructions:
    "You fix rig TypeScript programs. Output ONLY the complete corrected program source (no prose; one ts code fence allowed). Fix precisely what the error names and preserve what already worked. Constraints: import only from \"rig\"; at least two agents each with `// Agent role: ...` comment, models small/medium/large; final answer combines their outputs; root takes no input and is `export default`ed, never invoked. workflow() accepts only meta, optional input, and body (no output/execute); body returns { solution }. Use `output: s.string` for free-form agents. Required shape (abridged to one agent):\n\n" +
    "```ts\nimport { agent, workflow, s } from \"rig\";\n\n// Agent role: <role>\nconst analyze = agent({ model: \"small\", instructions: \"...\", output: s.string });\n\n// Workflow role: combines the agents' outputs into the final solution\nexport default workflow({\n  meta: { name: \"decomposed-solution\", description: \"...\" },\n  body: async ({ call }) => {\n    const analysis = await call(analyze, \"...\");\n    return { solution: analysis ?? \"\" };\n  },\n});\n```",
  output: s.string,
});

// Agent role: grade both solutions on correctness and completeness.
const grade = agent({
  name: "grade",
  model: "large",
  maxTurns: 1,
  addons: [repair()],
  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,
  }),
});

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

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

// Workflow role: pick a task, solve it two ways, run the generated program, and grade.
export default workflow({
  meta: { name: "decomposition-bench", description: "Single-call vs decomposed benchmark", phases: ["Pick", "Single", "Decompose", "Grade"] },
  body: async ({ call, phase, log }) => {
    phase("Pick");
    const task = await call(pickTask, "Pick the task now.", { label: "pick" });
    if (!task) {
      return { status: "fail", reason: "task picker returned null", task: null, attempts: [] as Attempt[], source: "" };
    }
    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(solveSingle, taskText, { label: "single", timeout: Math.max(5000, Math.min(left(), 6 * 60_000)) })) ?? "";
    const singleMs = Date.now() - t0;

    phase("Decompose");
    const d0 = Date.now();
    const attempts: Attempt[] = [];
    let source = "";
    let decomposed = "";
    let status = "fail";
    let lastError = "";
    for (let n = 1; n <= 2; n++) {
      if (left() < 4 * 60_000) { log("insufficient budget for another attempt"); break; }
      const rec: Attempt = { attempt: n, generation: "", typecheck: "skipped", execute: "skipped", error: "", note: "" };
      attempts.push(rec);
      let out: string | null;
      const wt = Math.max(5000, Math.min(left() - 60_000, 6 * 60_000));
      if (n === 1) {
        out = await call(writeProgram, taskText, { label: "write", timeout: wt });
      } else if (source.trim()) {
        out = await call(fixProgram, `Task:\n${taskText}\n\nCurrent source:\n${source}\n\nExact captured error:\n${lastError}\n\nFix the program.`, { label: "fix", timeout: wt });
      } else {
        out = await call(fixProgram, `There is no source to fix: the previous attempt produced no program. Generate the complete program from scratch for this task.\n\nTask:\n${taskText}`, { label: "fix-scratch", timeout: wt });
      }
      const src = out ? stripFence(out) : "";
      if (!src.trim()) {
        rec.generation = "empty (null or whitespace-only)";
        rec.error = "source generation returned no usable source";
        lastError = rec.error;
        if (n === 1) rec.note = "second attempt asks the fixer to generate from scratch";
        continue;
      }
      rec.generation = `ok (${src.length} chars)`;
      source = src;
      if (left() < 60_000) { rec.error = "time budget exhausted before typecheck"; break; }
      const tc = await runCli(src, ["--typecheck"], Math.min(2 * 60_000, left()));
      if (tc.code !== 0) {
        rec.typecheck = tc.timedOut ? "timeout" : "fail";
        rec.error = cap(tc.stdout + tc.stderr);
        lastError = rec.error;
        if (n === 1) rec.note = "fixer given source and the typecheck error";
        continue;
      }
      rec.typecheck = "pass";
      if (left() < 30_000) { rec.error = "time budget exhausted before execute"; break; }
      const ex = await runCli(src, ["--server"], Math.min(5 * 60_000, left()));
      if (ex.code !== 0 || !ex.stdout.trim()) {
        rec.execute = ex.timedOut ? "timeout" : "fail";
        rec.error = cap(ex.stderr + (ex.stdout ? "\n[stdout]\n" + ex.stdout : "") || "no output");
        lastError = rec.error;
        if (n === 1) rec.note = "fixer given source and the execute error";
        continue;
      }
      rec.execute = "pass";
      try {
        const parsed = JSON.parse(ex.stdout);
        decomposed = typeof parsed === "string" ? parsed : typeof parsed?.solution === "string" ? parsed.solution : JSON.stringify(parsed);
      } catch {
        decomposed = ex.stdout.trim();
      }
      if (decomposed.trim()) status = "pass";
      else rec.error = "execution succeeded but solution was empty";
      break;
    }
    const decomposedMs = Date.now() - d0;

    phase("Grade");
    const placeholder = "[Decomposition produced no solution: all attempts failed or the time budget was exhausted.]";
    const graded = left() > 30_000
      ? await call(grade, `${taskText}\n\n=== SOLUTION A (single-call) ===\n${single || "[empty]"}\n\n=== SOLUTION B (decomposed) ===\n${decomposed.trim() ? decomposed : placeholder}`, { label: "grade", timeout: Math.min(4 * 60_000, left()) })
      : null;

    return { status, task, singleMs, decomposedMs, single, decomposed, attempts, source, grading: graded };
  },
});

Verdict: no winner. The run failed at task selection (the pickTask agent returned null even with repair() and maxTurns: 2), so neither approach was attempted or graded.

Generated by Daily Rig Decomposition Benchmark · copilot · auto · 33 AIC · ⌖ 0.638 AIC · ⊞ 10K · ◷

  • expires on Oct 27, 2026, 1:08 PM UTC
主要语言
TypeScript
星标
14
派生
0
平均合并
9 小时 49 分钟
30 天内合并 PR
32

环境准备

这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

githubnext/rig 的其他 Issue

查看 githubnext/rig 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。