opencode-usage-coach 0.2.5 → 0.3.1
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/agents/usage-coach-harness.md +6 -1
- package/dist/index.js +67 -38
- package/dist/tui.js +1 -0
- package/package.json +1 -1
|
@@ -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):
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ function setStateDir(dir) {
|
|
|
25
25
|
}
|
|
26
26
|
var NOOP_HOOKS = {};
|
|
27
27
|
function log(msg) {
|
|
28
|
-
|
|
28
|
+
try {
|
|
29
29
|
appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
30
30
|
`);
|
|
31
31
|
} catch {
|
|
@@ -80,50 +80,53 @@ async function runModel(client, model, prompt, directory) {
|
|
|
80
80
|
const providerID = slash >= 0 ? model.slice(0, slash) : model;
|
|
81
81
|
const modelID = slash >= 0 ? model.slice(slash + 1) : "";
|
|
82
82
|
const s = await client.session.create({ body: { title: "uc-harness-sub" }, query: { directory } });
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
83
|
+
log(`runModel(${model}): create resp = ${JSON.stringify(s?.data ?? s).slice(0, 500)}`);
|
|
84
|
+
const id = s?.data?.info?.id ?? s?.data?.id ?? s?.id;
|
|
85
|
+
if (!id) return `ERROR: session.create returned no id (response: ${JSON.stringify(s?.data ?? s).slice(0, 200)})`;
|
|
88
86
|
log(`runModel(${model}): session ${id} created, prompt ${prompt.length} chars`);
|
|
89
87
|
await client.session.prompt({
|
|
90
88
|
path: { id },
|
|
91
89
|
body: { model: { providerID, modelID }, parts: [{ type: "text", text: prompt }] }
|
|
92
90
|
});
|
|
93
|
-
|
|
91
|
+
log(`runModel(${model}): prompt sent to ${id}`);
|
|
92
|
+
let lastMsgCount = -1;
|
|
94
93
|
let timedOut = false;
|
|
94
|
+
let text = "";
|
|
95
95
|
for (let i = 0; i < 600; i++) {
|
|
96
96
|
await sleep(1e3);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
try {
|
|
98
|
+
const msgs = await client.session.messages({ path: { id } });
|
|
99
|
+
const all = msgs?.data ?? msgs ?? [];
|
|
100
|
+
if (all.length !== lastMsgCount) {
|
|
101
|
+
log(`runModel(${model}): poll[${i}] msgs=${all.length}`);
|
|
102
|
+
lastMsgCount = all.length;
|
|
103
|
+
}
|
|
104
|
+
const lastAssistant = all.filter((m) => (m?.info?.role ?? m?.role) === "assistant").pop();
|
|
105
|
+
const parts = lastAssistant?.parts ?? lastAssistant?.info?.parts ?? [];
|
|
106
|
+
const t = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("").trim();
|
|
107
|
+
if (t) {
|
|
108
|
+
text = t;
|
|
109
|
+
log(`runModel(${model}): got assistant text at poll[${i}] (${t.length} chars)`);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
log(`runModel(${model}): poll[${i}] messages err: ${String(e).slice(0, 120)}`);
|
|
107
114
|
}
|
|
108
115
|
if (i === 599) timedOut = true;
|
|
109
116
|
}
|
|
110
117
|
const elapsed = Math.round((Date.now() - t0) / 1e3);
|
|
111
|
-
const msgs = await client.session.messages({ path: { id } });
|
|
112
|
-
const all = msgs?.data ?? [];
|
|
113
|
-
const lastAssistant = all.filter((m) => m?.info?.role === "assistant").pop();
|
|
114
|
-
const parts = lastAssistant?.parts ?? [];
|
|
115
|
-
const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
|
|
116
118
|
try {
|
|
117
119
|
await client.session.remove?.({ path: { id } });
|
|
118
120
|
} catch {
|
|
119
121
|
}
|
|
120
122
|
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars${timedOut ? " [TIMEOUT]" : ""}`);
|
|
121
|
-
if (timedOut) return `[runModel TIMEOUT after ${elapsed}s \u2014
|
|
122
|
-
${text
|
|
123
|
-
return text
|
|
123
|
+
if (timedOut) return `[runModel TIMEOUT after ${elapsed}s \u2014 no assistant text appeared]
|
|
124
|
+
${text}`;
|
|
125
|
+
return text || `ERROR: no assistant text after ${elapsed}s (${lastMsgCount} messages seen)`;
|
|
124
126
|
} catch (e) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
const elapsed = Math.round((Date.now() - t0) / 1e3);
|
|
128
|
+
log(`runModel err (${model}, ${elapsed}s): ${String(e)}`);
|
|
129
|
+
return `ERROR: runModel exception after ${elapsed}s: ${String(e)}`;
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
var num = (e, d) => {
|
|
@@ -372,28 +375,53 @@ async function UsageCoachPlugin(input) {
|
|
|
372
375
|
return "Harness complete.";
|
|
373
376
|
}
|
|
374
377
|
}),
|
|
375
|
-
// Per-role model execution (config-driven, same server, no deadlock).
|
|
378
|
+
// Per-role model execution (config-driven, quota-aware, same server, no deadlock).
|
|
379
|
+
// P1: quota decision drives model selection + concurrency.
|
|
376
380
|
generate: tool({
|
|
377
|
-
description: "Run the GENERATOR model on a prompt.
|
|
381
|
+
description: "Run the GENERATOR model on a prompt. Quota-aware: on THROTTLE, auto-switches to lighterModel if configured. Returns the model's text response.",
|
|
378
382
|
args: { prompt: tool.schema.string() },
|
|
379
383
|
async execute(args, ctx) {
|
|
380
384
|
const cfg = readHarnessCfg(ctx.directory);
|
|
381
385
|
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
382
|
-
|
|
383
|
-
|
|
386
|
+
let decision = "GO";
|
|
387
|
+
try {
|
|
388
|
+
decision = current().decision;
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
392
|
+
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
393
|
+
const out = await runModel(input.client, model, args.prompt, ctx.directory);
|
|
394
|
+
return out + (throttle ? `
|
|
395
|
+
[usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "");
|
|
384
396
|
}
|
|
385
397
|
}),
|
|
386
398
|
generate_batch: tool({
|
|
387
|
-
description: "Run the GENERATOR model on MULTIPLE tasks
|
|
399
|
+
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.",
|
|
388
400
|
args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })) },
|
|
389
401
|
async execute(args, ctx) {
|
|
390
402
|
const cfg = readHarnessCfg(ctx.directory);
|
|
391
403
|
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
404
|
+
let decision = "GO";
|
|
405
|
+
try {
|
|
406
|
+
decision = current().decision;
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
if (decision === "STOP") return 'ERROR: quota STOP \u2014 halt the harness loop now. Call task_update(current, "halted_quota") and stop.';
|
|
410
|
+
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
411
|
+
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
412
|
+
const limit = decision === "THROTTLE" ? 2 : args.tasks.length;
|
|
413
|
+
const results = [];
|
|
414
|
+
for (let i = 0; i < args.tasks.length; i += limit) {
|
|
415
|
+
const batch = args.tasks.slice(i, i + limit);
|
|
416
|
+
const out = await Promise.all(batch.map(async (t) => {
|
|
417
|
+
const r = await runModel(input.client, model, t.prompt, ctx.directory);
|
|
418
|
+
return `[task ${t.id}] ${r}`;
|
|
419
|
+
}));
|
|
420
|
+
results.push(...out);
|
|
421
|
+
}
|
|
422
|
+
const note = throttle ? `
|
|
423
|
+
[usage-coach] quota THROTTLE \u2014 lighter model ${cfg.lighterModel}, concurrency capped at ${limit}` : "";
|
|
424
|
+
return results.join("\n\n") + note;
|
|
397
425
|
}
|
|
398
426
|
}),
|
|
399
427
|
grade: tool({
|
|
@@ -404,7 +432,8 @@ async function UsageCoachPlugin(input) {
|
|
|
404
432
|
const model = cfg.grader ?? cfg.generator;
|
|
405
433
|
if (!model) return 'FAIL\n(ERROR: no grader/generator model configured. Set "grader" and "generator" in harness.config.json.)';
|
|
406
434
|
const out = await runModel(input.client, model, args.prompt, ctx.directory);
|
|
407
|
-
if (
|
|
435
|
+
if (out.startsWith("ERROR:")) return `FAIL
|
|
436
|
+
(${out})`;
|
|
408
437
|
const firstLine = out.split("\n").find((l) => l.trim()) ?? "";
|
|
409
438
|
const f = firstLine.trim();
|
|
410
439
|
const isPass = /^pass\b/i.test(f);
|
package/dist/tui.js
CHANGED
|
@@ -173,6 +173,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
173
173
|
const hf = join(STATE_DIR, sid, "harness.json");
|
|
174
174
|
if (existsSync(hf)) h = JSON.parse(readFileSync(hf, "utf8"));
|
|
175
175
|
}
|
|
176
|
+
if (!h || h.active === false) h = readHarness();
|
|
176
177
|
} catch {
|
|
177
178
|
h = null;
|
|
178
179
|
}
|
package/package.json
CHANGED