Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[rig-decomposition-bench] Daily rig decomposition benchmark — 2026-10-17 — single-call

Aperta
#663 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
42/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
json, python, typescript

Direzione di ricerca

Start with the benchmark harness and the generated .tmp/rig-stdin-WDzcey/program.ts failure at line 40, then inspect the writer path that returned null or whitespace-only output on attempt 2. Re-run the single-call and decomposed benchmark configurations and compare source generation, typecheck, and execution stages. Done means decomposed attempts consistently produce nonempty source, pass typecheck, execute, and no longer end with the recorded fail status.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

ai-agent automation

Task

Hospital Bed-Capacity Forecast Pipeline: Design, Implementation, and Review — domain: Healthcare operations / data engineering

A regional hospital network needs a daily bed-capacity forecasting tool, built in one day from an inline spec. The data is a CSV of 90 days of admissions, discharges, and transfers for 4 wards (ICU, Medical, Surgical, Pediatrics). The CSV is described inline with its columns: date, ward, admissions, discharges, transfers_in, transfers_out, staffed_beds. Split the work across sub-agents on different models in three phases. (1) Design: one agent writes a short design doc. It covers the data-cleaning rules (missing days, negative values, duplicate rows), the forecasting method (a 7-day moving average with day-of-week seasonality), and the alert thresholds (occupancy above 85% is a warning, above 95% is critical). (2) Implementation: a second agent writes a Python module with pytest tests that implement the design. It computes the daily occupancy per ward, forecasts the next 7 days, and emits a JSON report of the wards that breach a threshold. (3) Review: a third agent independently reviews the code and design for correctness bugs, edge cases (zero staffed beds, a ward with a single day of data, a DST/date gap), and the clinical-safety implications of false negatives. The final deliverable combines the design doc, the code, the tests, and a review summary. The summary lists each issue found and how it was resolved.

Success criteria:

  • The design doc specifies explicit rules for missing dates, negative values, and duplicate rows. It also states the forecasting formula and both occupancy thresholds (85% warning, 95% critical).
  • The Python module computes occupancy per ward as (previous occupancy + admissions + transfers_in - discharges - transfers_out) / staffed_beds. It returns a 7-day forecast for each of the 4 wards.
  • The module handles staffed_beds = 0 without raising an uncaught exception. It handles a ward with fewer than 7 days of data without raising one.
  • The pytest suite has at least 6 tests covering normal forecasting, threshold boundaries (exactly 85% and 95%), duplicate rows, missing dates, zero staffed beds, and JSON report structure. All tests pass when run.
  • The JSON report lists every ward that breaches a threshold. Each entry has the ward name, the severity (warning or critical), the forecast date, and the forecast occupancy percentage.
  • The review summary names at least 3 distinct issues or risks, including one about false negatives in clinical safety. For each, it states whether it was fixed or deliberately accepted, with a reason.

Timing

Approach Duration (ms)
Single call 88841
Decomposed 25724

Decomposed final status: fail

Decomposition attempts

Attempt 1
  • Source generation: ok
  • Typecheck: fail
  • Execute: skipped
Captured error
Typecheck failed for <stdin>.
.tmp/rig-stdin-wDzcey/program.ts(40,31): error TS6133: 'log' is declared but its value is never read.
Attempt 2
  • Source generation: empty/null
  • Typecheck: skipped
  • Execute: skipped
Captured error
writer returned null or whitespace-only output

