tryharness 0.7.23 → 0.7.26

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.
Files changed (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5268 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +384 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/models.mjs CHANGED
@@ -19,104 +19,230 @@ const CLAUDE_NAMES = ["fable", "opus", "sonnet", "haiku"];
19
19
  * model that can do this work well; more effort only for hard work.", sent demanding work to cheap models (eval
20
20
  * model-routing v1, 20261014-010225-80250750-357a).
21
21
  */
22
- export const DEFAULT_ROUTING = "Match the model to the work: for easy work, the cheapest model that does it well; for demanding work, the most capable one. Pass over a model that keeps failing here. More effort only for hard work.";
22
+ export const DEFAULT_ROUTING =
23
+ "Match the model to the work: for easy work, the cheapest model that does it well; for demanding work, the most capable one. Pass over a model that keeps failing here. More effort only for hard work.";
23
24
 
24
25
  const tokens = (text) => {
25
26
  const match = /^(\d+(?:\.\d+)?)([KM]?)$/i.exec(String(text || ""));
26
- return match ? Math.round(Number(match[1]) * ({ k: 1e3, m: 1e6 }[match[2].toLowerCase()] ?? 1)) : null;
27
+ return match
28
+ ? Math.round(
29
+ Number(match[1]) * ({ k: 1e3, m: 1e6 }[match[2].toLowerCase()] ?? 1)
30
+ )
31
+ : null;
27
32
  };
28
33
 
29
34
  // One reader per agent's own list command, its output as the agent prints it today.
