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.
- package/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- 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 =
|
|
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
|
|
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: {
|
|
32
|
-
|
|
33
|
-
|
|
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: {
|
|
36
|
-
|
|
37
|
-
|
|
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: {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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) {
|
|
106
|
+
if (environment.error) {
|
|
107
|
+
reject(new Error(environment.error));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
60
110
|
let child;
|
|
61
|
-
try {
|
|
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 = "",
|
|
65
|
-
|
|
66
|
-
child.
|
|
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) =>
|
|
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) => {
|
|
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) =>
|
|
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(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 = [
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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)
|
|
104
|
-
|
|
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(
|
|
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 {
|
|
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 (
|
|
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 {
|
|
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) {
|
|
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)
|
|
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) => {
|
|
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") {
|
|
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) => {
|
|
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) =>
|
|
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) =>
|
|
168
|
-
|
|
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) => {
|
|
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) ?? {
|
|
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(
|
|
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),
|
|
187
|
-
|
|
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 = ({
|
|
193
|
-
agent,
|
|
194
|
-
|
|
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({
|
|
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 =
|
|
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
|
|
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) => ({
|
|
214
|
-
|
|
215
|
-
|
|
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)
|
|
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(
|
|
220
|
-
|
|
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)
|
|
224
|
-
|
|
225
|
-
|
|
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 {
|
|
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(
|
|
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
|
}
|