Note: attempt 1 failed typecheck (unused log); attempt 2 (fix request) returned null/empty from the writer, so nothing was fixed. (The benchmark record's own fixNote field said "regenerated from scratch", which is inaccurate: a source existed.)

Grading

  • Single-call: 7/10
  • Decomposed: 0/10
  • Winner: single-call

Rationale: Solution A covers all six criteria. The design doc has explicit rules for missing dates, negative values and duplicate rows, plus the forecast formula and both thresholds. The module implements the census and occupancy recurrence and returns a 7-day forecast per ward. It handles zero staffed beds and wards with fewer than 7 days of data without raising. The pytest suite has more than 6 tests covering the required cases, and the JSON report has the required fields. The review summary lists 9 issues, including false-negative clinical-safety risks, each marked fixed or accepted with a reason. Weaknesses: the tests were not actually executed, and the answer says so, and the work was not split across sub-agents as the task asked. Solution B produced no solution, so it scores 0.

Single-call solution

Hospital Bed-Capacity Forecast Pipeline

The phases were run by three roles: Designer, Implementer and Reviewer. The tests were not executed in this response. They were hand-traced against the code, so run pytest -q to confirm.

1. Design doc

Input: CSV with columns date, ward, admissions, discharges, transfers_in, transfers_out, staffed_beds. Wards are ICU, Medical, Surgical and Pediatrics, over 90 days. Dates are calendar dates (YYYY-MM-DD) with no time or timezone, so DST cannot shift a day.

Cleaning rules

  • Missing dates: per ward, build a continuous daily range from its first date to the dataset's latest date. A missing day gets zero flows, and staffed_beds is carried forward from the last known value. Each ward is padded to the global end date, so a stale ward is not forecast from old data.
  • Negative values: a negative flow is clamped to 0 and logged. A negative staffed_beds is treated as 0 and logged. Census is floored at 0.
  • Unparseable values: a blank or non-numeric flow is treated as 0 and logged. A row with a bad date or no ward is dropped and logged.
  • Duplicate rows: duplicates are keyed on (date, ward). The last row wins, so an exact duplicate is never double-counted. A conflicting duplicate is also resolved as last-wins, and both cases are logged.
  • Ward names: names are matched case-insensitively and stripped of whitespace.

Census and occupancy

  • census_t = max(0, census_{t-1} + admissions + transfers_in - discharges - transfers_out).
  • occupancy_t = census_t / staffed_beds_t.
  • The starting census is 0 by default. A caller can supply initial_census per ward.

Forecast (per ward, 7 days)

  • base = mean(census over last min(7, n) days).
  • factor[dow] = mean(census on that weekday) / mean(census overall). It is used only when n >= 7 and the overall mean is above 0. Otherwise the factor is 1.0.
  • forecast_census(date) = base * factor[weekday(date)].
  • forecast_occupancy = forecast_census / last staffed_beds.

Alert thresholds (strictly above)

  • Occupancy above 85% is a warning. Occupancy above 95% is critical.
  • Exactly 85% gives no alert. Exactly 95% is a warning.
  • If staffed_beds is 0 and census is above 0, occupancy is undefined and the result is critical. If staffed_beds is 0 and census is 0, there is no alert.

Report: JSON with thresholds, breaching_wards, alerts and data_issues. Each alert has ward, severity, forecast_date and forecast_occupancy_pct. There is one alert per breaching ward per forecast date.

2. Code: bedforecast.py

import csv
import datetime as dt
import io
import json
import sys
from collections import defaultdict

WARDS = ('ICU', 'Medical', 'Surgical', 'Pediatrics')
WARNING_PCT = 85.0
CRITICAL_PCT = 95.0
HORIZON = 7
EPS = 1e-9
FLOWS = ('admissions', 'discharges', 'transfers_in', 'transfers_out')


def _num(v):
    try:
        return int(float(str(v).strip()))
    except (TypeError, ValueError):
        return None


def _ward(name):
    n = (name or '').strip()
    for w in WARDS:
        if w.lower() == n.lower():
            return w
    return n


def clean_rows(rows):
    issues = []
    recs = defaultdict(dict)
    for i, r in enumerate(rows):
        try:
            d = dt.date.fromisoformat(str(r.get('date', '')).strip()[:10])
        except ValueError:
            issues.append('row %d: bad date, dropped' % i)
            continue
        ward = _ward(r.get('ward'))
        if not ward:
            issues.append('row %d: missing ward, dropped' % i)
            continue
        rec = {}
        for k in FLOWS:
            v = _num(r.get(k))
            if v is None or v < 0:
                issues.append('row %d: invalid %s set to 0' % (i, k))
                v = 0
            rec[k] = v
        b = _num(r.get('staffed_beds'))
        if b is not None and b < 0:
            issues.append('row %d: negative staffed_beds set to 0' % i)
            b = 0
        rec['staffed_beds'] = b
        if d in recs[ward]:
            issues.append('row %d: duplicate %s %s, last row kept' % (i, ward, d))
        recs[ward][d] = rec
    return recs, issues


def occupancy_pct(census, beds):
    return census * 100.0 / beds if beds > 0 else None


def build_series(ward_recs, end=None, initial_census=0):
    if not ward_recs:
        return []
    start = min(ward_recs)
    last_obs = max(ward_recs)
    end = max(end, last_obs) if end else last_obs
    census = initial_census
    beds = None
    out = []
    d = start
    while d <= end:
        rec = ward_recs.get(d)
        net = 0
        if rec:
            net = (rec['admissions'] + rec['transfers_in']
                   - rec['discharges'] - rec['transfers_out'])
            if rec['staffed_beds'] is not None:
                beds = rec['staffed_beds']
        census = max(0, census + net)
        b = beds or 0
        out.append({'date': d, 'census': census, 'staffed_beds': b,
                    'occupancy_pct': occupancy_pct(census, b)})
        d += dt.timedelta(days=1)
    return out


def severity(pct, census):
    if pct is None:
        return 'critical' if census > EPS else None
    if pct > CRITICAL_PCT + EPS:
        return 'critical'
    if pct > WARNING_PCT + EPS:
        return 'warning'
    return None


def forecast_ward(series, horizon=HORIZON):
    if not series:
        return []
    c = [s['census'] for s in series]
    recent = c[-7:]
    base = sum(recent) / len(recent)
    overall = sum(c) / len(c)
    factors = {}
    if len(c) >= 7 and overall > 0:
        by = defaultdict(list)
        for s in series:
            by[s['date'].weekday()].append(s['census'])
        factors = {k: (sum(v) / len(v)) / overall for k, v in by.items()}
    last = series[-1]
    out = []
    for i in range(1, horizon + 1):
        d = last['date'] + dt.timedelta(days=i)
        fc = base * factors.get(d.weekday(), 1.0)
        pct = occupancy_pct(fc, last['staffed_beds'])
        out.append({'date': d.isoformat(), 'forecast_census': fc,
                    'staffed_beds': last['staffed_beds'],
                    'occupancy_pct': pct,
                    'severity': severity(pct, fc)})
    return out


def forecast_all(rows, initial_census=None):
    initial_census = initial_census or {}
    recs, issues = clean_rows(rows)
    all_dates = [d for w in recs.values() for d in w]
    end = max(all_dates) if all_dates else None
    wards = list(WARDS) + sorted(w for w in recs if w not in WARDS)
    result = {}
    for w in wards:
        series = build_series(recs.get(w, {}), end, initial_census.get(w, 0))
        result[w] = forecast_ward(series)
    return result, issues


def build_report(forecasts, issues=()):
    alerts = []
    for ward, days in forecasts.items():
        for f in days:
            if f['severity']:
                pct = f['occupancy_pct']
                alerts.append({
                    'ward': ward,
                    'severity': f['severity'],
                    'forecast_date': f['date'],
                    'forecast_occupancy_pct': None if pct is None else round(pct, 2),
                })
    alerts.sort(key=lambda a: (a['forecast_date'], a['ward']))
    return {
        'thresholds': {'warning_pct': WARNING_PCT, 'critical_pct': CRITICAL_PCT},
        'breaching_wards': sorted({a['ward'] for a in alerts}),
        'alerts': alerts,
        'data_issues': list(issues),
    }


def generate_report(csv_text, initial_census=None):
    rows = list(csv.DictReader(io.StringIO(csv_text)))
    forecasts, issues = forecast_all(rows, initial_census)
    return json.dumps(build_report(forecasts, issues), indent=2)


if __name__ == '__main__':
    with open(sys.argv[1], newline='') as fh:
        print(generate_report(fh.read()))

3. Tests: test_bedforecast.py

import csv
import datetime as dt
import io
import json

import pytest

import bedforecast as bf

START = dt.date(2024, 1, 1)


def census_rows(ward, census_list, beds=100, start=START):
    rows, prev = [], 0
    for i, c in enumerate(census_list):
        diff = c - prev
        prev = c
        rows.append({'date': (start + dt.timedelta(days=i)).isoformat(),
                     'ward': ward, 'admissions': max(diff, 0),
                     'discharges': max(-diff, 0), 'transfers_in': 0,
                     'transfers_out': 0, 'staffed_beds': beds})
    return rows


def report_for(rows):
    forecasts, issues = bf.forecast_all(rows)
    return forecasts, bf.build_report(forecasts, issues)


def test_normal_forecast_all_four_wards():
    rows = []
    for w in bf.WARDS:
        rows += census_rows(w, [80] * 14)
    forecasts, report = report_for(rows)
    assert set(forecasts) == set(bf.WARDS)
    for w in bf.WARDS:
        assert len(forecasts[w]) == 7
        assert all(f['occupancy_pct'] == pytest.approx(80) for f in forecasts[w])
    assert report['alerts'] == []


def test_day_of_week_seasonality():
    pattern = ([60] * 6 + [90]) * 2
    forecasts, report = report_for(census_rows('ICU', pattern))
    f = forecasts['ICU']
    assert f[-1]['date'] == '2024-01-21'
    assert f[-1]['occupancy_pct'] == pytest.approx(90)
    assert f[0]['occupancy_pct'] == pytest.approx(60)
    assert [a['forecast_date'] for a in report['alerts']] == ['2024-01-21']


@pytest.mark.parametrize('census,expected', [
    (85, None), (86, 'warning'), (95, 'warning'), (96, 'critical')])
def test_threshold_boundaries(census, expected):
    _, report = report_for(census_rows('ICU', [census] * 14))
    sev = {a['severity'] for a in report['alerts']}
    assert sev == (set() if expected is None else {expected})


def test_duplicate_rows():
    rows = census_rows('ICU', [50])
    exact = rows + [dict(rows[0])]
    series = bf.build_series(bf.clean_rows(exact)[0]['ICU'])
    assert series[0]['census'] == 50
    conflict = rows + [dict(rows[0], admissions=60)]
    recs, issues = bf.clean_rows(conflict)
    assert bf.build_series(recs['ICU'])[0]['census'] == 60
    assert any('duplicate' in i for i in issues)


def test_missing_dates_are_filled():
    rows = census_rows('ICU', [50, 50, 50])
    del rows[1]
    series = bf.build_series(bf.clean_rows(rows)[0]['ICU'])
    assert len(series) == 3
    assert series[1]['date'] == dt.date(2024, 1, 2)
    assert series[1]['census'] == 50


def test_date_gap_across_dst_change():
    rows = census_rows('ICU', [10, 10, 10], start=dt.date(2024, 3, 9))
    del rows[1]
    series = bf.build_series(bf.clean_rows(rows)[0]['ICU'])
    assert [s['date'].isoformat() for s in series] == [
        '2024-03-09', '2024-03-10', '2024-03-11']


def test_zero_staffed_beds():
    forecasts, report = report_for(census_rows('ICU', [5, 5, 5], beds=0))
    assert len(forecasts['ICU']) == 7
    assert len(report['alerts']) == 7
    assert all(a['severity'] == 'critical' and a['forecast_occupancy_pct'] is None
               for a in report['alerts'])
    _, empty = report_for(census_rows('ICU', [0, 0], beds=0))
    assert empty['alerts'] == []


def test_single_day_ward():
    forecasts, report = report_for(census_rows('ICU', [90]))
    assert len(forecasts['ICU']) == 7
    assert {a['severity'] for a in report['alerts']} == {'warning'}
    assert forecasts['Medical'] == []


def test_negative_values_clamped():
    rows = census_rows('ICU', [10])
    rows[0]['admissions'] = -5
    recs, issues = bf.clean_rows(rows)
    assert bf.build_series(recs['ICU'])[0]['census'] == 0
    assert issues


def test_json_report_structure():
    rows = census_rows('ICU', [96] * 14) + census_rows('Medical', [50] * 14)
    buf = io.StringIO()
    writer = csv.DictWriter(buf, fieldnames=list(rows[0]))
    writer.writeheader()
    writer.writerows(rows)
    report = json.loads(bf.generate_report(buf.getvalue()))
    assert report['breaching_wards'] == ['ICU']
    assert len(report['alerts']) == 7
    for a in report['alerts']:
        assert set(a) == {'ward', 'severity', 'forecast_date',
                          'forecast_occupancy_pct'}
        assert a['severity'] == 'critical'
        assert a['forecast_occupancy_pct'] == 96.0

Run with pytest -q. There are 12 test cases covering normal forecasting, seasonality, the 85% and 95% boundaries, duplicates, missing dates, a DST gap, zero beds, a single-day ward, negative values and the JSON structure.

4. Review summary

# Issue / risk Status Reason
1 Clinical safety, false negatives (zero beds). A ward with 0 staffed beds and patients present would divide by zero. A naive fix would skip it and hide a real problem. Fixed Occupancy is None. If census is above 0 the result is critical with a null percentage. If census is 0 there is no alert. No exception is raised.
2 Clinical safety, false negatives (smoothing). A 7-day moving average lags a sudden surge, so a real breach can be underforecast. Deliberately accepted, with mitigation The method is fixed by the spec. Seasonality partly offsets the lag. The report lists every breaching date. Recommend a future safety-margin config such as alerting at 80%.
3 Clinical safety, false negatives (strict thresholds). Strict > means exactly 85% gives no alert and exactly 95% is only a warning. Deliberately accepted The spec says "above". The behaviour is tested and documented. Comparison uses an epsilon to avoid float errors such as 85.0000001.
4 Unknown starting census. The default of 0 understates census, and therefore occupancy, for the whole window. Partly fixed, remaining risk accepted The CSV has no census column, so the formula needs a seed. Added an initial_census parameter per ward. The CLI defaults to 0, so operators must supply true census.
5 Missing days or a stale ward feed. Imputing zero flows can hide patients. Fixed (padding), imputation accepted Every ward is padded to the global latest date so staleness is visible. Zero-flow imputation is the spec rule. It is logged as a data issue only when rows are dropped or clamped.
6 Single-day ward and fewer than 7 days. Seasonality would divide by sparse data. Fixed The mean uses min(7, n) days. Seasonal factors are applied only when n >= 7, otherwise they are 1.0. Tested.
7 DST or date gap. Fixed Dates are pure date objects advanced with timedelta(days=1), so there is no timezone arithmetic. Tested across 2024-03-10.
8 Negative values, duplicates and ward-name case. Fixed Negatives are clamped and logged. The last duplicate wins and is logged. Ward names are normalised.
9 Net-flow-only census drift. Data errors accumulate in the census over 90 days. Accepted This is inherent in the spec formula. Recommend reconciling with the real midnight census periodically.

Outcome: the code and tests were checked against the design. Issues 1 and 5 to 8 are fixed. Issues 2, 3 and 9 are accepted with reasons. Issue 4 is partly mitigated.

Decomposed program source (last generated; it failed typecheck, never executed)

import { agent, workflow, s } from "rig";

// Agent role: design doc author (medium model for careful specification)
const designer = agent({
  name: "designer",
  model: "medium",
  maxTurns: 3,
  instructions:
    "You are a healthcare data engineer. Write a concise design doc in Markdown for a hospital bed-capacity forecasting tool. Input CSV columns: date, ward, admissions, discharges, transfers_in, transfers_out, staffed_beds; 90 days; wards ICU, Medical, Surgical, Pediatrics. Specify explicit rules for: missing dates (reindex to a full daily range, carry forward the last known staffed_beds, treat missing flows as 0 and flag the day as imputed), negative values (reject or clamp to 0 and log a warning), and duplicate rows (same date and ward: keep the last row and log it; do not sum). State the occupancy formula: occupancy_t = (occupancy_{t-1} + admissions + transfers_in - discharges - transfers_out) / staffed_beds, with census tracked in patients. State the forecasting method: a 7-day moving average of net flow adjusted by day-of-week seasonality factors, with the factor equal to the mean net flow for that weekday divided by the overall mean, falling back to factor 1.0 when there are fewer than 7 days of data. Give thresholds: occupancy above 85% is a warning and above 95% is critical (strictly greater than). Define edge-case handling for staffed_beds = 0 and for single-day wards. Define the JSON report format: a list of entries with ward, severity, forecast_date and forecast_occupancy_pct. Output only the document text.",
  output: s.string,
});

// Agent role: implementer of the Python module and pytest tests (large model for the hardest coding step)
const implementer = agent({
  name: "implementer",
  model: "large",
  maxTurns: 6,
  instructions:
    "You are a senior Python engineer. Given a design doc, write a Python module bedcap.py and a pytest file test_bedcap.py that implement it. The module must: load and clean the CSV (missing dates, negatives, duplicates per the design); compute daily occupancy per ward using (previous occupancy + admissions + transfers_in - discharges - transfers_out) / staffed_beds; forecast the next 7 days for each of the 4 wards using a 7-day moving average with day-of-week seasonality; and emit a JSON report listing every ward and forecast date that breaches a threshold (above 85% is warning, above 95% is critical), with fields ward, severity, forecast_date and forecast_occupancy_pct. Handle staffed_beds=0 without raising (skip or treat as unknown and flag it) and handle wards with fewer than 7 days of data. Use only the standard library plus optional pandas. Write at least 8 pytest tests covering normal forecasting, the exact 85% and 95% boundaries, duplicate rows, missing dates, zero staffed beds, a single-day ward, and the JSON report structure. Make sure the tests would pass. Output the two files as labeled code blocks, with the filename on a header line before each.",
  output: s.string,
});

// Agent role: independent reviewer for correctness and clinical safety (large model for deep critique)
const reviewer = agent({
  name: "reviewer",
  model: "large",
  maxTurns: 4,
  instructions:
    "You are an independent code and clinical-safety reviewer. Review the design doc and the code and tests you are given. Find correctness bugs and edge cases: zero staffed beds, a ward with a single day of data, DST or date gaps, duplicate rows, negative values, and threshold boundary semantics. Analyze the clinical-safety implications of false negatives, such as an under-forecast that hides a breach, or a skipped ward that is silently dropped. Output a Markdown review summary with at least 3 distinct issues. For each, list the severity, the issue, and the resolution as either Fixed (with the concrete fix) or Deliberately accepted (with a reason). One issue must be about false negatives. Include any corrected code snippets.",
  output: s.string,
});

// Workflow role: runs the three phases in order and combines the design, code, tests and review into the final solution
export default workflow({
  meta: {
    name: "decomposed-solution",
    description:
      "Hospital bed-capacity forecast pipeline: design, implementation, review.",
  },
  body: async ({ call, phase, log }) => {
    phase("design");
    const design = (await call(designer, "Write the design doc for the hospital bed-capacity forecasting pipeline.")) ?? "";

    phase("implementation");
    const code = (await call(implementer, "Implement this design:\n\n" + design)) ?? "";

    phase("review");
    const review =
      (await call(
        reviewer,
        "Review the following design and implementation.\n\n# DESIGN\n" + design + "\n\n# CODE AND TESTS\n" + code,
      )) ?? "";

    const solution =
      "# Hospital Bed-Capacity Forecast Pipeline\n\n## 1. Design Doc\n\n" +
      design +
      "\n\n## 2. Code and Tests\n\n" +
      code +
      "\n\n## 3. Review Summary\n\n" +
      review +
      "\n";
    return { solution };
  },
});

Decomposed solution: empty (execution never ran; no valid JSON output).

Benchmark program
import { agent, workflow, s, repair, configureAgent, copilotEngine } from "rig";

configureAgent(copilotEngine({ server: true }));

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

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

async function runRig(source: string, flag: "--typecheck" | "--server", limitMs: number): Promise<Run> {
  const { spawn } = await import("node:child_process");
  const cwd = process.cwd();
  return new Promise<Run>((resolve) => {
    const child = spawn("node", ["rig.ts", flag], { cwd, stdio: ["pipe", "pipe", "pipe"] });
    let stdout = "";
    let stderr = "";
    let timedOut = false;
    const timer = setTimeout(() => {
      timedOut = true;
      child.kill("SIGKILL");
    }, Math.max(1000, limitMs));
    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({ ok: false, stdout, stderr: stderr + String(e), code: null, timedOut });
    });
    child.on("close", (code: number | null) => {
      clearTimeout(timer);
      resolve({ ok: code === 0 && !timedOut, stdout, stderr: timedOut ? stderr + "\n[timed out]" : stderr, code, timedOut });
    });
    child.stdin.on("error", () => {});
    child.stdin.end(source);
  });
}

