opencode-usage-coach 0.13.2 → 0.13.4
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/cli.js +2 -1
- package/dist/index.js +106 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -350,7 +350,8 @@ var DEFAULT_HARNESS_CONFIG = {
|
|
|
350
350
|
generator: "opencode/deepseek-v4-flash-free",
|
|
351
351
|
grader: "opencode/mimo-v2.5-free",
|
|
352
352
|
provider: "",
|
|
353
|
-
lighterModel: ""
|
|
353
|
+
lighterModel: "",
|
|
354
|
+
maxSteps: 30
|
|
354
355
|
};
|
|
355
356
|
function resolveAgentSourceFile() {
|
|
356
357
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
package/dist/index.js
CHANGED
|
@@ -434,6 +434,9 @@ async function searchContext(query, frameworks, keyDeps, timeoutMs) {
|
|
|
434
434
|
var PLUGIN_NAME = "opencode-usage-coach";
|
|
435
435
|
var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
|
|
436
436
|
var DEFAULT_MAX_STEPS = Number(process.env.UC_MAX_STEPS ?? 30) || 30;
|
|
437
|
+
function resolveMaxSteps(cfg, explicit) {
|
|
438
|
+
return explicit ?? cfg.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
439
|
+
}
|
|
437
440
|
var WATCHDOG_POLL_MS = Math.max(1e3, Number(process.env.UC_WATCHDOG_POLL_MS ?? 3e3) || 3e3);
|
|
438
441
|
var WALL_TIMEOUT_MS = Math.max(1, Number(process.env.UC_WALL_TIMEOUT_MIN ?? 30) || 30) * 60 * 1e3;
|
|
439
442
|
var DEFAULT_MAX_QUESTIONS = Math.max(1, Math.round(Number(process.env.UC_MAX_QUESTIONS ?? 7)) || 7);
|
|
@@ -1821,7 +1824,7 @@ async function UsageCoachPlugin(input) {
|
|
|
1821
1824
|
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
1822
1825
|
currentAgent = agent;
|
|
1823
1826
|
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"];
|
|
1827
|
+
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
1828
|
if (!harnessTools.includes(_input.tool)) return;
|
|
1826
1829
|
if (!isHarnessAgent(agent)) {
|
|
1827
1830
|
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 +1888,16 @@ DETERMINISTIC LOOP \u2014 first classify the tasks:
|
|
|
1885
1888
|
INDEPENDENT = task B does NOT need task A's output -> use PATH A (parallel, faster)
|
|
1886
1889
|
DEPENDENT = task B needs task A's output -> use PATH B (sequential)
|
|
1887
1890
|
|
|
1888
|
-
PATH A \u2014 INDEPENDENT (parallel via generate_batch):
|
|
1891
|
+
PATH A \u2014 INDEPENDENT (parallel via generate_batch + grade_batch):
|
|
1889
1892
|
1. task_update(1..${args.total}, title, "generating")
|
|
1890
1893
|
2. generate_batch({tasks: [{id:1, prompt:"Task: <title1>. Perform it."}, ...]}) -> all results + NEXT
|
|
1891
|
-
3.
|
|
1892
|
-
4.
|
|
1894
|
+
3. task_update(1..${args.total}, title, "grading")
|
|
1895
|
+
4. grade_batch({tasks: [{id:1, prompt:"Evaluate... PASS/FAIL first line. Task: <title1>"}, ...]}) -> all verdicts + per-task NEXT
|
|
1896
|
+
5. for each i: PASS -> task_update(i, title, "completed", "PASS"); FAIL -> revise (up to 2x) or task_update(i, title, "failed", "FAIL")
|
|
1897
|
+
Note: grade_batch runs all grades IN PARALLEL \u2014 much faster than grading one-by-one.
|
|
1898
|
+
After revisions, you can grade_batch the revised tasks together too.
|
|
1893
1899
|
|
|
1894
|
-
PATH B \u2014 DEPENDENT (sequential):
|
|
1900
|
+
PATH B \u2014 DEPENDENT (sequential \u2014 one task at a time):
|
|
1895
1901
|
Optional: task_update(1..N, title, "pending") \u2190 pre-register all tasks first
|
|
1896
1902
|
for i in 1..${args.total}:
|
|
1897
1903
|
1. task_update(i, title, "generating")
|
|
@@ -1899,6 +1905,7 @@ PATH B \u2014 DEPENDENT (sequential):
|
|
|
1899
1905
|
3. task_update(i, title, "grading")
|
|
1900
1906
|
4. grade(...) -> verdict + NEXT
|
|
1901
1907
|
5. PASS -> task_update(i, title, "completed", "PASS"); FAIL -> revise (up to 2x) or failed
|
|
1908
|
+
Note: use single grade() here \u2014 tasks are dependent, grading is one at a time.
|
|
1902
1909
|
|
|
1903
1910
|
Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns. Do NOT improvise the sequence.`;
|
|
1904
1911
|
}
|
|
@@ -2336,7 +2343,7 @@ ${gate.summary}
|
|
|
2336
2343
|
` + prefix;
|
|
2337
2344
|
}
|
|
2338
2345
|
const genTaskId = findActiveTaskId(ctx.sessionID, "generating");
|
|
2339
|
-
const maxSteps = args.max_steps
|
|
2346
|
+
const maxSteps = resolveMaxSteps(cfg, args.max_steps);
|
|
2340
2347
|
const out = await runModel(
|
|
2341
2348
|
input.client,
|
|
2342
2349
|
model,
|
|
@@ -2397,7 +2404,7 @@ ${gate.summary}
|
|
|
2397
2404
|
const limit = decision === "THROTTLE" ? 2 : args.tasks.length;
|
|
2398
2405
|
const rules = readRules();
|
|
2399
2406
|
const priorNotes = readImplNotes(5);
|
|
2400
|
-
const maxSteps = args.max_steps
|
|
2407
|
+
const maxSteps = resolveMaxSteps(cfg, args.max_steps);
|
|
2401
2408
|
const gate = checkScanGate(ctx.sessionID);
|
|
2402
2409
|
const gatePrefix = gate.warning ? `${gate.warning}
|
|
2403
2410
|
|
|
@@ -2521,6 +2528,89 @@ The next generate call will automatically include the new rule.`;
|
|
|
2521
2528
|
return out + "\n" + next;
|
|
2522
2529
|
}
|
|
2523
2530
|
}),
|
|
2531
|
+
grade_batch: tool({
|
|
2532
|
+
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).",
|
|
2533
|
+
args: {
|
|
2534
|
+
tasks: tool.schema.array(tool.schema.object({
|
|
2535
|
+
id: tool.schema.number().describe("Task ID (must match the task_update ID)."),
|
|
2536
|
+
prompt: tool.schema.string().describe("The grading prompt for this task (same format as grade()).")
|
|
2537
|
+
})).describe("One entry per task to grade. All run in parallel on GO.")
|
|
2538
|
+
},
|
|
2539
|
+
async execute(args, ctx) {
|
|
2540
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
2541
|
+
const model = cfg.grader ?? cfg.generator;
|
|
2542
|
+
if (!model) {
|
|
2543
|
+
return 'ERROR: no grader/generator model configured. Set "grader" or "generator" in harness.config.json.';
|
|
2544
|
+
}
|
|
2545
|
+
let decision = "GO";
|
|
2546
|
+
try {
|
|
2547
|
+
decision = current().decision;
|
|
2548
|
+
} catch {
|
|
2549
|
+
}
|
|
2550
|
+
if (decision === "STOP") {
|
|
2551
|
+
return 'ERROR: quota STOP \u2014 halt the harness loop now. Call task_update(current, "halted_quota") and stop.';
|
|
2552
|
+
}
|
|
2553
|
+
const limit = decision === "THROTTLE" ? 2 : args.tasks.length;
|
|
2554
|
+
const gradeOne = async (t) => {
|
|
2555
|
+
const gradeTaskId = findActiveTaskId(ctx.sessionID, "grading");
|
|
2556
|
+
const out = await runModel(
|
|
2557
|
+
input.client,
|
|
2558
|
+
model,
|
|
2559
|
+
t.prompt,
|
|
2560
|
+
ctx.directory,
|
|
2561
|
+
gradeTaskId ? { sessionID: ctx.sessionID, taskId: gradeTaskId } : void 0
|
|
2562
|
+
);
|
|
2563
|
+
let verdict = "FAIL";
|
|
2564
|
+
if (!out.startsWith("ERROR:")) {
|
|
2565
|
+
const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
2566
|
+
if (/^pass\b/i.test(f)) verdict = "PASS";
|
|
2567
|
+
else if (/^fail\b/i.test(f)) verdict = "FAIL";
|
|
2568
|
+
}
|
|
2569
|
+
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").`;
|
|
2570
|
+
return { id: t.id, result: `[task ${t.id}] ${verdict}
|
|
2571
|
+
${out}
|
|
2572
|
+
${next}` };
|
|
2573
|
+
};
|
|
2574
|
+
const results = [];
|
|
2575
|
+
const failed = [];
|
|
2576
|
+
for (let i = 0; i < args.tasks.length; i += limit) {
|
|
2577
|
+
const batch = args.tasks.slice(i, i + limit);
|
|
2578
|
+
const settled = await Promise.allSettled(batch.map((t) => gradeOne(t)));
|
|
2579
|
+
for (let j = 0; j < settled.length; j++) {
|
|
2580
|
+
const s = settled[j];
|
|
2581
|
+
const t = batch[j];
|
|
2582
|
+
if (s.status === "fulfilled") {
|
|
2583
|
+
results.push(s.value.result);
|
|
2584
|
+
} else {
|
|
2585
|
+
const err = String(s.reason ?? "unknown rejection");
|
|
2586
|
+
log(`grade_batch task ${t.id} REJECTED: ${err}`);
|
|
2587
|
+
failed.push({ id: t.id, prompt: t.prompt, error: err });
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
if (failed.length > 0) {
|
|
2592
|
+
log(`grade_batch: retrying ${failed.length} failed grade(s) sequentially`);
|
|
2593
|
+
for (const f of failed) {
|
|
2594
|
+
try {
|
|
2595
|
+
const r = await gradeOne(f);
|
|
2596
|
+
results.push(r.result.replace(`[task ${r.id}]`, `[task ${r.id}] (retry)`));
|
|
2597
|
+
} catch (e2) {
|
|
2598
|
+
const err2 = String(e2 ?? "unknown rejection on retry");
|
|
2599
|
+
log(`grade_batch task ${f.id} retry ALSO FAILED: ${err2}`);
|
|
2600
|
+
results.push(`[task ${f.id}] FAIL
|
|
2601
|
+
(ERROR: grade failed on both batch and retry: ${err2})
|
|
2602
|
+
[usage-coach NEXT] task ${f.id}: grade error \u2014 re-run grade() individually for this task.`);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
const throttleNote = decision === "THROTTLE" ? `
|
|
2607
|
+
[usage-coach] quota THROTTLE \u2014 grader concurrency capped at ${limit}.` : "";
|
|
2608
|
+
const passCount = results.filter((r) => /\[task \d+\] PASS/.test(r)).length;
|
|
2609
|
+
const summary = `
|
|
2610
|
+
[usage-coach] Graded ${results.length} task(s): ${passCount} PASS, ${results.length - passCount} FAIL.`;
|
|
2611
|
+
return results.join("\n\n") + summary + throttleNote;
|
|
2612
|
+
}
|
|
2613
|
+
}),
|
|
2524
2614
|
reverse_interview: tool({
|
|
2525
2615
|
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
2616
|
args: {
|
|
@@ -2656,21 +2746,23 @@ If the task is already well-specified with no significant ambiguities, return {"
|
|
|
2656
2746
|
}
|
|
2657
2747
|
}),
|
|
2658
2748
|
coach_config: tool({
|
|
2659
|
-
description: 'View or update harness model configuration (generator, grader, lighterModel, provider). Call with no args to view current config. Pass any combination of
|
|
2749
|
+
description: 'View or update harness model configuration (generator, grader, lighterModel, provider, maxSteps). Call with no args to view current config. Pass any combination of fields to update. Example: coach_config({ generator: "anthropic/claude-sonnet-4-20250514", grader: "opencode/mimo-v2.5-free", maxSteps: 15 })',
|
|
2660
2750
|
args: {
|
|
2661
2751
|
generator: tool.schema.string().optional(),
|
|
2662
2752
|
grader: tool.schema.string().optional(),
|
|
2663
2753
|
lighterModel: tool.schema.string().optional(),
|
|
2664
|
-
provider: tool.schema.string().optional()
|
|
2754
|
+
provider: tool.schema.string().optional(),
|
|
2755
|
+
maxSteps: tool.schema.number().optional().describe("Max steps per generate/grade sub-session (default 30). Lower = faster but may timeout on complex tasks.")
|
|
2665
2756
|
},
|
|
2666
2757
|
async execute(args, ctx) {
|
|
2667
2758
|
const dir = ctx?.directory ?? input.directory;
|
|
2668
2759
|
const current2 = readHarnessCfg(dir);
|
|
2669
|
-
const hasUpdates = args.generator !== void 0 || args.grader !== void 0 || args.lighterModel !== void 0 || args.provider !== void 0;
|
|
2760
|
+
const hasUpdates = args.generator !== void 0 || args.grader !== void 0 || args.lighterModel !== void 0 || args.provider !== void 0 || args.maxSteps !== void 0;
|
|
2670
2761
|
if (!hasUpdates) {
|
|
2671
2762
|
const envOverrides = [];
|
|
2672
2763
|
if (process.env.UC_PROVIDER) envOverrides.push(`UC_PROVIDER=${process.env.UC_PROVIDER}`);
|
|
2673
2764
|
if (process.env.UC_LIGHTER_MODEL) envOverrides.push(`UC_LIGHTER_MODEL=${process.env.UC_LIGHTER_MODEL}`);
|
|
2765
|
+
if (process.env.UC_MAX_STEPS) envOverrides.push(`UC_MAX_STEPS=${process.env.UC_MAX_STEPS}`);
|
|
2674
2766
|
return [
|
|
2675
2767
|
`Harness Configuration`,
|
|
2676
2768
|
`\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
|
|
@@ -2678,6 +2770,7 @@ If the task is already well-specified with no significant ambiguities, return {"
|
|
|
2678
2770
|
` grader: ${current2.grader ?? "(defaults to generator)"}`,
|
|
2679
2771
|
` lighterModel: ${current2.lighterModel ?? "(not set \u2014 no THROTTLE fallback)"}`,
|
|
2680
2772
|
` provider: ${current2.provider ?? "(auto-detected from model)"}`,
|
|
2773
|
+
` maxSteps: ${current2.maxSteps ?? 30} (env: UC_MAX_STEPS)`,
|
|
2681
2774
|
`\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
|
|
2682
2775
|
envOverrides.length > 0 ? `
|
|
2683
2776
|
Environment overrides (take precedence):
|
|
@@ -2685,7 +2778,7 @@ Environment overrides (take precedence):
|
|
|
2685
2778
|
`
|
|
2686
2779
|
Config file: ~/.config/opencode-usage-coach/harness.config.json`,
|
|
2687
2780
|
`
|
|
2688
|
-
To update, call: coach_config({ generator: "provider/model-id", ... })`
|
|
2781
|
+
To update, call: coach_config({ generator: "provider/model-id", maxSteps: 15, ... })`
|
|
2689
2782
|
].filter(Boolean).join("\n");
|
|
2690
2783
|
}
|
|
2691
2784
|
const updates = {};
|
|
@@ -2693,6 +2786,7 @@ To update, call: coach_config({ generator: "provider/model-id", ... })`
|
|
|
2693
2786
|
if (args.grader !== void 0) updates.grader = args.grader.trim();
|
|
2694
2787
|
if (args.lighterModel !== void 0) updates.lighterModel = args.lighterModel.trim();
|
|
2695
2788
|
if (args.provider !== void 0) updates.provider = args.provider.trim();
|
|
2789
|
+
if (args.maxSteps !== void 0) updates.maxSteps = args.maxSteps;
|
|
2696
2790
|
const writtenPath = writeHarnessCfg(updates);
|
|
2697
2791
|
const updated = readHarnessCfg(dir);
|
|
2698
2792
|
return [
|
|
@@ -2702,6 +2796,7 @@ To update, call: coach_config({ generator: "provider/model-id", ... })`
|
|
|
2702
2796
|
` grader: ${updated.grader ?? "(defaults to generator)"}`,
|
|
2703
2797
|
` lighterModel: ${updated.lighterModel ?? "(not set)"}`,
|
|
2704
2798
|
` provider: ${updated.provider ?? "(auto-detected)"}`,
|
|
2799
|
+
` maxSteps: ${updated.maxSteps ?? 30}`,
|
|
2705
2800
|
`\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
|
|
2706
2801
|
`
|
|
2707
2802
|
Saved to: ${writtenPath}`,
|
package/package.json
CHANGED