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/runner.mjs CHANGED
@@ -19,7 +19,9 @@ export function stopAgentTask(runId) {
19
19
  export function trackRun(runId, stop) {
20
20
  const entry = { pid: null, kill: stop };
21
21
  children.set(runId, entry);
22
- return () => { if (children.get(runId) === entry) children.delete(runId); };
22
+ return () => {
23
+ if (children.get(runId) === entry) children.delete(runId);
24
+ };
23
25
  }
24
26
 
25
27
  /** A short command the daemon runs itself (an agent listing its models): while it runs, not the person's session either. */
@@ -35,11 +37,18 @@ export function runningPids() {
35
37
  }
36
38
 
37
39
  export function resolveWorkingDirectory(requested) {
38
- if (!requested || !path.isAbsolute(requested)) throw new Error("Delegated tasks require an absolute working directory.");
40
+ if (!requested || !path.isAbsolute(requested))
41
+ throw new Error("Delegated tasks require an absolute working directory.");
39
42
  let stat;
40
- try { stat = fs.statSync(requested); }
41
- catch { throw new Error(`Delegated task directory does not exist: ${requested}`); }
42
- if (!stat.isDirectory()) throw new Error(`Delegated task working directory is not a directory: ${requested}`);
43
+ try {
44
+ stat = fs.statSync(requested);
45
+ } catch {
46
+ throw new Error(`Delegated task directory does not exist: ${requested}`);
47
+ }
48
+ if (!stat.isDirectory())
49
+ throw new Error(
50
+ `Delegated task working directory is not a directory: ${requested}`
51
+ );
43
52
  return requested;
44
53
  }
45
54
 
@@ -49,44 +58,141 @@ export function resolveWorkingDirectory(requested) {
49
58
  export const RESTRICTED_CLAUDE_SETTINGS = {
50
59
  disableAllHooks: true,
51
60
  permissions: { deny: ["WebFetch", "WebSearch"] },
52
- sandbox: { enabled: true, autoAllowBashIfSandboxed: true, allowUnsandboxedCommands: false, failIfUnavailable: true, excludedCommands: [], network: { allowedDomains: [], strictAllowlist: true } },
61
+ sandbox: {
62
+ enabled: true,
63
+ autoAllowBashIfSandboxed: true,
64
+ allowUnsandboxedCommands: false,
65
+ failIfUnavailable: true,
66
+ excludedCommands: [],
67
+ network: { allowedDomains: [], strictAllowlist: true },
68
+ },
53
69
  };
54
70
  // Only the Harness board MCP, as Claude Code: none of the person's other MCP servers or claude.ai connectors load.
55
- export const restrictedMcpConfig = () => ({ mcpServers: { harness: { command: process.execPath, args: [fileURLToPath(new URL("./mcp-stdio.mjs", import.meta.url)), "--agent", "claude-code"] } } });
56
- const restrictedClaude = (task) => ["--permission-mode", "dontAsk", "--allowedTools", task.readOnly ? "mcp__harness" : "Edit Write mcp__harness",
57
- "--setting-sources", "user", "--settings", JSON.stringify(RESTRICTED_CLAUDE_SETTINGS), "--strict-mcp-config", "--mcp-config", JSON.stringify(restrictedMcpConfig())];
71
+ export const restrictedMcpConfig = () => ({
72
+ mcpServers: {
73
+ harness: {
74
+ command: process.execPath,
75
+ args: [
76
+ fileURLToPath(new URL("./mcp-stdio.mjs", import.meta.url)),
77
+ "--agent",
78
+ "claude-code",
79
+ ],
80
+ },
81
+ },
82
+ });
83
+ const restrictedClaude = (task) => [
84
+ "--permission-mode",
85
+ "dontAsk",
86
+ "--allowedTools",
87
+ task.readOnly ? "mcp__harness" : "Edit Write mcp__harness",
88
+ "--setting-sources",
89
+ "user",
90
+ "--settings",
91
+ JSON.stringify(RESTRICTED_CLAUDE_SETTINGS),
92
+ "--strict-mcp-config",
93
+ "--mcp-config",
94
+ JSON.stringify(restrictedMcpConfig()),
95
+ ];
58
96
 
59
97
  // The model and effort the relay chose for the run (models.mjs), in each agent's own flags; none leaves its settings as they are.
60
98
  const MODEL_FLAGS = {
61
- "claude-code": { model: (id) => ["--model", id], effort: (level) => ["--effort", level] },
62
- codex: { model: (id) => ["-m", id], effort: (level) => ["-c", `model_reasoning_effort="${level}"`] },
63
- pi: { model: (id) => ["--model", id], effort: (level) => ["--thinking", level] },
64
- opencode: { model: (id) => ["-m", id], effort: (level) => ["--variant", level] },
99
+ "claude-code": {
100
+ model: (id) => ["--model", id],
101
+ effort: (level) => ["--effort", level],
102
+ },
103
+ codex: {
104
+ model: (id) => ["-m", id],
105
+ effort: (level) => ["-c", `model_reasoning_effort="${level}"`],
106
+ },
107
+ pi: {
108
+ model: (id) => ["--model", id],
109
+ effort: (level) => ["--thinking", level],
110
+ },
111
+ opencode: {
112
+ model: (id) => ["-m", id],
113
+ effort: (level) => ["--variant", level],
114
+ },
65
115
  };
66
- const modelFlags = ({ agent, model, effort }) => [...(model ? MODEL_FLAGS[agent]?.model(model) ?? [] : []), ...(effort ? MODEL_FLAGS[agent]?.effort(effort) ?? [] : [])];
116
+ const modelFlags = ({ agent, model, effort }) => [
117
+ ...(model ? (MODEL_FLAGS[agent]?.model(model) ?? []) : []),
118
+ ...(effort ? (MODEL_FLAGS[agent]?.effort(effort) ?? []) : []),
119
+ ];
67
120
 
68
121
  export function agentInvocation(task) {
69
122
  if (task.agent === "claude-code") {
70
123
  return {
71
124
  command: "claude",
72
125
  // A review reads and reports: plan mode lets Claude Code read and run read-only tools but never edit.
73
- args: ["-p", task.brief, "--output-format", "stream-json", "--verbose", ...(task.restricted ? restrictedClaude(task) : task.readOnly ? ["--permission-mode", "plan"] : ["--dangerously-skip-permissions"]), "--name", `Harness: ${task.title}`, ...modelFlags(task), ...(task.resumeSession ? ["--resume", task.resumeSession] : [])],
126
+ args: [
127
+ "-p",
128
+ task.brief,
129
+ "--output-format",
130
+ "stream-json",
131
+ "--verbose",
132
+ ...(task.restricted
133
+ ? restrictedClaude(task)
134
+ : task.readOnly
135
+ ? ["--permission-mode", "plan"]
136
+ : ["--dangerously-skip-permissions"]),
137
+ "--name",
138
+ `Harness: ${task.title}`,
139
+ ...modelFlags(task),
140
+ ...(task.resumeSession ? ["--resume", task.resumeSession] : []),
141
+ ],
74
142
  };
75
143
  }
76
144
  // Only Claude Code has a restricted mode; any other agent's restricted run is refused here, whatever let it start.
77
- if (task.restricted) throw new Error(`${task.agent} has no restricted mode; it runs only after the person's Start`);
145
+ if (task.restricted)
146
+ throw new Error(
147
+ `${task.agent} has no restricted mode; it runs only after the person's Start`
148
+ );
78
149
  if (task.agent === "codex") {
79
150
  return {
80
151
  command: "codex",
81
- args: ["exec", ...(task.resumeSession ? ["resume", task.resumeSession] : []), "--json", ...modelFlags(task), ...(task.readOnly ? ["-c", 'sandbox_mode="read-only"', "-c", 'approval_policy="never"'] : ["--dangerously-bypass-approvals-and-sandbox"]), "--skip-git-repo-check", "-"],
152
+ args: [
153
+ "exec",
154
+ ...(task.resumeSession ? ["resume", task.resumeSession] : []),
155
+ "--json",
156
+ ...modelFlags(task),
157
+ ...(task.readOnly
158
+ ? ["-c", 'sandbox_mode="read-only"', "-c", 'approval_policy="never"']
159
+ : ["--dangerously-bypass-approvals-and-sandbox"]),
160
+ "--skip-git-repo-check",
161
+ "-",
162
+ ],
82
163
  stdin: task.brief,
83
164
  };
84
165
  }
85
166
  // Pi has no permission prompts. --approve trusts project-local configuration for
86
167
  // this non-interactive run; its complete built-in tool set remains enabled.
87
- if (task.agent === "pi") return { command: "pi", args: ["--mode", "json", "--approve", ...modelFlags(task), "-p", task.brief] };
168
+ if (task.agent === "pi")
169
+ return {
170
+ command: "pi",
171
+ args: [
172
+ "--mode",
173
+ "json",
174
+ "--approve",
175
+ ...modelFlags(task),
176
+ "-p",
177
+ task.brief,
178
+ ],
179
+ };
88
180
  if (task.agent === "opencode") {
89
- return { command: "opencode", args: ["run", "--format", "json", "--auto", ...modelFlags(task), ...(task.resumeSession ? ["--session", task.resumeSession] : ["--title", task.title]), "--", task.brief] };
181
+ return {
182
+ command: "opencode",
183
+ args: [
184
+ "run",
185
+ "--format",
186
+ "json",
187
+ "--auto",
188
+ ...modelFlags(task),
189
+ ...(task.resumeSession
190
+ ? ["--session", task.resumeSession]
191
+ : ["--title", task.title]),
192
+ "--",
193
+ task.brief,
194
+ ],
195
+ };
90
196
  }
91
197
  throw new Error(`Unsupported agent: ${task.agent}`);
92
198
  }
@@ -95,7 +201,14 @@ export function agentInvocation(task) {
95
201
  // only when the agent prices its own work (Claude Code, Pi, OpenCode). Codex reports tokens only.
96
202
  // Nothing is priced here; a missing figure stays missing.
97
203
  export function usageCollector() {
98
- const usage = { input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, cost_usd: null, reported: false };
204
+ const usage = {
205
+ input_tokens: 0,
206
+ output_tokens: 0,
207
+ cache_read_tokens: 0,
208
+ cache_write_tokens: 0,
209
+ cost_usd: null,
210
+ reported: false,
211
+ };
99
212
  const add = (tokens, cost) => {
100
213
  usage.reported = true;
101
214
  usage.input_tokens += Number(tokens.input) || 0;
@@ -107,62 +220,182 @@ export function usageCollector() {
107
220
  return {
108
221
  read(event) {
109
222
  // Claude Code: one result event carries the run's totals.
110
- if (event.type === "result" && event.usage) { const u = event.usage; add({ input: u.input_tokens, output: u.output_tokens, cacheRead: u.cache_read_input_tokens, cacheWrite: u.cache_creation_input_tokens }, Number(event.total_cost_usd)); return; }
223
+ if (event.type === "result" && event.usage) {
224
+ const u = event.usage;
225
+ add(
226
+ {
227
+ input: u.input_tokens,
228
+ output: u.output_tokens,
229
+ cacheRead: u.cache_read_input_tokens,
230
+ cacheWrite: u.cache_creation_input_tokens,
231
+ },
232
+ Number(event.total_cost_usd)
233
+ );
234
+ return;
235
+ }
111
236
  // Codex: each completed turn carries its usage.
112
- if (event.type === "turn.completed" && event.usage) { const u = event.usage; add({ input: u.input_tokens, output: u.output_tokens, cacheRead: u.cached_input_tokens, cacheWrite: u.cache_write_input_tokens }, NaN); return; }
237
+ if (event.type === "turn.completed" && event.usage) {
238
+ const u = event.usage;
239
+ add(
240
+ {
241
+ input: u.input_tokens,
242
+ output: u.output_tokens,
243
+ cacheRead: u.cached_input_tokens,
244
+ cacheWrite: u.cache_write_input_tokens,
245
+ },
246
+ NaN
247
+ );
248
+ return;
249
+ }
113
250
  // Pi: each finished assistant message carries its usage and price.
114
- if (event.type === "message_end" && event.message?.role === "assistant" && event.message.usage) { const u = event.message.usage; add({ input: u.input, output: u.output, cacheRead: u.cacheRead, cacheWrite: u.cacheWrite }, Number(u.cost?.total)); return; }
251
+ if (
252
+ event.type === "message_end" &&
253
+ event.message?.role === "assistant" &&
254
+ event.message.usage
255
+ ) {
256
+ const u = event.message.usage;
257
+ add(
258
+ {
259
+ input: u.input,
260
+ output: u.output,
261
+ cacheRead: u.cacheRead,
262
+ cacheWrite: u.cacheWrite,
263
+ },
264
+ Number(u.cost?.total)
265
+ );
266
+ return;
267
+ }
115
268
  // OpenCode: each finished step carries its tokens and price.
116
269
  const part = event.part;
117
- if (part?.type === "step-finish" && part.tokens) add({ input: part.tokens.input, output: part.tokens.output, cacheRead: part.tokens.cache?.read, cacheWrite: part.tokens.cache?.write }, Number(part.cost));
270
+ if (part?.type === "step-finish" && part.tokens)
271
+ add(
272
+ {
273
+ input: part.tokens.input,
274
+ output: part.tokens.output,
275
+ cacheRead: part.tokens.cache?.read,
276
+ cacheWrite: part.tokens.cache?.write,
277
+ },
278
+ Number(part.cost)
279
+ );
280
+ },
281
+ result: () => {
282
+ const { reported, ...totals } = usage;
283
+ return reported ? totals : null;
118
284
  },
119
- result: () => { const { reported, ...totals } = usage; return reported ? totals : null; },
120
285
  };
121
286
  }
122
287
 
123
288
  // What an agent did, as the steps of one event in its JSON stream: "say" for its words to the person, "do" for a tool
124
289
  // it used. Claude Code, Codex, Pi and OpenCode each say this their own way; anything else is not a step.
125
290
  const brief = (input) => {
126
- if (!input || typeof input !== "object") return String(input ?? "").slice(0, 200);
127
- const named = input.command ?? input.cmd ?? input.file_path ?? input.path ?? input.filePath ?? input.pattern ?? input.url ?? input.query ?? input.description;
291
+ if (!input || typeof input !== "object")
292
+ return String(input ?? "").slice(0, 200);
293
+ const named =
294
+ input.command ??
295
+ input.cmd ??
296
+ input.file_path ??
297
+ input.path ??
298
+ input.filePath ??
299
+ input.pattern ??
300
+ input.url ??
301
+ input.query ??
302
+ input.description;
128
303
  return String(named ?? JSON.stringify(input)).slice(0, 200);
129
304
  };
130
305
  export function stepsOf(event) {
131
306
  if (!event || typeof event !== "object") return [];
132
307
  // A step as Harness writes it for a run another app carries out (t3.mjs).
133
- if (event.type === "harness.step" && (event.kind === "say" || event.kind === "do") && typeof event.text === "string") return [{ kind: event.kind, text: event.text }];
308
+ if (
309
+ event.type === "harness.step" &&
310
+ (event.kind === "say" || event.kind === "do") &&
311
+ typeof event.text === "string"
312
+ )
313
+ return [{ kind: event.kind, text: event.text }];
134
314
  if (event.type === "assistant" && Array.isArray(event.message?.content)) {
135
- return event.message.content.flatMap((block) => block.type === "text" && block.text?.trim() ? [{ kind: "say", text: block.text.trim() }]
136
- : block.type === "tool_use" ? [{ kind: "do", text: `${block.name}: ${brief(block.input)}` }] : []);
315
+ return event.message.content.flatMap((block) =>
316
+ block.type === "text" && block.text?.trim()
317
+ ? [{ kind: "say", text: block.text.trim() }]
318
+ : block.type === "tool_use"
319
+ ? [{ kind: "do", text: `${block.name}: ${brief(block.input)}` }]
320
+ : []
321
+ );
137
322
  }
138
323
  if (event.type === "item.completed" && event.item) {
139
324
  const item = event.item;
140
- if (item.type === "agent_message" && item.text?.trim()) return [{ kind: "say", text: item.text.trim() }];
141
- if (item.type === "command_execution") return [{ kind: "do", text: `ran: ${String(item.command ?? "").slice(0, 200)}` }];
142
- if (item.type === "file_change") return [{ kind: "do", text: `edited: ${(item.changes ?? []).map((change) => change.path).join(", ").slice(0, 200)}` }];
143
- if (item.type === "mcp_tool_call") return [{ kind: "do", text: `${item.server ?? "tool"}: ${item.tool ?? ""}` }];
144
- if (item.type === "web_search") return [{ kind: "do", text: `searched: ${String(item.query ?? "").slice(0, 200)}` }];
325
+ if (item.type === "agent_message" && item.text?.trim())
326
+ return [{ kind: "say", text: item.text.trim() }];
327
+ if (item.type === "command_execution")
328
+ return [
329
+ {
330
+ kind: "do",
331
+ text: `ran: ${String(item.command ?? "").slice(0, 200)}`,
332
+ },
333
+ ];
334
+ if (item.type === "file_change")
335
+ return [
336
+ {
337
+ kind: "do",
338
+ text: `edited: ${(item.changes ?? [])
339
+ .map((change) => change.path)
340
+ .join(", ")
341
+ .slice(0, 200)}`,
342
+ },
343
+ ];
344
+ if (item.type === "mcp_tool_call")
345
+ return [
346
+ { kind: "do", text: `${item.server ?? "tool"}: ${item.tool ?? ""}` },
347
+ ];
348
+ if (item.type === "web_search")
349
+ return [
350
+ {
351
+ kind: "do",
352
+ text: `searched: ${String(item.query ?? "").slice(0, 200)}`,
353
+ },
354
+ ];
145
355
  return [];
146
356
  }
147
- if (event.type === "message_end" && event.message?.role === "assistant" && Array.isArray(event.message.content)) {
148
- return event.message.content.flatMap((block) => block.type === "text" && block.text?.trim() ? [{ kind: "say", text: block.text.trim() }]
149
- : block.type === "toolCall" ? [{ kind: "do", text: `${block.name}: ${brief(block.arguments)}` }] : []);
357
+ if (
358
+ event.type === "message_end" &&
359
+ event.message?.role === "assistant" &&
360
+ Array.isArray(event.message.content)
361
+ ) {
362
+ return event.message.content.flatMap((block) =>
363
+ block.type === "text" && block.text?.trim()
364
+ ? [{ kind: "say", text: block.text.trim() }]
365
+ : block.type === "toolCall"
366
+ ? [{ kind: "do", text: `${block.name}: ${brief(block.arguments)}` }]
367
+ : []
368
+ );
150
369
  }
151
370
  const part = event.part;
152
- if (part?.type === "text" && part.text?.trim() && (part.time?.end || event.type === "text")) return [{ kind: "say", text: part.text.trim() }];
153
- if (part?.type === "tool" && part.state?.status === "completed") return [{ kind: "do", text: `${part.tool}: ${brief(part.state.input)}` }];
371
+ if (
372
+ part?.type === "text" &&
373
+ part.text?.trim() &&
374
+ (part.time?.end || event.type === "text")
375
+ )
376
+ return [{ kind: "say", text: part.text.trim() }];
377
+ if (part?.type === "tool" && part.state?.status === "completed")
378
+ return [{ kind: "do", text: `${part.tool}: ${brief(part.state.input)}` }];
154
379
  return [];
155
380
  }
156
381
 
157
382
  /** The steps of a run from its log, oldest first; at most `limit`, the latest kept. */
158
383
  export function readRunSteps(file, limit = 200) {
159
384
  let raw = "";
160
- try { raw = fs.readFileSync(file, "utf8"); } catch { return []; }
385
+ try {
386
+ raw = fs.readFileSync(file, "utf8");
387
+ } catch {
388
+ return [];
389
+ }
161
390
  const steps = [];
162
391
  for (const line of raw.split("\n")) {
163
392
  if (!line.startsWith("{")) continue;
164
393
  let event;
165
- try { event = JSON.parse(line); } catch { continue; }
394
+ try {
395
+ event = JSON.parse(line);
396
+ } catch {
397
+ continue;
398
+ }
166
399
  steps.push(...stepsOf(event));
167
400
  }
168
401
  return steps.slice(-limit);
@@ -172,7 +405,10 @@ export function runAgentTask(task, emit, observe = () => {}) {
172
405
  // One run per folder: two agents in one checkout would trip over each other; a thread's worktree is its own folder.
173
406
  const slot = task.cwd ? path.resolve(task.cwd) : task.agent;
174
407
  if (running.has(slot)) {
175
- emit("failed", `Another run is already working in ${task.cwd || "that folder"}.`);
408
+ emit(
409
+ "failed",
410
+ `Another run is already working in ${task.cwd || "that folder"}.`
411
+ );
176
412
  return;
177
413
  }
178
414
  let cwd;
@@ -191,7 +427,10 @@ export function runAgentTask(task, emit, observe = () => {}) {
191
427
  emit("accepted");
192
428
  let child;
193
429
  try {
194
- child = spawn(invocation.command, invocation.args, { cwd, env: environment.env });
430
+ child = spawn(invocation.command, invocation.args, {
431
+ cwd,
432
+ env: environment.env,
433
+ });
195
434
  // An agent that exits before reading its brief breaks the pipe; its exit says how the run went.
196
435
  child.stdin.on("error", () => {});
197
436
  child.stdin.end(invocation.stdin);
@@ -219,23 +458,53 @@ export function runAgentTask(task, emit, observe = () => {}) {
219
458
  if (!line.trim()) continue;
220
459
  tail = (tail + "\n" + line).slice(-4_000);
221
460
  let event;
222
- try { event = JSON.parse(line); } catch { continue; }
223
- const sessionId = event.session_id || event.sessionId || event.sessionID || event.thread_id || event.thread?.id || event.properties?.sessionID;
461
+ try {
462
+ event = JSON.parse(line);
463
+ } catch {
464
+ continue;
465
+ }
466
+ const sessionId =
467
+ event.session_id ||
468
+ event.sessionId ||
469
+ event.sessionID ||
470
+ event.thread_id ||
471
+ event.thread?.id ||
472
+ event.properties?.sessionID;
224
473
  if (!sessionReported && typeof sessionId === "string" && sessionId) {
225
474
  sessionReported = true;
226
475
  emit("session", sessionId, { cwd });
227
476
  }
228
477
  usage.read(event);
229
- if (event.type === "result" && typeof event.result === "string") resultText = event.result;
230
- if (event.type === "item.completed" && event.item?.type === "agent_message" && typeof event.item.text === "string") resultText = event.item.text;
231
- if (["text", "message.part.updated"].includes(event.type) && typeof event.part?.text === "string") resultText = event.part.text;
478
+ if (event.type === "result" && typeof event.result === "string")
479
+ resultText = event.result;
480
+ if (
481
+ event.type === "item.completed" &&
482
+ event.item?.type === "agent_message" &&
483
+ typeof event.item.text === "string"
484
+ )
485
+ resultText = event.item.text;
486
+ if (
487
+ ["text", "message.part.updated"].includes(event.type) &&
488
+ typeof event.part?.text === "string"
489
+ )
490
+ resultText = event.part.text;
232
491
  // Pi: its last words are its answer; when its model call fails, Pi still exits 0 with the error on the message.
233
492
  if (event.type === "message_end" && event.message?.role === "assistant") {
234
- const said = (Array.isArray(event.message.content) ? event.message.content : []).filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
493
+ const said = (
494
+ Array.isArray(event.message.content) ? event.message.content : []
495
+ )
496
+ .filter((block) => block.type === "text")
497
+ .map((block) => block.text)
498
+ .join("\n")
499
+ .trim();
235
500
  if (said) resultText = said;
236
- modelError = event.message.stopReason === "error" ? String(event.message.errorMessage || "no error message") : "";
501
+ modelError =
502
+ event.message.stopReason === "error"
503
+ ? String(event.message.errorMessage || "no error message")
504
+ : "";
237
505
  }
238
- for (const step of stepsOf(event)) emit("progress", step.text.slice(0, 200), { step });
506
+ for (const step of stepsOf(event))
507
+ emit("progress", step.text.slice(0, 200), { step });
239
508
  }
240
509
  });
241
510
  child.stderr?.on("data", (chunk) => {
@@ -246,9 +515,30 @@ export function runAgentTask(task, emit, observe = () => {}) {
246
515
  child.on("close", (code) => {
247
516
  running.delete(slot);
248
517
  children.delete(task.runId);
249
- if (code === 0 && modelError) emit("failed", `${task.agent} stopped on an error from its model: ${modelError}`.slice(0, 4_000), { usage: usage.result() });
250
- else if (code === 0) emit("done", (resultText || "The run finished; check the repository for its changes.").slice(0, 20_000), { usage: usage.result() });
251
- else emit("failed", (`${task.agent} exited with code ${code}.\n${tail}`).slice(0, 4_000), { usage: usage.result() });
518
+ if (code === 0 && modelError)
519
+ emit(
520
+ "failed",
521
+ `${task.agent} stopped on an error from its model: ${modelError}`.slice(
522
+ 0,
523
+ 4_000
524
+ ),
525
+ { usage: usage.result() }
526
+ );
527
+ else if (code === 0)
528
+ emit(
529
+ "done",
530
+ (
531
+ resultText ||
532
+ "The run finished; check the repository for its changes."
533
+ ).slice(0, 20_000),
534
+ { usage: usage.result() }
535
+ );
536
+ else
537
+ emit(
538
+ "failed",
539
+ `${task.agent} exited with code ${code}.\n${tail}`.slice(0, 4_000),
540
+ { usage: usage.result() }
541
+ );
252
542
  });
253
543
  child.on("error", (error) => {
254
544
  running.delete(slot);
@@ -15,12 +15,20 @@ function dependencyTarget(root, name) {
15
15
  // What the runtime keeps of a large package. onnxruntime-node ships native binaries for every OS (about 200 MB); the
16
16
  // daemon needs its JavaScript and this machine's own binary.
17
17
  const KEEP = {
18
- "onnxruntime-node": ["package.json", "dist", "lib", `bin/napi-v6/${process.platform}/${process.arch}`],
18
+ "onnxruntime-node": [
19
+ "package.json",
20
+ "dist",
21
+ "lib",
22
+ `bin/napi-v6/${process.platform}/${process.arch}`,
23
+ ],
19
24
  };
20
25
 
21
26
  function copyPackage(source, target, name) {
22
27
  const keep = KEEP[name];
23
- if (!keep) { fs.cpSync(source, target, { recursive: true, force: true }); return; }
28
+ if (!keep) {
29
+ fs.cpSync(source, target, { recursive: true, force: true });
30
+ return;
31
+ }
24
32
  for (const file of keep) {
25
33
  const from = path.join(source, file);
26
34
  if (!fs.existsSync(from)) continue;
@@ -30,24 +38,32 @@ function copyPackage(source, target, name) {
30
38
  }
31
39
 
32
40
  function copyDependencyClosure(sourceRoot, releaseRoot, roots) {
33
- const queue = roots.map((name) => ({ name, require: createRequire(path.join(sourceRoot, "package.json")) }));
41
+ const queue = roots.map((name) => ({
42
+ name,
43
+ require: createRequire(path.join(sourceRoot, "package.json")),
44
+ }));
34
45
  const copied = new Set();
35
46
  while (queue.length) {
36
47
  const item = queue.shift();
37
48
  if (copied.has(item.name)) continue;
38
49
  let packageJson;
39
- try { packageJson = item.require.resolve(`${item.name}/package.json`); }
40
- catch {
50
+ try {
51
+ packageJson = item.require.resolve(`${item.name}/package.json`);
52
+ } catch {
41
53
  // Packages with restrictive exports still resolve their main file. Walk upward to
42
54
  // the package.json whose name matches the requested dependency.
43
55
  let current = path.dirname(item.require.resolve(item.name));
44
56
  while (current !== path.dirname(current)) {
45
57
  const candidate = path.join(current, "package.json");
46
- if (readJson(candidate, {}).name === item.name) { packageJson = candidate; break; }
58
+ if (readJson(candidate, {}).name === item.name) {
59
+ packageJson = candidate;
60
+ break;
61
+ }
47
62
  current = path.dirname(current);
48
63
  }
49
64
  }
50
- if (!packageJson) throw new Error(`could not package runtime dependency ${item.name}`);
65
+ if (!packageJson)
66
+ throw new Error(`could not package runtime dependency ${item.name}`);
51
67
  const source = packageDirectory(packageJson);
52
68
  const target = dependencyTarget(releaseRoot, item.name);
53
69
  ensureDir(path.dirname(target));
@@ -55,7 +71,10 @@ function copyDependencyClosure(sourceRoot, releaseRoot, roots) {
55
71
  copied.add(item.name);
56
72
  const metadata = readJson(packageJson, {});
57
73
  const nestedRequire = createRequire(packageJson);
58
- for (const name of Object.keys({ ...(metadata.dependencies || {}), ...(metadata.optionalDependencies || {}) })) {
74
+ for (const name of Object.keys({
75
+ ...(metadata.dependencies || {}),
76
+ ...(metadata.optionalDependencies || {}),
77
+ })) {
59
78
  try {
60
79
  nestedRequire.resolve(name);
61
80
  queue.push({ name, require: nestedRequire });
@@ -71,23 +90,45 @@ export function installRuntime({ sourceRoot, version }) {
71
90
  const staging = `${release}.staging-${process.pid}`;
72
91
  fs.rmSync(staging, { recursive: true, force: true });
73
92
  ensureDir(staging);
74
- for (const entry of ["cli.mjs", "package.json", "README.md", "lib", "assets"]) {
93
+ for (const entry of [
94
+ "cli.mjs",
95
+ "package.json",
96
+ "README.md",
97
+ "lib",
98
+ "assets",
99
+ ]) {
75
100
  const source = path.join(sourceRoot, entry);
76
101
  if (!fs.existsSync(source)) continue;
77
- fs.cpSync(source, path.join(staging, entry), { recursive: true, force: true });
102
+ fs.cpSync(source, path.join(staging, entry), {
103
+ recursive: true,
104
+ force: true,
105
+ });
78
106
  }
79
107
  // Everything the package depends on, so a dependency added there cannot be left out here.
80
- copyDependencyClosure(sourceRoot, staging, Object.keys(readJson(path.join(sourceRoot, "package.json"), {}).dependencies || {}));
81
- fs.writeFileSync(path.join(staging, ".ready"), version + "\n", { mode: 0o600 });
108
+ copyDependencyClosure(
109
+ sourceRoot,
110
+ staging,
111
+ Object.keys(
112
+ readJson(path.join(sourceRoot, "package.json"), {}).dependencies || {}
113
+ )
114
+ );
115
+ fs.writeFileSync(path.join(staging, ".ready"), version + "\n", {
116
+ mode: 0o600,
117
+ });
82
118
  ensureDir(path.dirname(release));
83
119
  fs.rmSync(release, { recursive: true, force: true });
84
120
  fs.renameSync(staging, release);
85
121
  }
86
- writeJsonAtomic(path.join(paths.runtime, "current.json"), { version, path: release });
122
+ writeJsonAtomic(path.join(paths.runtime, "current.json"), {
123
+ version,
124
+ path: release,
125
+ });
87
126
  return release;
88
127
  }
89
128
 
90
129
  export function currentRuntime() {
91
130
  const current = readJson(path.join(paths.runtime, "current.json"), {});
92
- return typeof current.path === "string" && fs.existsSync(current.path) ? current.path : null;
131
+ return typeof current.path === "string" && fs.existsSync(current.path)
132
+ ? current.path
133
+ : null;
93
134
  }