function extractSolution(stdout: string): string | null {
  const tryParse = (t: string): string | null => {
    try {
      const v = JSON.parse(t) as { solution?: unknown };
      return typeof v?.solution === "string" ? v.solution : null;
    } catch {
      return null;
    }
  };
  const whole = tryParse(stdout.trim());
  if (whole !== null) return whole;
  const lines = stdout.trim().split("\n");
  for (let i = lines.length - 1; i >= 0; i--) {
    const r = tryParse(lines[i] ?? "");
    if (r !== null) return r;
  }
  const first = stdout.indexOf("{");
  const last = stdout.lastIndexOf("}");
  if (first >= 0 && last > first) return tryParse(stdout.slice(first, last + 1));
  return null;
}

// Agent role: pick a concrete, complicated, gradable 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; pick randomly rather than the first idea. Return a title, a domain, a 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 alone in one turn.
const soloSolver = agent({
  name: "soloSolver",
  model: "medium",
  maxTurns: 1,
  instructions:
    "Solve the entire task given in the input yourself, in a single response, addressing every success criterion. No delegation, no tools. Return the complete solution.",
  output: s.string,
});

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 using the "rig" library. Rules:
- Import only from "rig". Models are "small", "medium", "large". No required input on the root. Export the root with \`export default\` and never invoke it.
- Use at least two agents, each with a "// Agent role: ..." comment above it, choosing "small" for simple sub-steps and "medium" or "large" for harder ones. The final answer must combine their outputs, not come from one agent solving everything.
- 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, parallel, pipeline, phase, log }; call(agent, input) returns the agent output or null on failure.
- An agent() root instead would declare output: s.object({ solution: s.string }).
- Use s.string output for every agent whose result is source text or free-form solution. Do not wrap large solutions in object schemas.
- Agent fields: name, instructions, input, output, model, maxTurns. Agents take string input by default; pass a string prompt to call.
- No console.log. Reply with ONLY the program source, no Markdown fences, no commentary.
Required shape (abridged to one agent; you need at least two):
${SKELETON}`;

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

