tryharness 0.7.23 → 0.7.25
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 +5248 -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 +382 -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/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 () => {
|
|
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))
|
|
40
|
+
if (!requested || !path.isAbsolute(requested))
|
|
41
|
+
throw new Error("Delegated tasks require an absolute working directory.");
|
|
39
42
|
let stat;
|
|
40
|
-
try {
|
|
41
|
-
|
|
42
|
-
|
|
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: {
|
|
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 = () => ({
|
|
56
|
-
|
|
57
|
-
|
|
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": {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 }) => [
|
|
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: [
|
|
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)
|
|
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: [
|
|
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")
|
|
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 {
|
|
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 = {
|
|
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) {
|
|
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) {
|
|
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 (
|
|
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)
|
|
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")
|
|
127
|
-
|
|
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 (
|
|
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) =>
|
|
136
|
-
|
|
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())
|
|
141
|
-
|
|
142
|
-
if (item.type === "
|
|
143
|
-
|
|
144
|
-
|
|
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 (
|
|
148
|
-
|
|
149
|
-
|
|
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 (
|
|
153
|
-
|
|
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 {
|
|
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 {
|
|
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(
|
|
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, {
|
|
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 {
|
|
223
|
-
|
|
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")
|
|
230
|
-
|
|
231
|
-
if (
|
|
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 = (
|
|
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 =
|
|
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))
|
|
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)
|
|
250
|
-
|
|
251
|
-
|
|
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);
|
package/lib/runtime-install.mjs
CHANGED
|
@@ -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": [
|
|
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) {
|
|
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) => ({
|
|
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 {
|
|
40
|
-
|
|
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) {
|
|
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)
|
|
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({
|
|
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 [
|
|
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), {
|
|
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(
|
|
81
|
-
|
|
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"), {
|
|
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)
|
|
131
|
+
return typeof current.path === "string" && fs.existsSync(current.path)
|
|
132
|
+
? current.path
|
|
133
|
+
: null;
|
|
93
134
|
}
|