@mutagent/cli 0.1.268 → 0.1.269
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/dist/bin/cli.js +164 -30
- package/dist/bin/cli.js.map +6 -6
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -6417,25 +6417,32 @@ async function fetchSandboxTraces(id) {
|
|
|
6417
6417
|
}
|
|
6418
6418
|
async function openAttachStream(id, options = {}) {
|
|
6419
6419
|
const query = options.since === undefined ? "" : `?since=${String(options.since)}`;
|
|
6420
|
-
return openStream(`/api/sandbox/${encodeURIComponent(id)}/stream${query}`, id, options);
|
|
6420
|
+
return openStream(`/api/sandbox/${encodeURIComponent(id)}/stream${query}`, { id, retryHint: `Retry with: mutagent helix attach ${id}` }, options);
|
|
6421
6421
|
}
|
|
6422
6422
|
async function openExecStream(id, body, options = {}) {
|
|
6423
|
-
return openStream(`/api/sandbox/${encodeURIComponent(id)}/exec?stream=1`, id, options, {
|
|
6423
|
+
return openStream(`/api/sandbox/${encodeURIComponent(id)}/exec?stream=1`, { id, retryHint: `Retry with: mutagent helix attach ${id}` }, options, {
|
|
6424
6424
|
method: "POST",
|
|
6425
6425
|
body: JSON.stringify(body),
|
|
6426
6426
|
headers: { "Content-Type": "application/json" }
|
|
6427
6427
|
});
|
|
6428
6428
|
}
|
|
6429
|
-
async function
|
|
6429
|
+
async function openAgentRunStream(body, options = {}) {
|
|
6430
|
+
return openStream("/api/sandbox/agent/run?stream=1", { retryHint: "Retry the turn, or run it blocking with: mutagent helix run ... --no-stream" }, options, {
|
|
6431
|
+
method: "POST",
|
|
6432
|
+
body: JSON.stringify(body),
|
|
6433
|
+
headers: { "Content-Type": "application/json" }
|
|
6434
|
+
});
|
|
6435
|
+
}
|
|
6436
|
+
async function openStream(path, sandbox, options, init = {}) {
|
|
6430
6437
|
const { response } = await sandboxFetch(path, {
|
|
6431
6438
|
...init,
|
|
6432
6439
|
headers: { Accept: "text/event-stream", ...toHeaderRecord(init.headers) },
|
|
6433
6440
|
signal: options.signal
|
|
6434
6441
|
});
|
|
6435
6442
|
if (!response.ok)
|
|
6436
|
-
throw await toCliError(response,
|
|
6443
|
+
throw await toCliError(response, sandbox.id);
|
|
6437
6444
|
if (!response.body) {
|
|
6438
|
-
throw new MutagentError("EMPTY_STREAM", "The server accepted the stream request but sent no body.",
|
|
6445
|
+
throw new MutagentError("EMPTY_STREAM", "The server accepted the stream request but sent no body.", sandbox.retryHint);
|
|
6439
6446
|
}
|
|
6440
6447
|
return response.body;
|
|
6441
6448
|
}
|
|
@@ -6463,6 +6470,31 @@ function parseStatusPayload(data) {
|
|
|
6463
6470
|
exitCode: typeof record.exitCode === "number" ? record.exitCode : null
|
|
6464
6471
|
};
|
|
6465
6472
|
}
|
|
6473
|
+
function parseSandboxRunPayload(data) {
|
|
6474
|
+
const record = parseRecord(data);
|
|
6475
|
+
if (!record)
|
|
6476
|
+
return null;
|
|
6477
|
+
const { sandboxId, preset } = record;
|
|
6478
|
+
if (typeof sandboxId !== "string" || sandboxId === "")
|
|
6479
|
+
return null;
|
|
6480
|
+
return { sandboxId, preset: typeof preset === "string" ? preset : "" };
|
|
6481
|
+
}
|
|
6482
|
+
function parseSandboxResultPayload(data) {
|
|
6483
|
+
const record = parseRecord(data);
|
|
6484
|
+
if (!record)
|
|
6485
|
+
return null;
|
|
6486
|
+
const { sandboxId, preset, exitCode, timedOut, tornDown, error } = record;
|
|
6487
|
+
if (typeof sandboxId !== "string" || sandboxId === "")
|
|
6488
|
+
return null;
|
|
6489
|
+
return {
|
|
6490
|
+
sandboxId,
|
|
6491
|
+
preset: typeof preset === "string" ? preset : "",
|
|
6492
|
+
exitCode: typeof exitCode === "number" ? exitCode : null,
|
|
6493
|
+
timedOut: timedOut === true,
|
|
6494
|
+
tornDown: tornDown === true,
|
|
6495
|
+
...typeof error === "string" ? { error } : {}
|
|
6496
|
+
};
|
|
6497
|
+
}
|
|
6466
6498
|
function parseErrorPayload(data) {
|
|
6467
6499
|
const record = parseRecord(data);
|
|
6468
6500
|
const message = record && typeof record.message === "string" ? record.message : data.trim();
|
|
@@ -6580,12 +6612,24 @@ async function runStreamSession(options) {
|
|
|
6580
6612
|
const retry = options.retry ?? DEFAULT_RETRY;
|
|
6581
6613
|
const sleep2 = options.sleep ?? delay;
|
|
6582
6614
|
const { sink, signal } = options;
|
|
6615
|
+
const onDisconnect = options.onDisconnect ?? "survives";
|
|
6583
6616
|
let lastSeq = options.since ?? 0;
|
|
6584
6617
|
let exitCode = null;
|
|
6585
6618
|
let reconnects = 0;
|
|
6586
6619
|
let gaps = 0;
|
|
6587
6620
|
let attempt = 0;
|
|
6588
|
-
|
|
6621
|
+
let sandbox;
|
|
6622
|
+
let result;
|
|
6623
|
+
const outcome = (end) => ({
|
|
6624
|
+
end,
|
|
6625
|
+
lastSeq,
|
|
6626
|
+
exitCode,
|
|
6627
|
+
reconnects,
|
|
6628
|
+
gaps,
|
|
6629
|
+
...sandbox === undefined ? {} : { sandbox },
|
|
6630
|
+
...result === undefined ? {} : { result }
|
|
6631
|
+
});
|
|
6632
|
+
const detached = () => outcome("detached");
|
|
6589
6633
|
const isAborted = () => signal.aborted;
|
|
6590
6634
|
for (;; ) {
|
|
6591
6635
|
if (isAborted())
|
|
@@ -6600,7 +6644,7 @@ async function runStreamSession(options) {
|
|
|
6600
6644
|
throw error;
|
|
6601
6645
|
attempt += 1;
|
|
6602
6646
|
if (attempt > retry.attempts)
|
|
6603
|
-
throw exhausted(lastSeq, error);
|
|
6647
|
+
throw exhausted(lastSeq, error, onDisconnect);
|
|
6604
6648
|
reconnects += 1;
|
|
6605
6649
|
sink.notice(reconnectNotice(attempt, retry.attempts, lastSeq));
|
|
6606
6650
|
await sleep2(backoffMs(retry.baseDelayMs, attempt), signal);
|
|
@@ -6625,6 +6669,20 @@ async function runStreamSession(options) {
|
|
|
6625
6669
|
sink.stderr(payload.text, payload.seq);
|
|
6626
6670
|
continue;
|
|
6627
6671
|
}
|
|
6672
|
+
if (event.event === "sandbox") {
|
|
6673
|
+
const payload = parseSandboxRunPayload(event.data);
|
|
6674
|
+
if (payload) {
|
|
6675
|
+
sandbox = payload;
|
|
6676
|
+
options.onSandbox?.(payload);
|
|
6677
|
+
}
|
|
6678
|
+
continue;
|
|
6679
|
+
}
|
|
6680
|
+
if (event.event === "result") {
|
|
6681
|
+
const payload = parseSandboxResultPayload(event.data);
|
|
6682
|
+
if (payload)
|
|
6683
|
+
result = payload;
|
|
6684
|
+
continue;
|
|
6685
|
+
}
|
|
6628
6686
|
if (event.event === "status") {
|
|
6629
6687
|
const payload = parseStatusPayload(event.data);
|
|
6630
6688
|
if (payload?.state === "exited") {
|
|
@@ -6650,19 +6708,19 @@ async function runStreamSession(options) {
|
|
|
6650
6708
|
return detached();
|
|
6651
6709
|
attempt += 1;
|
|
6652
6710
|
if (attempt > retry.attempts)
|
|
6653
|
-
throw exhausted(lastSeq, error);
|
|
6711
|
+
throw exhausted(lastSeq, error, onDisconnect);
|
|
6654
6712
|
reconnects += 1;
|
|
6655
6713
|
sink.notice(reconnectNotice(attempt, retry.attempts, lastSeq));
|
|
6656
6714
|
await sleep2(backoffMs(retry.baseDelayMs, attempt), signal);
|
|
6657
6715
|
continue;
|
|
6658
6716
|
}
|
|
6659
6717
|
if (exited)
|
|
6660
|
-
return
|
|
6718
|
+
return outcome("exited");
|
|
6661
6719
|
if (isAborted())
|
|
6662
6720
|
return detached();
|
|
6663
6721
|
attempt += 1;
|
|
6664
6722
|
if (attempt > retry.attempts)
|
|
6665
|
-
throw exhausted(lastSeq, null);
|
|
6723
|
+
throw exhausted(lastSeq, null, onDisconnect);
|
|
6666
6724
|
reconnects += 1;
|
|
6667
6725
|
sink.notice(reconnectNotice(attempt, retry.attempts, lastSeq));
|
|
6668
6726
|
await sleep2(backoffMs(retry.baseDelayMs, attempt), signal);
|
|
@@ -6678,9 +6736,10 @@ function gapNotice(lastSeq, nextSeq) {
|
|
|
6678
6736
|
const missing = nextSeq - lastSeq - 1;
|
|
6679
6737
|
return `Output gap: ${String(missing)} event(s) between seq ${String(lastSeq)} and ` + `${String(nextSeq)} are unavailable — the server buffer rolled past the resume point. ` + "What follows is not continuous with what came before.";
|
|
6680
6738
|
}
|
|
6681
|
-
function exhausted(lastSeq, cause) {
|
|
6739
|
+
function exhausted(lastSeq, cause, onDisconnect) {
|
|
6682
6740
|
const detail = cause instanceof Error ? ` (${cause.message})` : "";
|
|
6683
|
-
|
|
6741
|
+
const guidance = onDisconnect === "released" ? "This was a one-shot run: the server releases the sandbox as soon as the connection drops, so nothing is left running and there is nothing to re-attach to. Run the turn again, or use --no-stream to wait for the whole result in one reply." : `The sandbox is still running — nothing was destroyed. Resume with: mutagent helix attach <id> --since ${String(lastSeq)}`;
|
|
6742
|
+
return new MutagentError("STREAM_DISCONNECTED", `Lost the sandbox stream and could not reconnect${detail}.`, guidance, 5);
|
|
6684
6743
|
}
|
|
6685
6744
|
function mentionsLostBuffer(message) {
|
|
6686
6745
|
return /buffer|rolled|expired|evicted|no longer available|too old/i.test(message);
|
|
@@ -6754,29 +6813,45 @@ async function attachToSandbox(options) {
|
|
|
6754
6813
|
open: options.open,
|
|
6755
6814
|
sink,
|
|
6756
6815
|
since: options.since,
|
|
6757
|
-
signal: controller.signal
|
|
6816
|
+
signal: controller.signal,
|
|
6817
|
+
...options.retry === undefined ? {} : { retry: options.retry },
|
|
6818
|
+
...options.onDisconnect === undefined ? {} : { onDisconnect: options.onDisconnect },
|
|
6819
|
+
...options.onSandbox === undefined ? {} : { onSandbox: options.onSandbox }
|
|
6758
6820
|
});
|
|
6759
|
-
reportOutcome(options.id, result, sink);
|
|
6821
|
+
reportOutcome(options.id, result, sink, options.onDisconnect ?? "survives");
|
|
6760
6822
|
return result;
|
|
6761
6823
|
} finally {
|
|
6762
6824
|
process.removeListener("SIGINT", onSigint);
|
|
6763
6825
|
}
|
|
6764
6826
|
}
|
|
6765
|
-
function reportOutcome(id, result, sink) {
|
|
6827
|
+
function reportOutcome(id, result, sink, onDisconnect) {
|
|
6828
|
+
const box = result.sandbox?.sandboxId ?? id;
|
|
6766
6829
|
if (result.end === "detached") {
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6830
|
+
if (onDisconnect === "released") {
|
|
6831
|
+
sink.notice(box === "" ? "Stopped. The turn was cancelled and the sandbox released — nothing is left running." : `Stopped. The turn was cancelled and sandbox ${box} released — nothing is left running.`);
|
|
6832
|
+
sink.notice("Run the turn again to retry, or add --keep next time to hold the sandbox open.");
|
|
6833
|
+
return;
|
|
6834
|
+
}
|
|
6835
|
+
sink.notice(`Detached. Sandbox ${box} is still running — nothing was destroyed.`);
|
|
6836
|
+
sink.notice(`Re-attach with: ${reattachHint(box, result.lastSeq)}`);
|
|
6837
|
+
sink.notice(`Destroy it with: mutagent helix rm ${box} --force`);
|
|
6770
6838
|
return;
|
|
6771
6839
|
}
|
|
6772
6840
|
if (result.gaps > 0) {
|
|
6773
6841
|
sink.notice(`Session ended, but ${String(result.gaps)} gap(s) occurred — the output above is not complete.`);
|
|
6774
6842
|
}
|
|
6843
|
+
if (result.result !== undefined && result.result.tornDown === false) {
|
|
6844
|
+
sink.notice(`Sandbox ${result.result.sandboxId} was NOT torn down. Destroy it with: mutagent helix rm ${result.result.sandboxId} --force`);
|
|
6845
|
+
}
|
|
6846
|
+
if (result.result?.timedOut === true) {
|
|
6847
|
+
sink.notice("The turn timed out.");
|
|
6848
|
+
}
|
|
6775
6849
|
if (result.exitCode === null) {
|
|
6776
|
-
|
|
6850
|
+
const why = result.result?.error;
|
|
6851
|
+
sink.notice(why === undefined ? `Sandbox ${box} exited (no exit code reported).` : `The turn did not complete: ${why}`);
|
|
6777
6852
|
return;
|
|
6778
6853
|
}
|
|
6779
|
-
sink.notice(`Sandbox ${
|
|
6854
|
+
sink.notice(`Sandbox ${box} exited with code ${String(result.exitCode)}.`);
|
|
6780
6855
|
if (result.exitCode !== 0) {
|
|
6781
6856
|
process.exitCode = result.exitCode;
|
|
6782
6857
|
}
|
|
@@ -7004,7 +7079,7 @@ AI Agent Directive:
|
|
|
7004
7079
|
import chalk22 from "chalk";
|
|
7005
7080
|
init_errors();
|
|
7006
7081
|
function registerRunCommand(parent) {
|
|
7007
|
-
parent.command("run").description("Run one Helix agent turn in a fresh sandbox").argument("<task...>", "WHAT to do this turn").requiredOption("--prompt <definition>", "WHO the agent is — the agent definition").option("--preset <name>", "Preset to run on (default: the backend default)").option("--keep", "Keep the sandbox afterwards instead of tearing it down").option("--timeout-ms <n>", "Per-command timeout in milliseconds").addHelpText("after", `
|
|
7082
|
+
parent.command("run").description("Run one Helix agent turn in a fresh sandbox").argument("<task...>", "WHAT to do this turn").requiredOption("--prompt <definition>", "WHO the agent is — the agent definition").option("--preset <name>", "Preset to run on (default: the backend default)").option("--keep", "Keep the sandbox afterwards instead of tearing it down").option("--timeout-ms <n>", "Per-command timeout in milliseconds").option("--no-stream", "Wait for the whole turn instead of watching it happen").addHelpText("after", `
|
|
7008
7083
|
Examples:
|
|
7009
7084
|
${chalk22.dim("$")} mutagent helix run "summarise the failing tests" --prompt "You are a test triage agent."
|
|
7010
7085
|
${chalk22.dim("$")} mutagent helix run fix the flaky login spec --prompt "$(cat agent.md)"
|
|
@@ -7022,13 +7097,27 @@ and tears the sandbox down. There is no sandbox id argument because there is no
|
|
|
7022
7097
|
endpoint that runs an agent turn inside an existing box. Pass --keep to hold the
|
|
7023
7098
|
sandbox open; the id is reported so you can attach or exec against it.
|
|
7024
7099
|
|
|
7025
|
-
The turn
|
|
7026
|
-
|
|
7027
|
-
|
|
7100
|
+
The turn STREAMS by default — output appears as the agent produces it, and the
|
|
7101
|
+
sandbox id is printed before the first line so you always know what is running.
|
|
7102
|
+
Its exit code becomes this command's exit code.
|
|
7103
|
+
|
|
7104
|
+
Ctrl-C CANCELS THE TURN. This is the one place in 'mutagent helix' where it does:
|
|
7105
|
+
a one-shot sandbox belongs to this connection, so leaving releases it. Elsewhere
|
|
7106
|
+
Ctrl-C detaches and the box keeps running. Use --keep if you want it to survive.
|
|
7107
|
+
|
|
7108
|
+
There is no reconnect on a streamed run. If the connection drops the turn is
|
|
7109
|
+
lost, and you are told so — retrying would spawn a second sandbox and run the
|
|
7110
|
+
turn again. Use --no-stream over a flaky link.
|
|
7111
|
+
|
|
7112
|
+
See 'mutagent helix presets' for what --preset accepts.
|
|
7028
7113
|
|
|
7029
7114
|
AI Agent Directive:
|
|
7030
|
-
--json returns ONE object: { success, result: { sandboxId,
|
|
7031
|
-
stderr, timedOut, tornDown }, _links }.
|
|
7115
|
+
--no-stream --json returns ONE object: { success, result: { sandboxId,
|
|
7116
|
+
exitCode, stdout, stderr, timedOut, tornDown }, _links }. Prefer it for a
|
|
7117
|
+
result you intend to parse.
|
|
7118
|
+
--json while streaming emits NDJSON — one object per line — and the LAST line
|
|
7119
|
+
is a complete summary object, so '| tail -1' still yields one object. Same
|
|
7120
|
+
convention as 'mutagent helix exec'.
|
|
7032
7121
|
Both --prompt and the task are required; do not fold one into the other.
|
|
7033
7122
|
`).action(async (taskParts, options) => {
|
|
7034
7123
|
const isJson = getJsonFlag(parent);
|
|
@@ -7043,17 +7132,62 @@ AI Agent Directive:
|
|
|
7043
7132
|
throw new MutagentError("MISSING_ARGUMENTS", "--prompt is required — it is WHO the agent is, not what it should do.", `The task you already passed is the WHAT. Add the agent definition:
|
|
7044
7133
|
--prompt "You are a ..." (or --prompt "$(cat agent.md)")`);
|
|
7045
7134
|
}
|
|
7046
|
-
const
|
|
7135
|
+
const body = {
|
|
7047
7136
|
prompt,
|
|
7048
7137
|
task,
|
|
7049
7138
|
...oneShotFields(options.preset, options.keep, options.timeoutMs)
|
|
7050
|
-
}
|
|
7051
|
-
|
|
7139
|
+
};
|
|
7140
|
+
if (options.stream === false) {
|
|
7141
|
+
const result = await agentRun(body);
|
|
7142
|
+
reportResult(result, output, isJson, { prompt, task });
|
|
7143
|
+
return;
|
|
7144
|
+
}
|
|
7145
|
+
await streamAgentTurn(body, isJson, options.keep === true);
|
|
7052
7146
|
} catch (error) {
|
|
7053
7147
|
handleError(error, isJson);
|
|
7054
7148
|
}
|
|
7055
7149
|
});
|
|
7056
7150
|
}
|
|
7151
|
+
async function streamAgentTurn(body, isJson, keep) {
|
|
7152
|
+
let sandboxId;
|
|
7153
|
+
let opened = false;
|
|
7154
|
+
const session = await attachToSandbox({
|
|
7155
|
+
id: "",
|
|
7156
|
+
isJson,
|
|
7157
|
+
onDisconnect: keep ? "survives" : "released",
|
|
7158
|
+
retry: { attempts: 0, baseDelayMs: 0 },
|
|
7159
|
+
onSandbox: (sandbox) => {
|
|
7160
|
+
sandboxId = sandbox.sandboxId;
|
|
7161
|
+
},
|
|
7162
|
+
open: ({ signal }) => {
|
|
7163
|
+
if (opened) {
|
|
7164
|
+
throw new MutagentError("STREAM_DISCONNECTED", "The turn’s stream dropped and cannot be resumed.", "A one-shot run cannot be reconnected — the server released the sandbox when the connection dropped, and re-opening would run the whole turn again in a new box. Run it again, or use --no-stream.", 5);
|
|
7165
|
+
}
|
|
7166
|
+
opened = true;
|
|
7167
|
+
return openAgentRunStream(body, { signal });
|
|
7168
|
+
}
|
|
7169
|
+
});
|
|
7170
|
+
if (isJson) {
|
|
7171
|
+
console.log(JSON.stringify({
|
|
7172
|
+
success: true,
|
|
7173
|
+
prompt: body.prompt,
|
|
7174
|
+
task: body.task,
|
|
7175
|
+
sandboxId: session.result?.sandboxId ?? sandboxId ?? null,
|
|
7176
|
+
preset: session.result?.preset ?? session.sandbox?.preset ?? null,
|
|
7177
|
+
end: session.end,
|
|
7178
|
+
exitCode: session.exitCode,
|
|
7179
|
+
timedOut: session.result?.timedOut ?? false,
|
|
7180
|
+
tornDown: session.result?.tornDown ?? null,
|
|
7181
|
+
...session.result?.error === undefined ? {} : { error: session.result.error },
|
|
7182
|
+
lastSeq: session.lastSeq,
|
|
7183
|
+
gaps: session.gaps,
|
|
7184
|
+
...sandboxId === undefined ? {} : { _links: sandboxLinks(sandboxId) }
|
|
7185
|
+
}));
|
|
7186
|
+
}
|
|
7187
|
+
if (session.end === "exited" && session.exitCode === null) {
|
|
7188
|
+
process.exitCode = 1;
|
|
7189
|
+
}
|
|
7190
|
+
}
|
|
7057
7191
|
function registerExecCommand(parent) {
|
|
7058
7192
|
parent.command("exec").description("Run a command in a sandbox — an existing one, or a fresh one").argument("<argv...>", "Command and arguments (argv, not a shell string)").option("--sandbox <id>", "Run in this existing sandbox instead of a fresh one").option("--preset <name>", "Preset for the fresh sandbox (ignored with --sandbox)").option("--keep", "Keep the fresh sandbox afterwards (ignored with --sandbox)").option("--cwd <dir>", "Working directory inside the sandbox").option("--timeout-ms <n>", "Command timeout in milliseconds").option("--no-stream", "Wait for the full result instead of streaming it").addHelpText("after", `
|
|
7059
7193
|
Examples:
|
|
@@ -7675,5 +7809,5 @@ program.addCommand(createFeedbackCommand());
|
|
|
7675
7809
|
program.addCommand(createTraceCommand());
|
|
7676
7810
|
program.parse();
|
|
7677
7811
|
|
|
7678
|
-
//# debugId=
|
|
7812
|
+
//# debugId=84C2BB73F467526664756E2164756E21
|
|
7679
7813
|
//# sourceMappingURL=cli.js.map
|