// Agent role: grade both solutions on content only.
const grader = agent({
  name: "grader",
  model: "large",
  maxTurns: 1,
  instructions:
    "You grade two solutions to the same task. Score each 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.",
  input: s.string,
  output: s.object({
    singleScore: s.number,
    decomposedScore: s.number,
    winner: s.enum("single-call", "decomposed", "tie"),
    rationale: s.string,
  }),
});

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

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

// Workflow role: pick a task, solve it two ways, grade both, and return the report.
export default workflow({
  meta: { name: "decomposition-bench", description: "Benchmark single-call vs decomposed solving" },
  body: async ({ call, phase }) => {
    phase("Pick");
    const task = await call(pickTask, "Pick the task now.", { label: "pick", timeout: 3 * 60_000 });
    if (!task) {
      return { status: "failed: task picker returned null", task: null, attempts: [], winner: "none" };
    }
    const taskText = `Title: ${task.title}\nDomain: ${task.domain}\nDescription: ${task.description}\nSuccess criteria:\n${task.criteria.map((c: string, i: number) => `${i + 1}. ${c}`).join("\n")}`;

    phase("Single");
    const t0 = Date.now();
    const single = await call(soloSolver, taskText, { label: "single", timeout: Math.max(5000, Math.min(8 * 60_000, left())) });
    const singleMs = Date.now() - t0;

    phase("Decompose");
    const t1 = Date.now();
    const attempts: Attempt[] = [];
    let source = "";
    let decomposedSolution: string | null = null;
    let status = "fail";

    for (let n = 1; n <= 2; n++) {
      if (left() < 4 * 60_000) break;
      const prev = attempts[attempts.length - 1];
      let prompt: string;
      if (n === 1) {
        prompt = `Write the program for this task.\n\n${taskText}`;
      } else if (source.trim()) {
        prompt = `Fix this program. Fix precisely what the error names and preserve what already worked.\n\nTask:\n${taskText}\n\nError:\n${prev?.error ?? ""}\n\nCurrent source:\n${source}\n\nRequired shape:\n${SKELETON}`;
      } 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, { label: `writer-${n}`, timeout: Math.max(5000, Math.min(6 * 60_000, left())) });
      const rec: Attempt = { attempt: n, generation: "", typecheck: "skipped", execute: "skipped", error: "", fixNote: "" };
      attempts.push(rec);
      if (raw === null || raw.trim() === "") {
        rec.generation = "empty/null";
        rec.error = "writer returned null or whitespace-only output";
        if (prev) prev.fixNote = "Next attempt regenerated the program from scratch (no source).";
        continue;
      }
      source = stripFences(raw);
      rec.generation = "ok";
      if (n > 1 && prev) prev.fixNote = "Next attempt fixed the source per the captured error.";
      const tc = await runRig(source, "--typecheck", Math.min(2 * 60_000, left()));
      if (!tc.ok) {
        rec.typecheck = "fail";
        rec.error = (tc.stderr + tc.stdout).trim();
        continue;
      }
      rec.typecheck = "pass";
      if (left() < 60_000) {
        rec.execute = "skipped (budget exhausted)";
        break;
      }
      const ex = await runRig(source, "--server", Math.min(5 * 60_000, left()));
      if (!ex.ok) {
        rec.execute = "fail";
        rec.error = (ex.stderr + "\n" + ex.stdout.slice(0, 2000)).trim();
        continue;
      }
      const sol = extractSolution(ex.stdout);
      if (sol === null || sol.trim() === "") {
        rec.execute = "fail";
        rec.error = "execution succeeded but no non-empty solution could be parsed from stdout:\n" + ex.stdout.slice(0, 2000);
        continue;
      }
      rec.execute = "pass";
      decomposedSolution = sol;
      status = "pass";
      break;
    }
    const decomposedMs = Date.now() - t1;

    phase("Grade");
    const singleText = single && single.trim() ? single : "(single-call solver produced no output)";
    const decompText = decomposedSolution ?? "(decomposition produced no solution)";
    const grade =
      left() > 30_000
        ? await call(
            grader,
            `${taskText}\n\n=== SOLUTION A (single-call) ===\n${singleText}\n\n=== SOLUTION B (decomposed) ===\n${decompText}`,
            { label: "grade", timeout: Math.max(5000, Math.min(5 * 60_000, left())) },
          )
        : null;

    return {
      status,
      task,
      singleMs,
      decomposedMs,
      singleSolution: single ?? "",
      decomposedSolution: decomposedSolution ?? "",
      attempts,
      grade,
      winner: grade?.winner ?? "none",
      decomposedSource: source,
    };
  },
});

Verdict: single-call wins — the decomposed program failed typecheck (unused variable) and the retry returned nothing, so it scored 0.

Generated by Daily Rig Decomposition Benchmark · copilot · auto · 81.5 AIC · ⌖ 0.795 AIC · ⊞ 9.9K · ◷

  • expires on Oct 24, 2026, 1:09 PM UTC
Lingua principale
TypeScript
Stelle
14
Fork
0
Merge medio
3h 38m
PR unite (30g)
29

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di githubnext/rig

Tutte le issue di githubnext/rig

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.