[rig-decomposition-bench] Daily rig decomposition benchmark — 2026-10-11 — tie
Maintainer antworten meist innerhalb von 1 Tag
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 1-2 Tage
- Anfängerfreundlichkeit
- 45/100
- Issue-Typ
- Bug
- Klarheit
- Größtenteils klar
- Aktivitätsstatus
- Aktiv
- Tech-Stack
- github, typescript
- Bereich
- testing-qa
Rechercherichtung
Start with bench.ts, especially the s.string output contracts for singleCallSolver, decompositionWriter, and decompositionFixer, plus the captured JSON parsing failures. Trace how agent responses are parsed and how the benchmark records generation failures. Done means the agents return usable results under the stated constraints and the benchmark can report and grade both solutions.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Task
Title: Quarterly Vendor Consolidation Decision Memo
Domain: operations / procurement analytics
A mid-size logistics company is deciding whether to consolidate its packaging supplies from four vendors down to two, to simplify ordering and cut costs. Given data for four vendors (unit price, minimum order quantity, lead time, defect rate), quarterly demand of 18,000 boxes, holding-cost and rework-cost formulas, the model must evaluate every 2-vendor combination's total quarterly cost and weighted average lead time, pick the cheapest combination meeting a ≤15-day lead-time constraint, and compute exact dollar savings versus an all-four-vendor baseline.
Success criteria:
- All 6 pairwise vendor combinations (AB, AC, AD, BC, BD, CD) are listed with computed total quarterly cost and weighted average lead time, with arithmetic traceable to the given formulas
- The weighted average lead time for each combination is correctly computed as (qty1lead1 + qty2lead2)/18000 and correctly flagged as meeting or violating the ≤15 day constraint
- The identified cheapest feasible (lead-time ≤15 days) combination is actually the minimum-cost among only the combinations that satisfy the constraint, not the global minimum if that one is infeasible
- The baseline all-four-vendor cost (4,500 boxes each) is computed using the same cost formula (price + holding + defect rework) and the savings figure equals baseline cost minus the recommended combination's cost, stated as an exact dollar amount
- The recommendation paragraph explicitly names which combination is cheapest overall (even if infeasible) and which is the cheapest feasible pick, and explains the cost difference between them
- Each vendor's assigned quantity in every combination respects that vendor's own minimum order quantity
Timing comparison
| Approach | Duration (ms) | Status |
|---|---|---|
| Single-call | 93,229 | Failed — agent produced no parseable output (see below) |
| Decomposed | 117,944 | Fail (decomposedPass: false) |
Decomposition attempts
| # | Source generation | Typecheck | Execute | Note |
|---|---|---|---|---|
| 1 | empty | skipped | skipped | Writer/fixer produced no usable source; next attempt (if any) regenerates from scratch. |
| 2 | empty | skipped | skipped | Writer/fixer produced no usable source; next attempt (if any) regenerates from scratch. |
Captured error/output detail
Both attempts returned an empty (or whitespace-only) s.string result from the decompositionWriter / decompositionFixer agents, so per the hard requirement in the spec ("Treat a null, missing, or whitespace-only writer result as a source-generation failure: never pass it to the fixer or invoke the CLI with it"), neither attempt reached the typecheck or execute stage — both are recorded as skipped.
No typecheck or execute stderr/stdout was captured because the CLI was never invoked for either attempt.
A separate (non-reported, investigative-only) debug run of the same program confirmed the proximate cause: the single-turn s.string-output agents (singleCallSolver, decompositionWriter, decompositionFixer — each maxTurns: 1 with no repair() addon, per the spec's requirement that only the task-picker gets repair()) returned responses that could not be parsed as a single JSON string value ("No JSON value found." / "Expected property name or '}' in JSON at position 2"), causing call() to return null for all three. This is consistent with how rig's s.string output contract expects the model's raw response to itself be valid JSON (a quoted string), and the underlying model for these calls instead returned free-form Markdown/prose at maxTurns: 1 with no parse-repair retry available.
Grading
| Candidate | Score |
|---|---|
| Single-call | 0 / 10 |
| Decomposed | 0 / 10 |
Winner: tie
Rationale (verbatim):
Neither candidate produced any solution content. The single-call solution explicitly states '(no solution produced)' and the decomposed solution states it 'produced no valid solution within the allotted attempts and time budget.' Neither addresses any of the six success criteria: no pairwise combination table, no lead-time calculations, no feasibility-constrained minimum identification, no baseline cost comparison, no recommendation paragraph, and no verification of minimum order quantity constraints. Both are complete failures to deliver any output, so they are equally unsatisfactory and tied at the lowest possible score.
Single-call solution
Single-call solution (verbatim)
(empty — the singleCallSolver agent call returned null, so singleCallSolution is the empty string)
Decomposed program source
The decomposition writer and its one fixer retry both returned empty/whitespace-only source on both attempts, so no decomposed program was ever generated, typechecked, or executed. decomposedSource is the empty string.
(no source was generated)
Decomposed solution (verbatim)
Decomposition produced no valid solution within the allotted attempts and time budget.
Benchmark program (Step 1)
bench.ts
import { agent, configureAgent, copilotEngine, repair, s, workflow } from "rig";
// Configure the Copilot engine in server (stdio) mode.
configureAgent(copilotEngine({ server: true }));
// Overall wall-clock budget for this benchmark run (~25 minutes from module eval).
const DEADLINE = Date.now() + 25 * 60_000;
const remainingMs = () => DEADLINE - Date.now();
const clamp = (desiredMs: number, minMs: number) => Math.max(minMs, Math.min(desiredMs, remainingMs()));
// Agent role: pick one concrete, complicated single-day task suited to multi-agent decomposition.
const taskPicker = 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 distinct design/implementation/review " +
"phases, a structured report combining several independent analyses, or a multi-step data " +
"transformation pipeline. The task must be fully self-contained and solvable from its " +
"description alone, with no external file or live web access required, and concrete enough to " +
"grade. Prefer variety across domains from run to run (avoid always picking software or writing " +
"tasks). Return a title, a domain, a full description, and 3-6 concrete, independently checkable " +
"success criteria.",
output: s.object({
title: s.string,
domain: s.string,
description: s.string("The full self-contained task description, solvable with no external access"),
criteria: s.array(s.string, "3-6 concrete, independently checkable success criteria"),
}),
});
// Agent role: solve the whole task alone in a single turn, addressing every success criterion.
const singleCallSolver = agent({
model: "medium",
maxTurns: 1,
instructions:
"Solve the task completely and address every success criterion listed below, by yourself, with " +
"no delegation and no tools. Return the complete solution verbatim, including Markdown code " +
"fences when appropriate.",
output: s.string,
});
const DECOMPOSITION_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 WRITER_CONTRACT =
"You are writing a self-contained rig TypeScript program (the rig skill is NOT available to you " +
"for reference, so follow this contract exactly). The program must import only from \"rig\". Its " +
"root export must be a workflow() that takes NO required input, exported via `export default` " +
"WITHOUT invoking it. A workflow() spec accepts only `meta`, an optional `input`, and `body`; it " +
"has no `output` or `execute` field — the final `{ solution }` object returned by the program is " +
"simply whatever `body` returns. (An agent() root would instead declare " +
"`output: s.object({ solution: s.string })`, but you must use a workflow() root here.) The program " +
"must split the task across AT LEAST TWO separate agent() definitions, each with its own " +
"`// Agent role: ...` comment directly above it, each choosing model: \"small\" for simple " +
"sub-steps and model: \"medium\" or \"large\" for harder sub-steps, coordinated by the workflow's " +
"body so that the final answer combines their outputs rather than one agent solving everything by " +
"itself. Use `s.string` as the output schema for every agent whose result is free-form source text " +
"or another free-form solution fragment; do not make any sub-agent re-emit a large solution wrapped " +
"inside an object schema. Here is the required shape, abridged to a single agent for brevity — your " +
"program must still define at least two agents:\n\n" +
DECOMPOSITION_SKELETON +
"\n\nReturn ONLY the complete TypeScript source of the program (no explanation, no markdown fences " +
"around it unless they are stripped before use).";
// Agent role: write a self-contained rig program that decomposes the task across at least two agents.
const decompositionWriter = agent({
model: "medium",
maxTurns: 1,
instructions: WRITER_CONTRACT,
output: s.string,
});
// Agent role: fix a previously generated decomposition program using a captured error, or regenerate it from scratch.
const decompositionFixer = agent({
model: "medium",
maxTurns: 1,
instructions: WRITER_CONTRACT,
output: s.string,
});
// Agent role: grade both solutions against the success criteria and pick a winner.
const grader = agent({
model: "large",
maxTurns: 1,
instructions:
"Score each of the two candidate solutions 0-10 on how completely and correctly it satisfies the " +
"listed success criteria. Judge strictly on correctness and completeness of content only — never " +
"on length, verbosity, or which approach (single call vs decomposition) produced the solution. " +
"Pick a winner of exactly 'single-call', 'decomposed', or 'tie', and explain the comparison.",
output: s.object({
singleCallScore: s.number,
decomposedScore: s.number,
winner: s.enum("single-call", "decomposed", "tie"),
rationale: s.string,
}),
});
function stripFences(text: string): string {
const trimmed = text.trim();
const fenceMatch = trimmed.match(/^```(?:ts|typescript)?\s*\n([\s\S]*?)\n```\s*$/);
return fenceMatch ? fenceMatch[1].trim() : trimmed;
}
interface SubprocessResult {
status: "pass" | "fail" | "timeout";
output: string;
}
async function runRigCli(source: string, args: string[], timeoutMs: number): Promise<SubprocessResult> {
const { spawn } = await import("node:child_process");
return new Promise<SubprocessResult>((resolvePromise) => {
const child = spawn("node", ["rig.ts", ...args], {
cwd: process.cwd(),
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
child.kill("SIGKILL");
resolvePromise({ status: "timeout", output: (stdout + stderr).slice(0, 8000) || "(timed out, no output)" });
}, Math.max(1000, timeoutMs));
child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString(); });
child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
child.on("error", (err: Error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolvePromise({ status: "fail", output: `spawn error: ${err.message}` });
});
child.on("close", (code: number | null) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (code === 0) {
resolvePromise({ status: "pass", output: stdout });
} else {
resolvePromise({ status: "fail", output: (stderr + stdout).slice(0, 8000) });
}
});
child.stdin.write(source);
child.stdin.end();
});
}
interface AttemptRecord {
attempt: number;
sourceResult: "ok" | "empty";
typecheck: "pass" | "fail" | "timeout" | "skipped";
execute: "pass" | "fail" | "timeout" | "skipped";
typecheckOutput: string;
executeOutput: string;
note: string;
}
// Workflow role: orchestrate task selection, single-call solving, decomposed solving, and grading.
export default workflow({
meta: {
name: "rig-decomposition-bench",
description: "Benchmark single-call vs decomposed multi-agent solving on a daily generated task",
phases: ["Pick task", "Solve single-call", "Solve decomposed", "Grade"],
},
body: async ({ call, phase }) => {
phase("Pick task");
const task = await call(taskPicker, "Pick today's benchmark task.");
phase("Solve single-call");
const singleCallStart = Date.now();
let singleCallSolution = "";
if (task && remainingMs() > 15_000) {
const result = await call(
singleCallSolver,
`Task: ${task.title} (${task.domain})\n\n${task.description}\n\nSuccess criteria:\n` +
task.criteria.map((c: string, i: number) => `${i + 1}. ${c}`).join("\n"),
);
singleCallSolution = result ?? "";
}
const singleCallDurationMs = Date.now() - singleCallStart;
phase("Solve decomposed");
const decompStart = Date.now();
const attempts: AttemptRecord[] = [];
let decomposedSolution = "";
let decomposedSource = "";
let decomposedPass = false;
const taskPrompt = task
? `Task to decompose:\nTitle: ${task.title}\nDomain: ${task.domain}\nDescription: ${task.description}\n` +
`Success criteria:\n${task.criteria.map((c: string, i: number) => `${i + 1}. ${c}`).join("\n")}`
: "No task was generated; invent a reasonable concrete multi-step task and decompose it.";
let previousSource = "";
let previousError = "";
let previousWasEmpty = false;
for (let attemptNum = 1; attemptNum <= 2 && !decomposedPass; attemptNum++) {
if (remainingMs() < 30_000) break;
let rawSource: string | null;
if (attemptNum === 1) {
rawSource = await call(decompositionWriter, taskPrompt);
} else if (previousWasEmpty) {
rawSource = await call(
decompositionFixer,
`${taskPrompt}\n\nThe previous attempt produced no usable source at all (empty or ` +
"whitespace-only result). There is no source to fix. Generate the complete program from " +
"scratch following the task and the required skeleton exactly.",
);
} else {
rawSource = await call(
decompositionFixer,
`${taskPrompt}\n\nHere is the previously generated program source:\n\n\`\`\`ts\n${previousSource}\n\`\`\`\n\n` +
`It failed with this exact captured error:\n\n${previousError}\n\n` +
"Fix precisely what this error names while preserving everything that already worked. Do " +
"not invent unrelated API edits.",
);
}
const trimmed = (rawSource ?? "").trim();
if (!trimmed) {
attempts.push({
attempt: attemptNum,
sourceResult: "empty",
typecheck: "skipped",
execute: "skipped",
typecheckOutput: "",
executeOutput: "",
note: "Writer/fixer produced no usable source; next attempt (if any) regenerates from scratch.",
});
previousWasEmpty = true;
continue;
}
const source = stripFences(trimmed);
previousSource = source;
previousWasEmpty = false;
if (remainingMs() < 20_000) {
attempts.push({
attempt: attemptNum,
sourceResult: "ok",
typecheck: "skipped",
execute: "skipped",
typecheckOutput: "",
executeOutput: "",
note: "Skipped verification: insufficient time budget remaining.",
});
break;
}
const typecheckTimeout = clamp(2 * 60_000, 15_000);
const typecheckResult = await runRigCli(source, ["--typecheck"], typecheckTimeout);
if (typecheckResult.status !== "pass") {
previousError = `typecheck ${typecheckResult.status}:\n${typecheckResult.output}`;
attempts.push({
attempt: attemptNum,
sourceResult: "ok",
typecheck: typecheckResult.status,
execute: "skipped",
typecheckOutput: typecheckResult.output,
executeOutput: "",
note:
attemptNum < 2
? "Next attempt asks the fixer to resolve this exact typecheck error."
: "No further attempts remain.",
});
decomposedSource = source;
continue;
}
if (remainingMs() < 15_000) {
attempts.push({
attempt: attemptNum,
sourceResult: "ok",
typecheck: "pass",
execute: "skipped",
typecheckOutput: typecheckResult.output,
executeOutput: "",
note: "Skipped execution: insufficient time budget remaining after typecheck.",
});
decomposedSource = source;
break;
}
const executeTimeout = clamp(5 * 60_000, 20_000);
const executeResult = await runRigCli(source, ["--server"], executeTimeout);
decomposedSource = source;
if (executeResult.status !== "pass") {
previousError = `execute ${executeResult.status}:\n${executeResult.output}`;
attempts.push({
attempt: attemptNum,
sourceResult: "ok",
typecheck: "pass",
execute: executeResult.status,
typecheckOutput: typecheckResult.output,
executeOutput: executeResult.output,
note:
attemptNum < 2
? "Next attempt asks the fixer to resolve this exact execute error."
: "No further attempts remain.",
});
continue;
}
// Execute succeeded; parse the final solution from stdout JSON.
let parsedSolution = "";
try {
const parsed = JSON.parse(executeResult.output);
if (parsed && typeof parsed === "object" && typeof parsed.solution === "string") {
parsedSolution = parsed.solution;
} else if (typeof parsed === "string") {
parsedSolution = parsed;
} else {
parsedSolution = executeResult.output;
}
} catch {
parsedSolution = executeResult.output;
}
decomposedSolution = parsedSolution;
decomposedPass = true;
attempts.push({
attempt: attemptNum,
sourceResult: "ok",
typecheck: "pass",
execute: "pass",
typecheckOutput: typecheckResult.output,
executeOutput: executeResult.output,
note: "Succeeded; no further attempts needed.",
});
}
const decompDurationMs = Date.now() - decompStart;
phase("Grade");
const placeholderSolution =
"Decomposition produced no valid solution within the allotted attempts and time budget.";
let grading: Awaited<ReturnType<typeof grader>> | null = null;
if (remainingMs() > 15_000) {
grading = await call(
grader,
`${taskPrompt}\n\n` +
`--- Single-call solution ---\n${singleCallSolution || "(no solution produced)"}\n\n` +
`--- Decomposed solution ---\n${decomposedPass ? decomposedSolution : placeholderSolution}`,
);
}
return {
task,
singleCallDurationMs,
decompDurationMs,
singleCallSolution,
decomposedSolution: decomposedPass ? decomposedSolution : placeholderSolution,
decomposedPass,
attempts,
grading,
decomposedSource,
};
},
});
Verdict
Winner: tie (both failed). The single-call solver and both decomposition-writer/fixer attempts all returned unparseable/empty structured output from their respective s.string-constrained, single-turn, no-repair() agent calls, so neither approach produced any graded content — the run did not meaningfully compare single-call vs. decomposed performance this time, only that both paths are susceptible to the same raw-JSON-parsing failure mode when a model returns prose instead of a bare JSON string at maxTurns: 1.
Generated by Daily Rig Decomposition Benchmark · copilot · auto · 203.1 AIC · ⌖ 14 AIC · ⊞ 9.9K · ◷
- expires on Oct 18, 2026, 1:20 PM UTC
- Vorherrschende Sprache
- TypeScript
- Sterne
- 13
- Forks
- 0
- Ø Merge
- 3 Std. 44 Min.
- Gemergte PRs (30 T.)
- 28
Entwicklungsumgebung
Dieses Projekt bietet weder Dev-Container noch Dockerfile noch Beitragsleitfaden – die Einrichtung liegt bei Ihnen. Beginnen Sie mit der README; die allgemeinen Schritte stehen in unserem Leitfaden für den ersten Beitrag.
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus githubnext/rig
-
[aw] Upgrade availableOffenagentic-workflows
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 48/100
githubnext/rig#634 ·
Maintainer antworten meist innerhalb von 1 Tag
-
agentic-workflows
Schwierigkeit 4/5 3-5 Tage Anfängerfreundlichkeit 35/100
githubnext/rig#633 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
agentic-workflows
Schwierigkeit 4/5 3-5 Tage Anfängerfreundlichkeit 30/100
githubnext/rig#631 ·
Maintainer antworten meist innerhalb von 1 Tag
-
ai-agent automation
Schwierigkeit 5/5 Über eine Woche Anfängerfreundlichkeit 1/100
githubnext/rig#630 ·
Maintainer antworten meist innerhalb von 1 Tag
-
agentic-workflows
Schwierigkeit 3/5 1-2 Tage Anfängerfreundlichkeit 45/100
githubnext/rig#629 ·
Maintainer antworten meist innerhalb von 1 Tag
Ähnliche Issues
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 85/100
-
area:cowork area:desktop documentation enhancement platform:macos
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
anthropics/claude-code#101720 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 77/100
apache/rocketmq-dashboard#6610 ·
Maintainer antworten meist innerhalb von 3 Tagen
-
Add: MS NOWOffencheck:passed streams:add
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 65/100
Maintainer antworten meist innerhalb von 1 Tag
-
Global update drops overrideAccess, so Local API updateGlobal fails filterOptions validation (3.90 regression)Evtl. vergeben @PINYOPATTANAWASANPORN hat das heute übernommen. Offenarea: core Bug
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
payloadcms/payload#18674 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag