opencode-usage-coach 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +92 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1821,7 +1821,7 @@ async function UsageCoachPlugin(input) {
|
|
|
1821
1821
|
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
1822
1822
|
currentAgent = agent;
|
|
1823
1823
|
refreshBackground();
|
|
1824
|
-
const harnessTools = ["unknown_scan", "question", "generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure", "reverse_interview"];
|
|
1824
|
+
const harnessTools = ["unknown_scan", "question", "generate", "generate_batch", "grade", "grade_batch", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure", "reverse_interview"];
|
|
1825
1825
|
if (!harnessTools.includes(_input.tool)) return;
|
|
1826
1826
|
if (!isHarnessAgent(agent)) {
|
|
1827
1827
|
throw new Error(`[${PLUGIN_NAME}] '${_input.tool}' is restricted to agent mode ${JSON.stringify(HARNESS_AGENTS)} (current: ${JSON.stringify(agent || "unknown")}). Switch to that agent mode to use it.`);
|
|
@@ -1885,13 +1885,16 @@ DETERMINISTIC LOOP \u2014 first classify the tasks:
|
|
|
1885
1885
|
INDEPENDENT = task B does NOT need task A's output -> use PATH A (parallel, faster)
|
|
1886
1886
|
DEPENDENT = task B needs task A's output -> use PATH B (sequential)
|
|
1887
1887
|
|
|
1888
|
-
PATH A \u2014 INDEPENDENT (parallel via generate_batch):
|
|
1888
|
+
PATH A \u2014 INDEPENDENT (parallel via generate_batch + grade_batch):
|
|
1889
1889
|
1. task_update(1..${args.total}, title, "generating")
|
|
1890
1890
|
2. generate_batch({tasks: [{id:1, prompt:"Task: <title1>. Perform it."}, ...]}) -> all results + NEXT
|
|
1891
|
-
3.
|
|
1892
|
-
4.
|
|
1891
|
+
3. task_update(1..${args.total}, title, "grading")
|
|
1892
|
+
4. grade_batch({tasks: [{id:1, prompt:"Evaluate... PASS/FAIL first line. Task: <title1>"}, ...]}) -> all verdicts + per-task NEXT
|
|
1893
|
+
5. for each i: PASS -> task_update(i, title, "completed", "PASS"); FAIL -> revise (up to 2x) or task_update(i, title, "failed", "FAIL")
|
|
1894
|
+
Note: grade_batch runs all grades IN PARALLEL \u2014 much faster than grading one-by-one.
|
|
1895
|
+
After revisions, you can grade_batch the revised tasks together too.
|
|
1893
1896
|
|
|
1894
|
-
PATH B \u2014 DEPENDENT (sequential):
|
|
1897
|
+
PATH B \u2014 DEPENDENT (sequential \u2014 one task at a time):
|
|
1895
1898
|
Optional: task_update(1..N, title, "pending") \u2190 pre-register all tasks first
|
|
1896
1899
|
for i in 1..${args.total}:
|
|
1897
1900
|
1. task_update(i, title, "generating")
|
|
@@ -1899,6 +1902,7 @@ PATH B \u2014 DEPENDENT (sequential):
|
|
|
1899
1902
|
3. task_update(i, title, "grading")
|
|
1900
1903
|
4. grade(...) -> verdict + NEXT
|
|
1901
1904
|
5. PASS -> task_update(i, title, "completed", "PASS"); FAIL -> revise (up to 2x) or failed
|
|
1905
|
+
Note: use single grade() here \u2014 tasks are dependent, grading is one at a time.
|
|
1902
1906
|
|
|
1903
1907
|
Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns. Do NOT improvise the sequence.`;
|
|
1904
1908
|
}
|
|
@@ -2521,6 +2525,89 @@ The next generate call will automatically include the new rule.`;
|
|
|
2521
2525
|
return out + "\n" + next;
|
|
2522
2526
|
}
|
|
2523
2527
|
}),
|
|
2528
|
+
grade_batch: tool({
|
|
2529
|
+
description: "Run the GRADER model on MULTIPLE tasks IN PARALLEL. Quota-aware: GO = full parallel; THROTTLE = concurrency capped at 2; STOP = refused. Use after generate_batch returns results for all INDEPENDENT tasks \u2014 grade them all at once instead of one-by-one. Each result includes a per-task PASS/FAIL verdict + NEXT directive. Resilient: failed grades are retried sequentially (once).",
|
|
2530
|
+
args: {
|
|
2531
|
+
tasks: tool.schema.array(tool.schema.object({
|
|
2532
|
+
id: tool.schema.number().describe("Task ID (must match the task_update ID)."),
|
|
2533
|
+
prompt: tool.schema.string().describe("The grading prompt for this task (same format as grade()).")
|
|
2534
|
+
})).describe("One entry per task to grade. All run in parallel on GO.")
|
|
2535
|
+
},
|
|
2536
|
+
async execute(args, ctx) {
|
|
2537
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
2538
|
+
const model = cfg.grader ?? cfg.generator;
|
|
2539
|
+
if (!model) {
|
|
2540
|
+
return 'ERROR: no grader/generator model configured. Set "grader" or "generator" in harness.config.json.';
|
|
2541
|
+
}
|
|
2542
|
+
let decision = "GO";
|
|
2543
|
+
try {
|
|
2544
|
+
decision = current().decision;
|
|
2545
|
+
} catch {
|
|
2546
|
+
}
|
|
2547
|
+
if (decision === "STOP") {
|
|
2548
|
+
return 'ERROR: quota STOP \u2014 halt the harness loop now. Call task_update(current, "halted_quota") and stop.';
|
|
2549
|
+
}
|
|
2550
|
+
const limit = decision === "THROTTLE" ? 2 : args.tasks.length;
|
|
2551
|
+
const gradeOne = async (t) => {
|
|
2552
|
+
const gradeTaskId = findActiveTaskId(ctx.sessionID, "grading");
|
|
2553
|
+
const out = await runModel(
|
|
2554
|
+
input.client,
|
|
2555
|
+
model,
|
|
2556
|
+
t.prompt,
|
|
2557
|
+
ctx.directory,
|
|
2558
|
+
gradeTaskId ? { sessionID: ctx.sessionID, taskId: gradeTaskId } : void 0
|
|
2559
|
+
);
|
|
2560
|
+
let verdict = "FAIL";
|
|
2561
|
+
if (!out.startsWith("ERROR:")) {
|
|
2562
|
+
const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
2563
|
+
if (/^pass\b/i.test(f)) verdict = "PASS";
|
|
2564
|
+
else if (/^fail\b/i.test(f)) verdict = "FAIL";
|
|
2565
|
+
}
|
|
2566
|
+
const next = verdict === "PASS" ? `[usage-coach NEXT] task ${t.id}: PASS -> task_update(${t.id}, title, "completed", "PASS"), then proceed.` : `[usage-coach NEXT] task ${t.id}: FAIL -> if revisions < 2: task_update(${t.id}, title, "revising") + generate({prompt:"Apply feedback:\\n${out.slice(0, 500)}\\nTask: {title}"}); else: record_failure + investigate + verify + generalize, then task_update(${t.id}, title, "failed", "FAIL").`;
|
|
2567
|
+
return { id: t.id, result: `[task ${t.id}] ${verdict}
|
|
2568
|
+
${out}
|
|
2569
|
+
${next}` };
|
|
2570
|
+
};
|
|
2571
|
+
const results = [];
|
|
2572
|
+
const failed = [];
|
|
2573
|
+
for (let i = 0; i < args.tasks.length; i += limit) {
|
|
2574
|
+
const batch = args.tasks.slice(i, i + limit);
|
|
2575
|
+
const settled = await Promise.allSettled(batch.map((t) => gradeOne(t)));
|
|
2576
|
+
for (let j = 0; j < settled.length; j++) {
|
|
2577
|
+
const s = settled[j];
|
|
2578
|
+
const t = batch[j];
|
|
2579
|
+
if (s.status === "fulfilled") {
|
|
2580
|
+
results.push(s.value.result);
|
|
2581
|
+
} else {
|
|
2582
|
+
const err = String(s.reason ?? "unknown rejection");
|
|
2583
|
+
log(`grade_batch task ${t.id} REJECTED: ${err}`);
|
|
2584
|
+
failed.push({ id: t.id, prompt: t.prompt, error: err });
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
if (failed.length > 0) {
|
|
2589
|
+
log(`grade_batch: retrying ${failed.length} failed grade(s) sequentially`);
|
|
2590
|
+
for (const f of failed) {
|
|
2591
|
+
try {
|
|
2592
|
+
const r = await gradeOne(f);
|
|
2593
|
+
results.push(r.result.replace(`[task ${r.id}]`, `[task ${r.id}] (retry)`));
|
|
2594
|
+
} catch (e2) {
|
|
2595
|
+
const err2 = String(e2 ?? "unknown rejection on retry");
|
|
2596
|
+
log(`grade_batch task ${f.id} retry ALSO FAILED: ${err2}`);
|
|
2597
|
+
results.push(`[task ${f.id}] FAIL
|
|
2598
|
+
(ERROR: grade failed on both batch and retry: ${err2})
|
|
2599
|
+
[usage-coach NEXT] task ${f.id}: grade error \u2014 re-run grade() individually for this task.`);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
const throttleNote = decision === "THROTTLE" ? `
|
|
2604
|
+
[usage-coach] quota THROTTLE \u2014 grader concurrency capped at ${limit}.` : "";
|
|
2605
|
+
const passCount = results.filter((r) => /\[task \d+\] PASS/.test(r)).length;
|
|
2606
|
+
const summary = `
|
|
2607
|
+
[usage-coach] Graded ${results.length} task(s): ${passCount} PASS, ${results.length - passCount} FAIL.`;
|
|
2608
|
+
return results.join("\n\n") + summary + throttleNote;
|
|
2609
|
+
}
|
|
2610
|
+
}),
|
|
2524
2611
|
reverse_interview: tool({
|
|
2525
2612
|
description: "Reverse interview: identify ambiguities in the task and ask the user one question at a time, highest design-impact first. Call WITHOUT answer to start or get the next question. Call WITH answer to record the user's response and advance. Returns the next question or a completion summary. ALWAYS present the question to the user verbatim \u2014 do NOT answer it yourself.",
|
|
2526
2613
|
args: {
|
package/package.json
CHANGED