opencode-usage-coach 0.3.0 → 0.3.2
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/README.md +33 -0
- package/agents/usage-coach-harness.md +7 -1
- package/dist/index.js +69 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,39 @@ cp dist/index.js ~/.config/opencode/plugins/opencode-usage-coach.js
|
|
|
52
52
|
# ~/.config/opencode/tui.json: { "plugin": ["/abs/path/dist/tui.js"] }
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Prompting the harness
|
|
56
|
+
|
|
57
|
+
How to trigger and use the harness loop for substantive work.
|
|
58
|
+
|
|
59
|
+
### Triggering
|
|
60
|
+
With the harness agent mode active (`agents/usage-coach-harness.md` installed), ask for substantive work — explicitly or just describe a multi-step task:
|
|
61
|
+
- `"run this through the harness: write CONTRIBUTING.md from git log"`
|
|
62
|
+
- `"harness: add TypeScript strict mode across the repo"`
|
|
63
|
+
- Or just describe the work; the agent triages and enters the loop.
|
|
64
|
+
|
|
65
|
+
Trivial requests (a one-line fix, a direct answer) are done directly — the loop only runs when generate→grade adds value.
|
|
66
|
+
|
|
67
|
+
### Independent vs dependent tasks
|
|
68
|
+
The harness picks the loop path based on task dependency:
|
|
69
|
+
- **Independent** (task B doesn't need A's output) → `generate_batch` runs all tasks in **parallel** (faster). Example: `"CONTRIBUTING.md, PR template, issue template"` — three separate docs.
|
|
70
|
+
- **Dependent** (B needs A) → **sequential** `generate` calls. Example: `"1) schema, 2) migration, 3) API"` — each needs the prior.
|
|
71
|
+
|
|
72
|
+
### Deterministic NEXT directives
|
|
73
|
+
Each tool appends a `[usage-coach NEXT]` line to its return value, telling the agent exactly what to call next:
|
|
74
|
+
- `generate` → NEXT: grade the work
|
|
75
|
+
- `grade` PASS → NEXT: mark completed, proceed
|
|
76
|
+
- `grade` FAIL → NEXT: revise (up to 2x) or mark failed
|
|
77
|
+
|
|
78
|
+
The agent follows NEXT — you just describe the work, the loop runs itself.
|
|
79
|
+
|
|
80
|
+
### Quota-aware (automatic)
|
|
81
|
+
You don't manage quota — the tools adapt:
|
|
82
|
+
- **GO** + headroom → strong generator, full parallel
|
|
83
|
+
- **THROTTLE** → auto-switch to `lighterModel`, concurrency capped at 2
|
|
84
|
+
- **STOP** → loop halts
|
|
85
|
+
|
|
86
|
+
Set `lighterModel` in `harness.config.json` to enable THROTTLE switching.
|
|
87
|
+
|
|
55
88
|
## Config
|
|
56
89
|
|
|
57
90
|
This plugin has **four config surfaces**. Only the first two are required to run.
|
|
@@ -36,7 +36,12 @@ The user's message is the task source. If it has multiple distinct parts, decomp
|
|
|
36
36
|
|
|
37
37
|
**Multi-model, 1 terminal:** the work runs via the plugin tools `generate` and `grade`, which use the configured models (generator/grader from harness.config.json) on the same server — so you get e.g. a paid generator + a free grader without a second terminal. Do NOT use the `task` tool for harness work; use `generate`/`grade`.
|
|
38
38
|
|
|
39
|
-
**
|
|
39
|
+
**Quota-aware loop strategy (the tools handle this):** the quota coaching injected into your system prompt drives loop strategy. You do NOT pick the model — `generate`/`generate_batch` auto-switch to a lighter model on THROTTLE (if `lighterModel` is configured) and refuse on STOP. You pick the **task size and parallelism**:
|
|
40
|
+
- **GO + headroom** → big tasks OK, use `generate_batch` for independent tasks (full parallel).
|
|
41
|
+
- **THROTTLE** → small tasks only, sequential or limited parallelism. Tools cap concurrency at 2 and use a lighter model automatically.
|
|
42
|
+
- **STOP** → finish the in-progress task, then halt. `generate_batch` returns an error on STOP — call `task_update(current, "halted_quota")` and stop.
|
|
43
|
+
|
|
44
|
+
DEPENDENT tasks (B needs A) → always sequential `generate` calls, regardless of quota.
|
|
40
45
|
|
|
41
46
|
1. Call `harness_start(name, N)` to register the run on the panel.
|
|
42
47
|
2. For each task i (1..N):
|
|
@@ -53,6 +58,7 @@ The user's message is the task source. If it has multiple distinct parts, decomp
|
|
|
53
58
|
3. When all tasks are done → `harness_done()`.
|
|
54
59
|
|
|
55
60
|
## Rules
|
|
61
|
+
- **Follow the [usage-coach NEXT] directive each tool returns.** `harness_start`, `generate`, and `grade` all append a `NEXT` line telling you exactly what to call next. This makes the loop deterministic — do not improvise the sequence, follow `NEXT`.
|
|
56
62
|
- In the loop, do NOT do the work yourself — call `generate`/`grade` (they run the configured models). You orchestrate. (Outside the loop, for trivial requests, act directly.)
|
|
57
63
|
- Call `task_update` on every state transition — the sidebar panel reads it for live visibility.
|
|
58
64
|
- Grading criteria come from the user's request, or sensible defaults; ask the user only if it is truly ambiguous and grading matters.
|
package/dist/index.js
CHANGED
|
@@ -340,7 +340,27 @@ async function UsageCoachPlugin(input) {
|
|
|
340
340
|
args: { name: tool.schema.string(), total: tool.schema.number() },
|
|
341
341
|
async execute(args, ctx) {
|
|
342
342
|
writeHarness(ctx.sessionID, { name: args.name, total: args.total, current: 0, tasks: [], usage: {}, active: true, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
343
|
-
return `Harness '${args.name}' started (${args.total} tasks).
|
|
343
|
+
return `Harness '${args.name}' started (${args.total} tasks).
|
|
344
|
+
|
|
345
|
+
DETERMINISTIC LOOP \u2014 first classify the tasks:
|
|
346
|
+
INDEPENDENT = task B does NOT need task A's output -> use PATH A (parallel, faster)
|
|
347
|
+
DEPENDENT = task B needs task A's output -> use PATH B (sequential)
|
|
348
|
+
|
|
349
|
+
PATH A \u2014 INDEPENDENT (parallel via generate_batch):
|
|
350
|
+
1. task_update(1..${args.total}, title, "generating")
|
|
351
|
+
2. generate_batch({tasks: [{id:1, prompt:"Task: <title1>. Perform it."}, ...]}) -> all results + NEXT
|
|
352
|
+
3. for each i: task_update(i, title, "grading") + grade({prompt:"Evaluate... PASS/FAIL first line. Task: <title>"}) -> verdict + NEXT
|
|
353
|
+
4. for each i: PASS -> task_update(i, title, "completed", "PASS"); FAIL -> revise (up to 2x) or task_update(i, title, "failed", "FAIL")
|
|
354
|
+
|
|
355
|
+
PATH B \u2014 DEPENDENT (sequential):
|
|
356
|
+
for i in 1..${args.total}:
|
|
357
|
+
1. task_update(i, title, "generating")
|
|
358
|
+
2. generate({prompt:"Task: <title>. Perform it."}) -> work + NEXT
|
|
359
|
+
3. task_update(i, title, "grading")
|
|
360
|
+
4. grade(...) -> verdict + NEXT
|
|
361
|
+
5. PASS -> task_update(i, title, "completed", "PASS"); FAIL -> revise (up to 2x) or failed
|
|
362
|
+
|
|
363
|
+
Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns. Do NOT improvise the sequence.`;
|
|
344
364
|
}
|
|
345
365
|
}),
|
|
346
366
|
task_update: tool({
|
|
@@ -375,50 +395,75 @@ async function UsageCoachPlugin(input) {
|
|
|
375
395
|
return "Harness complete.";
|
|
376
396
|
}
|
|
377
397
|
}),
|
|
378
|
-
// Per-role model execution (config-driven, same server, no deadlock).
|
|
398
|
+
// Per-role model execution (config-driven, quota-aware, same server, no deadlock).
|
|
399
|
+
// P1: quota decision drives model selection + concurrency.
|
|
379
400
|
generate: tool({
|
|
380
|
-
description: "Run the GENERATOR model on a prompt.
|
|
401
|
+
description: "Run the GENERATOR model on a prompt. Quota-aware: on THROTTLE, auto-switches to lighterModel if configured. Returns the model's text response.",
|
|
381
402
|
args: { prompt: tool.schema.string() },
|
|
382
403
|
async execute(args, ctx) {
|
|
383
404
|
const cfg = readHarnessCfg(ctx.directory);
|
|
384
405
|
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
385
|
-
|
|
386
|
-
|
|
406
|
+
let decision = "GO";
|
|
407
|
+
try {
|
|
408
|
+
decision = current().decision;
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
412
|
+
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
413
|
+
const out = await runModel(input.client, model, args.prompt, ctx.directory);
|
|
414
|
+
return out + (throttle ? `
|
|
415
|
+
[usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "") + `
|
|
416
|
+
[usage-coach NEXT] call task_update(i, title, "grading"), then grade to evaluate this work.`;
|
|
387
417
|
}
|
|
388
418
|
}),
|
|
389
419
|
generate_batch: tool({
|
|
390
|
-
description: "Run the GENERATOR model on MULTIPLE tasks
|
|
420
|
+
description: "Run the GENERATOR model on MULTIPLE tasks. Quota-aware: GO = full parallel; THROTTLE = lighter model + concurrency capped at 2; STOP = refused. Use for INDEPENDENT tasks.",
|
|
391
421
|
args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })) },
|
|
392
422
|
async execute(args, ctx) {
|
|
393
423
|
const cfg = readHarnessCfg(ctx.directory);
|
|
394
424
|
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
|
|
425
|
+
let decision = "GO";
|
|
426
|
+
try {
|
|
427
|
+
decision = current().decision;
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
if (decision === "STOP") return 'ERROR: quota STOP \u2014 halt the harness loop now. Call task_update(current, "halted_quota") and stop.';
|
|
431
|
+
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
432
|
+
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
433
|
+
const limit = decision === "THROTTLE" ? 2 : args.tasks.length;
|
|
434
|
+
const results = [];
|
|
435
|
+
for (let i = 0; i < args.tasks.length; i += limit) {
|
|
436
|
+
const batch = args.tasks.slice(i, i + limit);
|
|
437
|
+
const out = await Promise.all(batch.map(async (t) => {
|
|
438
|
+
const r = await runModel(input.client, model, t.prompt, ctx.directory);
|
|
439
|
+
return `[task ${t.id}] ${r}`;
|
|
440
|
+
}));
|
|
441
|
+
results.push(...out);
|
|
442
|
+
}
|
|
443
|
+
const note = throttle ? `
|
|
444
|
+
[usage-coach] quota THROTTLE \u2014 lighter model ${cfg.lighterModel}, concurrency capped at ${limit}` : "";
|
|
445
|
+
return results.join("\n\n") + note;
|
|
400
446
|
}
|
|
401
447
|
}),
|
|
402
448
|
grade: tool({
|
|
403
|
-
description: "Run the GRADER model on a prompt. Returns PASS/FAIL on the first line. Falls back to generator if grader quota is out.",
|
|
449
|
+
description: "Run the GRADER model on a prompt. Returns PASS/FAIL on the first line + a [usage-coach NEXT] directive. Falls back to generator if grader quota is out.",
|
|
404
450
|
args: { prompt: tool.schema.string() },
|
|
405
451
|
async execute(args, ctx) {
|
|
406
452
|
const cfg = readHarnessCfg(ctx.directory);
|
|
407
453
|
const model = cfg.grader ?? cfg.generator;
|
|
408
|
-
if (!model) return
|
|
454
|
+
if (!model) return "FAIL\n(ERROR: no grader/generator model configured.)\n[usage-coach NEXT] configure grader in harness.config.json, then retry grade.";
|
|
409
455
|
const out = await runModel(input.client, model, args.prompt, ctx.directory);
|
|
410
|
-
|
|
411
|
-
(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (!isPass && !isFail) {
|
|
417
|
-
return `FAIL
|
|
418
|
-
(grader did not output PASS/FAIL on the first line; defaulting to FAIL to trigger a revise)
|
|
419
|
-
${out}`;
|
|
456
|
+
let verdict = "FAIL";
|
|
457
|
+
if (!out.startsWith("ERROR:")) {
|
|
458
|
+
const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
459
|
+
if (/^pass\b/i.test(f)) verdict = "PASS";
|
|
460
|
+
else if (/^fail\b/i.test(f)) verdict = "FAIL";
|
|
461
|
+
else verdict = "FAIL";
|
|
420
462
|
}
|
|
421
|
-
|
|
463
|
+
const next = verdict === "PASS" ? `
|
|
464
|
+
[usage-coach NEXT] PASS -> call task_update(i, title, "completed", "PASS"), then proceed to next task (or harness_done if last).` : `
|
|
465
|
+
[usage-coach NEXT] FAIL -> if revisions < 2: task_update(i, title, "revising", revisions+1) + generate({prompt: "Apply feedback:\\n{grade result}\\nTask: {title}"}); else: task_update(i, title, "failed", "FAIL") -> next task.`;
|
|
466
|
+
return out + "\n" + next;
|
|
422
467
|
}
|
|
423
468
|
})
|
|
424
469
|
}
|
package/package.json
CHANGED