[rig-decomposition-bench] Daily rig decomposition benchmark — 2026-10-11 — tie
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 45/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- github, typescript
- Área
- testing-qa
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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
- Lenguaje dominante
- TypeScript
- Estrellas
- 13
- Forks
- 0
- Merge medio
- 1 h 40 min
- PR fusionados (30 d)
- 26
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de githubnext/rig
-
agentic-workflows
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
githubnext/rig#625 ·
Los mantenedores suelen responder en 1 día
-
ai-agent automation
Dificultad 1/5 Menos de una hora Aptitud para principiantes 15/100
githubnext/rig#624 ·
Los mantenedores suelen responder en 1 día
-
ai-agent automation
Dificultad 5/5 Más de una semana Aptitud para principiantes 1/100
githubnext/rig#623 ·
Los mantenedores suelen responder en 1 día
-
agentic-workflows
Dificultad 2/5 1-3 horas Aptitud para principiantes 28/100
githubnext/rig#622 ·
Los mantenedores suelen responder en 1 día
-
ai-agent automation
Dificultad 2/5 1-3 horas Aptitud para principiantes 20/100
githubnext/rig#620 ·
Los mantenedores suelen responder en 1 día
Todos los issues de githubnext/rig
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
microsoft/playwright#43327 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
microsoft/TypeScript#64758 ·
Los mantenedores suelen responder en 1 día
-
enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
opensearch-project/OpenSearch-Dashboards#12937 ·
Los mantenedores suelen responder en 2 días
-
Liveness marks block-included attesters live in the block's epoch instead of the attestation's epochAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
Los mantenedores suelen responder en 1 día
-
status: needs-triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 83/100
a2ui-project/a2ui#3158 ·
Los mantenedores suelen responder en 2 días