30
35
  const LISTS = {
31
- codex: { command: "codex", args: ["debug", "models"], parse: (out) => JSON.parse(out).models.map((entry) => ({
32
- id: entry.slug, about: entry.description || null, efforts: (entry.supported_reasoning_levels || []).map((level) => level.effort),
33
- defaultEffort: entry.default_reasoning_level || null, context: entry.context_window || null })) },
36
+ codex: {
37
+ command: "codex",
38
+ args: ["debug", "models"],
39
+ parse: (out) =>
40
+ JSON.parse(out).models.map((entry) => ({
41
+ id: entry.slug,
42
+ about: entry.description || null,
43
+ efforts: (entry.supported_reasoning_levels || []).map(
44
+ (level) => level.effort
45
+ ),
46
+ defaultEffort: entry.default_reasoning_level || null,
47
+ context: entry.context_window || null,
48
+ })),
49
+ },
34
50
  // provider model context max-out thinking images
35
- pi: { command: "pi", args: ["--list-models"], parse: (out) => out.split("\n").map((line) => line.trim().split(/\s+/))
36
- .filter((cells) => cells.length >= 5 && cells[0] !== "provider")
37
- .map(([provider, model, context, , thinking]) => ({ id: `${provider}/${model}`, efforts: thinking === "yes" ? PI_THINKING : [], context: tokens(context) })) },
51
+ pi: {
52
+ command: "pi",
53
+ args: ["--list-models"],
54
+ parse: (out) =>
55
+ out
56
+ .split("\n")
57
+ .map((line) => line.trim().split(/\s+/))
58
+ .filter((cells) => cells.length >= 5 && cells[0] !== "provider")
59
+ .map(([provider, model, context, , thinking]) => ({
60
+ id: `${provider}/${model}`,
61
+ efforts: thinking === "yes" ? PI_THINKING : [],
62
+ context: tokens(context),
63
+ })),
64
+ },
38
65
  // Each model as "provider/id" on its own line, then its details as JSON.
39
- opencode: { command: "opencode", args: ["models", "--verbose"], parse: (out) => {
40
- const models = [];
41
- let id = null, body = [];
42
- const flush = () => {
43
- if (!id) return;
44
- const entry = JSON.parse(body.join("\n"));
45
- models.push({ id, efforts: Object.keys(entry.variants || {}), context: entry.limit?.context ?? null,
46
- ...(entry.cost && Number.isFinite(entry.cost.input) ? { price: { inputUsdPerMillion: entry.cost.input, outputUsdPerMillion: entry.cost.output } } : {}) });
47
- };
48
- for (const line of out.split("\n")) {
49
- if (/^[^\s{}"]+\/\S+$/.test(line)) { flush(); id = line.trim(); body = []; } else if (id) body.push(line);
50
- }
51
- flush();
52
- return models;
53
- } },
66
+ opencode: {
67
+ command: "opencode",
68
+ args: ["models", "--verbose"],
69
+ parse: (out) => {
70
+ const models = [];
71
+ let id = null,
72
+ body = [];
73
+ const flush = () => {
74
+ if (!id) return;
75
+ const entry = JSON.parse(body.join("\n"));
76
+ models.push({
77
+ id,
78
+ efforts: Object.keys(entry.variants || {}),
79
+ context: entry.limit?.context ?? null,
80
+ ...(entry.cost && Number.isFinite(entry.cost.input)
81
+ ? {
82
+ price: {
83
+ inputUsdPerMillion: entry.cost.input,
84
+ outputUsdPerMillion: entry.cost.output,
85
+ },
86
+ }
87
+ : {}),
88
+ });
89
+ };
90
+ for (const line of out.split("\n")) {
91
+ if (/^[^\s{}"]+\/\S+$/.test(line)) {
92
+ flush();
93
+ id = line.trim();
94
+ body = [];
95
+ } else if (id) body.push(line);
96
+ }
97
+ flush();
98
+ return models;
99
+ },
100
+ },
54
101
  };
55
102
 
56
103
  function capture(command, args) {
57
104
  return new Promise((resolve, reject) => {
58
105
  const environment = agentEnvironment(command);
59
- if (environment.error) { reject(new Error(environment.error)); return; }
106
+ if (environment.error) {
107
+ reject(new Error(environment.error));
108
+ return;
109
+ }
60
110
  let child;
61
- try { child = spawn(command, args, { cwd: os.tmpdir(), env: environment.env }); } catch (error) { reject(error); return; }
111
+ try {
112
+ child = spawn(command, args, { cwd: os.tmpdir(), env: environment.env });
113
+ } catch (error) {
114
+ reject(error);
115
+ return;
116
+ }
62
117
  child.stdin.end();
63
118
  trackHelper(child);
64
- let out = "", err = "";
65
- child.stdout.on("data", (chunk) => { out += chunk; });
66
- child.stderr.on("data", (chunk) => { err += chunk; });
119
+ let out = "",
120
+ err = "";
121
+ child.stdout.on("data", (chunk) => {
122
+ out += chunk;
123
+ });
124
+ child.stderr.on("data", (chunk) => {
125
+ err += chunk;
126
+ });
67
127
  child.on("error", reject);
68
- child.on("close", (code) => code === 0 ? resolve(out) : reject(new Error(`${command} ${args.join(" ")} exited with code ${code}: ${err.trim().slice(0, 200)}`)));
128
+ child.on("close", (code) =>
129
+ code === 0
130
+ ? resolve(out)
131
+ : reject(
132
+ new Error(
133
+ `${command} ${args.join(" ")} exited with code ${code}: ${err.trim().slice(0, 200)}`
134
+ )
135
+ )
136
+ );
69
137
  });
70
138
  }
71
139
 
72
140
  const catalogKey = (agent) => `models:${agent}`;
73
141
  /** An agent's last listing: { at, models, error? }, error when its list command last failed. */
74
- export const listingOf = (store, agent) => { try { return JSON.parse(store.meta(catalogKey(agent)) || "null"); } catch { return null; } };
142
+ export const listingOf = (store, agent) => {
143
+ try {
144
+ return JSON.parse(store.meta(catalogKey(agent)) || "null");
145
+ } catch {
146
+ return null;
147
+ }
148
+ };
75
149
  /** An agent's models as it last listed them: [{ id, about?, efforts, defaultEffort?, context?, price? }]. */
76
- export const catalogOf = (store, agent) => listingOf(store, agent)?.models ?? [];
150
+ export const catalogOf = (store, agent) =>
151
+ listingOf(store, agent)?.models ?? [];
77
152
 
78
153
  /**
79
154
  * Asks each installed agent for its models, all at once so one slow agent holds up no other, and keeps the lists; an
80
155
  * agent whose command fails keeps its last list and the error.
81
156
  */
82
157
  export async function refreshCatalogs(store, agents, log = () => {}) {
83
- await Promise.all(agents.filter((name) => LISTS[name]).map(async (agent) => {
84
- try {
85
- const models = LISTS[agent].parse(await capture(LISTS[agent].command, LISTS[agent].args)).filter((entry) => entry.id);
86
- store.meta(catalogKey(agent), JSON.stringify({ at: new Date().toISOString(), models }));
87
- } catch (error) {
88
- log(`model list for ${agent}: ${error.message}`);
89
- store.meta(catalogKey(agent), JSON.stringify({ ...(listingOf(store, agent) ?? { models: [] }), error: error.message.slice(0, 300) }));
90
- }
91
- }));
158
+ await Promise.all(
159
+ agents
160
+ .filter((name) => LISTS[name])
161
+ .map(async (agent) => {
162
+ try {
163
+ const models = LISTS[agent]
164
+ .parse(await capture(LISTS[agent].command, LISTS[agent].args))
165
+ .filter((entry) => entry.id);
166
+ store.meta(
167
+ catalogKey(agent),
168
+ JSON.stringify({ at: new Date().toISOString(), models })
169
+ );
170
+ } catch (error) {
171
+ log(`model list for ${agent}: ${error.message}`);
172
+ store.meta(
173
+ catalogKey(agent),
174
+ JSON.stringify({
175
+ ...(listingOf(store, agent) ?? { models: [] }),
176
+ error: error.message.slice(0, 300),
177
+ })
178
+ );
179
+ }
180
+ })
181
+ );
92
182
  if (agents.includes("claude-code")) {
93
- const anthropic = [...new Set(["pi", "opencode"].flatMap((other) => catalogOf(store, other)).map((entry) => entry.id).filter((id) => id.startsWith("anthropic/")).map((id) => id.slice("anthropic/".length)))];
94
- const models = [...CLAUDE_NAMES.map((name) => ({ id: name, about: `Claude Code's name for its newest ${name[0].toUpperCase()}${name.slice(1)} model.`, efforts: CLAUDE_EFFORTS })),
95
- ...anthropic.sort().map((id) => ({ id, efforts: CLAUDE_EFFORTS }))];
96
- store.meta(catalogKey("claude-code"), JSON.stringify({ at: new Date().toISOString(), models }));
183
+ const anthropic = [
184
+ ...new Set(
185
+ ["pi", "opencode"]
186
+ .flatMap((other) => catalogOf(store, other))
187
+ .map((entry) => entry.id)
188
+ .filter((id) => id.startsWith("anthropic/"))
189
+ .map((id) => id.slice("anthropic/".length))
190
+ ),
191
+ ];
192
+ const models = [
193
+ ...CLAUDE_NAMES.map((name) => ({
194
+ id: name,
195
+ about: `Claude Code's name for its newest ${name[0].toUpperCase()}${name.slice(1)} model.`,
196
+ efforts: CLAUDE_EFFORTS,
197
+ })),
198
+ ...anthropic.sort().map((id) => ({ id, efforts: CLAUDE_EFFORTS })),
199
+ ];
200
+ store.meta(
201
+ catalogKey("claude-code"),
202
+ JSON.stringify({ at: new Date().toISOString(), models })
203
+ );
97
204
  }
98
205
  }
99
206
 
100
207
  /** One model in an agent's list, written as its id or, when only one model has it, the part after the provider. */
101
208
  function findModel(store, agent, written) {
102
209
  const models = catalogOf(store, agent);
103
- if (!models.length) return { error: `the relay has no model list for ${nameOf(agent)} yet` };
104
- const wanted = String(written || "").trim().toLowerCase();
210
+ if (!models.length)
211
+ return { error: `the relay has no model list for ${nameOf(agent)} yet` };
212
+ const wanted = String(written || "")
213
+ .trim()
214
+ .toLowerCase();
105
215
  const exact = models.find((entry) => entry.id.toLowerCase() === wanted);
106
216
  if (exact) return { entry: exact };
107
- const tail = models.filter((entry) => entry.id.toLowerCase().split("/").pop() === wanted);
217
+ const tail = models.filter(
218
+ (entry) => entry.id.toLowerCase().split("/").pop() === wanted
219
+ );
108
220
  if (tail.length === 1) return { entry: tail[0] };
109
- return { error: tail.length ? `"${written}" could be ${tail.map((entry) => entry.id).join(" or ")}; name one` : `"${written}" is not one of ${nameOf(agent)}'s models (npx tryharness agents models ${agent})` };
221
+ return {
222
+ error: tail.length
223
+ ? `"${written}" could be ${tail.map((entry) => entry.id).join(" or ")}; name one`
224
+ : `"${written}" is not one of ${nameOf(agent)}'s models (npx tryharness agents models ${agent})`,
225
+ };
110
226
  }
111
227
 
112
228
  /** A model and effort for an agent, checked against its list: { ok, pick } with pick { model, effort } or { auto: true }. */
113
229
  export function resolvePick(store, agent, model, effort = null) {
114
- if (String(model || "").trim().toLowerCase() === "auto") return { ok: true, pick: { auto: true } };
230
+ if (
231
+ String(model || "")
232
+ .trim()
233
+ .toLowerCase() === "auto"
234
+ )
235
+ return { ok: true, pick: { auto: true } };
115
236
  const found = findModel(store, agent, model);
116
237
  if (found.error) return { ok: false, error: found.error };
117
238
  const level = effort ? String(effort).trim().toLowerCase() : null;
118
239
  if (level && !found.entry.efforts.includes(level)) {
119
- return { ok: false, error: found.entry.efforts.length ? `${found.entry.id} takes effort ${found.entry.efforts.join(", ")}, not "${effort}"` : `${found.entry.id} has no effort setting in ${nameOf(agent)}` };
240
+ return {
241
+ ok: false,
242
+ error: found.entry.efforts.length
243
+ ? `${found.entry.id} takes effort ${found.entry.efforts.join(", ")}, not "${effort}"`
244
+ : `${found.entry.id} has no effort setting in ${nameOf(agent)}`,
245
+ };
120
246
  }
121
247
  return { ok: true, pick: { model: found.entry.id, effort: level } };
122
248
  }
@@ -126,7 +252,15 @@ export function resolveSpec(store, agent, spec) {
126
252
  const whole = resolvePick(store, agent, spec);
127
253
  if (whole.ok) return whole;
128
254
  const cut = spec.lastIndexOf("/");
129
- if (cut > 0) { const split = resolvePick(store, agent, spec.slice(0, cut), spec.slice(cut + 1)); if (split.ok) return split; }
255
+ if (cut > 0) {
256
+ const split = resolvePick(
257
+ store,
258
+ agent,
259
+ spec.slice(0, cut),
260
+ spec.slice(cut + 1)
261
+ );
262
+ if (split.ok) return split;
263
+ }
130
264
  return whole;
131
265
  }
132
266
 
@@ -141,7 +275,8 @@ export function picksIn(store, text, agents) {
141
275
  if (!agents.includes(agent) || !PICKABLE.has(agent)) continue;
142
276
  const spec = match[2].replace(/[.,;:!?)]+$/, "");
143
277
  const resolved = resolveSpec(store, agent, spec);
144
- if (!resolved.ok) return { picks, error: `@${match[1]}:${spec}: ${resolved.error}` };
278
+ if (!resolved.ok)
279
+ return { picks, error: `@${match[1]}:${spec}: ${resolved.error}` };
145
280
  picks.set(agent, resolved.pick);
146
281
  }
147
282
  return { picks };
@@ -149,33 +284,73 @@ export function picksIn(store, text, agents) {
149
284
 
150
285
  const choiceKey = (agent) => `model-choice:${agent}`;
151
286
  /** The person's choice for an agent: { model, effort }, { auto: true }, or null for the agent's own settings. */
152
- export const choiceOf = (store, agent) => { try { return JSON.parse(store.meta(choiceKey(agent)) || "null"); } catch { return null; } };
287
+ export const choiceOf = (store, agent) => {
288
+ try {
289
+ return JSON.parse(store.meta(choiceKey(agent)) || "null");
290
+ } catch {
291
+ return null;
292
+ }
293
+ };
153
294
  export function setChoice(store, agent, model, effort = null) {
154
- if (!model || String(model).toLowerCase() === "default") { store.meta(choiceKey(agent), ""); return { ok: true, choice: null }; }
295
+ if (!model || String(model).toLowerCase() === "default") {
296
+ store.meta(choiceKey(agent), "");
297
+ return { ok: true, choice: null };
298
+ }
155
299
  const resolved = resolvePick(store, agent, model, effort);
156
300
  if (!resolved.ok) return resolved;
157
301
  store.meta(choiceKey(agent), JSON.stringify(resolved.pick));
158
302
  return { ok: true, choice: resolved.pick };
159
303
  }
160
304
  export const routingOf = (store) => store.meta("routing-note") || null;
161
- export const setRouting = (store, text) => { const note = String(text || "").replace(/\s+/g, " ").trim().slice(0, 1_000); store.meta("routing-note", note); return note || null; };
305
+ export const setRouting = (store, text) => {
306
+ const note = String(text || "")
307
+ .replace(/\s+/g, " ")
308
+ .trim()
309
+ .slice(0, 1_000);
310
+ store.meta("routing-note", note);
311
+ return note || null;
312
+ };
162
313
 
163
314
  /** A pick in words: "gpt-6-luna, effort high", "Auto". */
164
- export const describePick = (pick) => !pick ? null : pick.auto ? "Auto" : [pick.model, pick.effort && `effort ${pick.effort}`].filter(Boolean).join(", ");
315
+ export const describePick = (pick) =>
316
+ !pick
317
+ ? null
318
+ : pick.auto
319
+ ? "Auto"
320
+ : [pick.model, pick.effort && `effort ${pick.effort}`]
321
+ .filter(Boolean)
322
+ .join(", ");
165
323
 
166
324
  /** The pick named for one run (a post, post_task), kept with the run until it starts. */
167
- export const setRunPick = (store, runId, pick, by) => store.meta(`model:${runId}`, JSON.stringify({ ...pick, by }));
168
- const runPick = (store, runId) => { try { return JSON.parse(store.meta(`model:${runId}`) || "null"); } catch { return null; } };
325
+ export const setRunPick = (store, runId, pick, by) =>
326
+ store.meta(`model:${runId}`, JSON.stringify({ ...pick, by }));
327
+ const runPick = (store, runId) => {
328
+ try {
329
+ return JSON.parse(store.meta(`model:${runId}`) || "null");
330
+ } catch {
331
+ return null;
332
+ }
333
+ };
169
334
 
170
- const median = (values) => { const sorted = values.filter(Number.isFinite).sort((a, b) => a - b); return sorted.length ? sorted[Math.floor(sorted.length / 2)] : null; };
335
+ const median = (values) => {
336
+ const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
337
+ return sorted.length ? sorted[Math.floor(sorted.length / 2)] : null;
338
+ };
171
339
  /** How each of an agent's models did on this board: finished, failed, and the median minutes and dollars of a finished run. */
172
340
  function recordByModel(store, agent) {
173
341
  const byModel = new Map();
174
342
  for (const row of store.modelWork(agent)) {
175
- const entry = byModel.get(row.model) ?? { done: 0, failed: 0, minutes: [], cost: [] };
343
+ const entry = byModel.get(row.model) ?? {
344
+ done: 0,
345
+ failed: 0,
346
+ minutes: [],
347
+ cost: [],
348
+ };
176
349
  if (row.status === "done") {
177
350
  entry.done++;
178
- entry.minutes.push((Date.parse(row.reported_at) - Date.parse(row.started_at)) / 60_000);
351
+ entry.minutes.push(
352
+ (Date.parse(row.reported_at) - Date.parse(row.started_at)) / 60_000
353
+ );
179
354
  entry.cost.push(Number(row.usage?.cost_usd));
180
355
  } else if (row.status === "failed") entry.failed++;
181
356
  byModel.set(row.model, entry);
@@ -183,15 +358,36 @@ function recordByModel(store, agent) {
183
358
  return (model) => {
184
359
  const entry = byModel.get(model);
185
360
  if (!entry) return null;
186
- const minutes = median(entry.minutes), cost = median(entry.cost);
187
- return { done: entry.done, failed: entry.failed, medianMinutes: minutes === null ? null : Math.round(minutes * 10) / 10, medianCostUsd: cost === null ? null : Math.round(cost * 10_000) / 10_000 };
361
+ const minutes = median(entry.minutes),
362
+ cost = median(entry.cost);
363
+ return {
364
+ done: entry.done,
365
+ failed: entry.failed,
366
+ medianMinutes: minutes === null ? null : Math.round(minutes * 10) / 10,
367
+ medianCostUsd: cost === null ? null : Math.round(cost * 10_000) / 10_000,
368
+ };
188
369
  };
189
370
  }
190
371
 
191
372
  /** What Auto sends Harness for one run (src/server/swarm/route.ts): the work, the person's words, every listed model with its record here. */
192
- export const routeInput = ({ agent, work, transcript, routing, ownModel, models, record }) => ({
193
- agent, name: nameOf(agent), work, transcript, routing: routing ?? DEFAULT_ROUTING, ownModel,
194
- models: models.slice(0, 250).map((entry) => ({ ...entry, record: record(entry.id) })),
373
+ export const routeInput = ({
374
+ agent,
375
+ work,
376
+ transcript,
377
+ routing,
378
+ ownModel,
379
+ models,
380
+ record,
381
+ }) => ({
382
+ agent,
383
+ name: nameOf(agent),
384
+ work,
385
+ transcript,
386
+ routing: routing ?? DEFAULT_ROUTING,
387
+ ownModel,
388
+ models: models
389
+ .slice(0, 250)
390
+ .map((entry) => ({ ...entry, record: record(entry.id) })),
195
391
  });
196
392
 
197
393
  /**
@@ -199,35 +395,101 @@ export const routeInput = ({ agent, work, transcript, routing, ownModel, models,
199
395
  * who chose: "you" (the person's post), another agent's id, "your choice" (the person's setting) or "auto".
200
396
  * Auto asks Harness only when the person allows it (not the privacy setting) and Harness can answer.
201
397
  */
202
- export function createModelRouter({ store, askHarness, canAsk, privacy, ownModel, workOf, log = () => {} }) {
398
+ export function createModelRouter({
399
+ store,
400
+ askHarness,
401
+ canAsk,
402
+ privacy,
403
+ ownModel,
404
+ workOf,
405
+ log = () => {},
406
+ }) {
203
407
  return async function modelForRun(task, agent) {
204
408
  const named = runPick(store, task.id);
205
- const pick = named ?? (choiceOf(store, agent) ? { ...choiceOf(store, agent), by: "your choice" } : null);
409
+ const pick =
410
+ named ??
411
+ (choiceOf(store, agent)
412
+ ? { ...choiceOf(store, agent), by: "your choice" }
413
+ : null);
206
414
  if (!pick) return null;
207
415
  const by = named ? (named.by === "user" ? "you" : named.by) : pick.by;
208
416
  if (!pick.auto) {
209
417
  // The list may have changed since the pick: a model the agent no longer lists runs on its own settings, said so.
210
418
  const checked = resolvePick(store, agent, pick.model, pick.effort);
211
- return checked.ok ? { ...checked.pick, by } : { model: null, effort: null, by, note: `${checked.error}; it ran on its own settings` };
419
+ return checked.ok
420
+ ? { ...checked.pick, by }
421
+ : {
422
+ model: null,
423
+ effort: null,
424
+ by,
425
+ note: `${checked.error}; it ran on its own settings`,
426
+ };
212
427
  }
213
- const fallback = (note) => ({ model: null, effort: null, by: "auto", note });
214
- if (privacy()) return fallback("Auto is off on the privacy setting; it ran on its own settings");
215
- if (!canAsk()) return fallback("Harness could not pick a model just now; it ran on its own settings");
428
+ const fallback = (note) => ({
429
+ model: null,
430
+ effort: null,
431
+ by: "auto",
432
+ note,
433
+ });
434
+ if (privacy())
435
+ return fallback(
436
+ "Auto is off on the privacy setting; it ran on its own settings"
437
+ );
438
+ if (!canAsk())
439
+ return fallback(
440
+ "Harness could not pick a model just now; it ran on its own settings"
441
+ );
216
442
  const models = catalogOf(store, agent);
217
- if (!models.length) return fallback(`the relay has no model list for ${nameOf(agent)} yet; it ran on its own settings`);
443
+ if (!models.length)
444
+ return fallback(
445
+ `the relay has no model list for ${nameOf(agent)} yet; it ran on its own settings`
446
+ );
218
447
  try {
219
- const reply = await askHarness("route_model", routeInput({ agent, ...workOf(task), routing: routingOf(store), ownModel: ownModel(agent), models, record: recordByModel(store, agent) }), null);
220
- if (!reply.ok) throw new Error(reply.error || "Harness did not pick a model");
448
+ const reply = await askHarness(
449
+ "route_model",
450
+ routeInput({
451
+ agent,
452
+ ...workOf(task),
453
+ routing: routingOf(store),
454
+ ownModel: ownModel(agent),
455
+ models,
456
+ record: recordByModel(store, agent),
457
+ }),
458
+ null
459
+ );
460
+ if (!reply.ok)
461
+ throw new Error(reply.error || "Harness did not pick a model");
221
462
  // The bridge hands back Harness's answer with its fields beside ok.
222
463
  const picked = reply;
223
- if (!picked.model) return fallback("Harness picked no model; it ran on its own settings");
224
- const checked = resolvePick(store, agent, picked.model, picked.effort && models.find((entry) => entry.id === picked.model)?.efforts.includes(picked.effort) ? picked.effort : null);
225
- if (!checked.ok) return fallback(`Auto picked ${picked.model}, which ${nameOf(agent)} does not list; it ran on its own settings`);
464
+ if (!picked.model)
465
+ return fallback("Harness picked no model; it ran on its own settings");
466
+ const checked = resolvePick(
467
+ store,
468
+ agent,
469
+ picked.model,
470
+ picked.effort &&
471
+ models
472
+ .find((entry) => entry.id === picked.model)
473
+ ?.efforts.includes(picked.effort)
474
+ ? picked.effort
475
+ : null
476
+ );
477
+ if (!checked.ok)
478
+ return fallback(
479
+ `Auto picked ${picked.model}, which ${nameOf(agent)} does not list; it ran on its own settings`
480
+ );
226
481
  // Jev's confidence in the pick stays with the run, to see later how sure the picks that worked were.
227
- return { ...checked.pick, by: "auto", judgments: picked.judgments ?? null, estimatedMicroUsd: picked.estimatedMicroUsd ?? null };
482
+ return {
483
+ ...checked.pick,
484
+ by: "auto",
485
+ judgments: picked.judgments ?? null,
486
+ estimatedMicroUsd: picked.estimatedMicroUsd ?? null,
487
+ };
228
488
  } catch (error) {
229
489
  log(`auto model for ${task.id}: ${error.message}`);
230
- return fallback(`Harness could not pick a model (${error.message}); it ran on its own settings`);
490
+ return fallback(
491
+ `Harness could not pick a model (${error.message}); it ran on its own settings`
492
+ );
231
493
  }
232
494
  };
233
495
  }