@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 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 openStream(path, sandboxId, options, init = {}) {
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, sandboxId);
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.", `Retry with: mutagent helix attach ${sandboxId}`);
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
- const detached = () => ({ end: "detached", lastSeq, exitCode, reconnects, gaps });
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 { end: "exited", lastSeq, exitCode, reconnects, gaps };
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
- return new MutagentError("STREAM_DISCONNECTED", `Lost the sandbox stream and could not reconnect${detail}.`, `The sandbox is still running — nothing was destroyed. Resume with: mutagent helix attach <id> --since ${String(lastSeq)}`, 5);
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
- sink.notice(`Detached. Sandbox ${id} is still running — nothing was destroyed.`);
6768
- sink.notice(`Re-attach with: ${reattachHint(id, result.lastSeq)}`);
6769
- sink.notice(`Destroy it with: mutagent helix rm ${id} --force`);
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
- sink.notice(`Sandbox ${id} exited (no exit code reported).`);
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 ${id} exited with code ${String(result.exitCode)}.`);
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 is blocking and does not stream the server returns the whole result.
7026
- Its exit code becomes this command's exit code. See 'mutagent helix presets' for
7027
- what --preset accepts.
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, exitCode, stdout,
7031
- stderr, timedOut, tornDown }, _links }. No NDJSON here — the call is blocking.
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 result = await agentRun({
7135
+ const body = {
7047
7136
  prompt,
7048
7137
  task,
7049
7138
  ...oneShotFields(options.preset, options.keep, options.timeoutMs)
7050
- });
7051
- reportResult(result, output, isJson, { prompt, task });
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=8A72E382D57F3D0964756E2164756E21
7812
+ //# debugId=84C2BB73F467526664756E2164756E21
7679
7813
  //# sourceMappingURL=cli.js.map