@brainbase-labs/cli 0.34.0 → 0.36.0
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/README.md +54 -2
- package/dist/index.js +290 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,58 @@ brainbase benchmark list # list benchmarks for the linked agent
|
|
|
27
27
|
|
|
28
28
|
Run `brainbase help` to see every command.
|
|
29
29
|
|
|
30
|
+
## Task follow-ups
|
|
31
|
+
|
|
32
|
+
Send an instruction to an existing task, including while its agent is running:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
brainbase task send <task-id> "Also update the Linear ticket"
|
|
36
|
+
brainbase task send <task-id> "Use the revised approach instead" --json
|
|
37
|
+
brainbase task stop <task-id>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`send` prints the input ID and generation to stderr before submitting and reports delivery
|
|
41
|
+
status in stdout. Initial acceptance can say **delivery pending** while the
|
|
42
|
+
server determines whether native steering is available. Native steering follows
|
|
43
|
+
the harness's own timing. A confirmed fallback says **queued for the next turn**.
|
|
44
|
+
`submitted` means the harness has received the input; it does not mean the agent
|
|
45
|
+
has acted on it. Multiple follow-ups can join one execution and receive one
|
|
46
|
+
combined answer.
|
|
47
|
+
|
|
48
|
+
By default, both commands read the current input generation before sending their request.
|
|
49
|
+
The credential must have receipt-read access as well as permission to send or stop.
|
|
50
|
+
Resource-scoped keys can lack that read access when task-read enforcement is enabled.
|
|
51
|
+
If the read fails, no mutation is sent. Use `--generation <n>` to bypass the read only
|
|
52
|
+
with a trusted current cutoff for a new operation, or the original cutoff for a retry.
|
|
53
|
+
A concurrent Stop prevents a send from joining newer work with an older generation.
|
|
54
|
+
If the HTTP response is lost or you press Ctrl-C, keep the input ID and generation,
|
|
55
|
+
and repeat the exact message with `--input-id <uuid> --generation <n>` to recover that request safely. Reusing an ID
|
|
56
|
+
does not replay an existing input. A **delivery unconfirmed** receipt means
|
|
57
|
+
the agent may already have received the instruction; deliberately resend
|
|
58
|
+
without `--input-id` only if you want to retry it as a new input.
|
|
59
|
+
|
|
60
|
+
Known pre-admission HTTP 503 refusals include `task_inputs_disabled`,
|
|
61
|
+
`task_input_recovery_unavailable`, and recognized sandbox provisioning or access
|
|
62
|
+
codes. These mean no input was admitted by that request. Retry the same ID, message
|
|
63
|
+
and generation after the service recovers. Unrecognized server or network failures
|
|
64
|
+
can leave delivery unconfirmed. A recovered receipt reports `idempotency_replayed: true`
|
|
65
|
+
in JSON when the server supplies its replay header.
|
|
66
|
+
|
|
67
|
+
`stop` stops execution and cancels pending follow-ups while retaining their
|
|
68
|
+
text. A new instruction will not replay them. Before submitting, it prints a retry
|
|
69
|
+
command with `--generation <n>`. Keep that command even if the response is lost:
|
|
70
|
+
retrying the original generation cannot interrupt newer work. An error can mean
|
|
71
|
+
queued inputs were already cancelled while the original execution is still running.
|
|
72
|
+
Follow the error's retry or operator-recovery guidance. Omit `--generation` only
|
|
73
|
+
when deliberately issuing a new Stop. JSON includes the original `expected_generation`
|
|
74
|
+
separately from the server's resulting `generation`.
|
|
75
|
+
|
|
76
|
+
Ctrl-C in `task create --wait`
|
|
77
|
+
or `task logs --follow` still only detaches; it does not stop remote work.
|
|
78
|
+
Both commands support `--json`. `send` exits 1 for unconfirmed, cancelled or
|
|
79
|
+
failed inputs, and for request errors. Other acknowledged states exit 0;
|
|
80
|
+
this confirms admission or delivery, not task success.
|
|
81
|
+
|
|
30
82
|
## Benchmark management
|
|
31
83
|
|
|
32
84
|
`benchmark init`, `benchmark validate`, and command help are local and need no
|
|
@@ -237,7 +289,7 @@ Playbook files carry a small YAML frontmatter (`title`, `description`) — the C
|
|
|
237
289
|
|
|
238
290
|
| Variable | Purpose |
|
|
239
291
|
|-|-|
|
|
240
|
-
| `BRAINBASE_CONTROL_PLANE_URL` | Override the MAS host. Agent/orchestration requests use `/v2/cli`;
|
|
292
|
+
| `BRAINBASE_CONTROL_PLANE_URL` | Override the MAS host. Agent/orchestration requests use `/v2/cli`; task commands use `/v2/tasks`. The default is `https://api.brainbaselabs.com`. |
|
|
241
293
|
| `BRAINBASE_API_URL` | Legacy KLS host override. Control requests use `/api/cli`; it is also the fallback for model-proxy and registry traffic. |
|
|
242
294
|
| `BRAINBASE_PROXY_URL` | Override the model-proxy host used when enabling harness tracking. |
|
|
243
295
|
| `BRAINBASE_REGISTRY_URL` | Override the registry API host. |
|
|
@@ -256,7 +308,7 @@ it talks to:
|
|
|
256
308
|
|-|-|
|
|
257
309
|
| `agent`, `orchestration`, `link`, `unlink`, `sync`, `status`, `team` | `BRAINBASE_TOKEN`, then the `auth.json` session |
|
|
258
310
|
| `template`, `skill`, `token` | `BRAINBASE_TOKEN`, then the session, then `token.json` |
|
|
259
|
-
| `task
|
|
311
|
+
| `task` commands | `BRAINBASE_TOKEN`, then the session, then `token.json` — the last only when no session is configured |
|
|
260
312
|
| `benchmark` control-plane commands | `BRAINBASE_TOKEN`, then the session |
|
|
261
313
|
|
|
262
314
|
`brainbase whoami` (or `--json`) reports which one is active.
|
package/dist/index.js
CHANGED
|
@@ -36018,7 +36018,7 @@ function padStart(s, n) {
|
|
|
36018
36018
|
// package.json
|
|
36019
36019
|
var package_default = {
|
|
36020
36020
|
name: "@brainbase-labs/cli",
|
|
36021
|
-
version: "0.
|
|
36021
|
+
version: "0.36.0",
|
|
36022
36022
|
description: "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
|
|
36023
36023
|
type: "module",
|
|
36024
36024
|
bin: {
|
|
@@ -54605,7 +54605,7 @@ async function masReadRequest(pathname, callerSignal) {
|
|
|
54605
54605
|
} catch {}
|
|
54606
54606
|
if (!res.ok) {
|
|
54607
54607
|
const message = masApiErrorMessage(body, res.status);
|
|
54608
|
-
throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : message, res.status, body);
|
|
54608
|
+
throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
|
|
54609
54609
|
}
|
|
54610
54610
|
return body;
|
|
54611
54611
|
}
|
|
@@ -54623,7 +54623,7 @@ function withRequestDeadline(callerSignal, timeoutMs) {
|
|
|
54623
54623
|
});
|
|
54624
54624
|
return combined.signal;
|
|
54625
54625
|
}
|
|
54626
|
-
async function masResourceRequest(pathname, init = {}) {
|
|
54626
|
+
async function masResourceRequest(pathname, init = {}, onHeaders) {
|
|
54627
54627
|
const credential = await resolveMasCredential();
|
|
54628
54628
|
let currentSession = credential.session;
|
|
54629
54629
|
const res = await sendWithAuthRetry(credential.source === "session" ? currentSession : null, async (refreshed) => {
|
|
@@ -54648,6 +54648,7 @@ async function masResourceRequest(pathname, init = {}) {
|
|
|
54648
54648
|
const message = masApiErrorMessage(body, res.status);
|
|
54649
54649
|
throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
|
|
54650
54650
|
}
|
|
54651
|
+
onHeaders?.(res.headers);
|
|
54651
54652
|
return body;
|
|
54652
54653
|
}
|
|
54653
54654
|
|
|
@@ -54687,6 +54688,12 @@ function masItems(body, what) {
|
|
|
54687
54688
|
}
|
|
54688
54689
|
throw new ApiError(`MAS returned an unreadable ${what} response.`, undefined, body);
|
|
54689
54690
|
}
|
|
54691
|
+
function isTaskInputReceipt(body) {
|
|
54692
|
+
if (!body || typeof body !== "object")
|
|
54693
|
+
return false;
|
|
54694
|
+
const receipt = body;
|
|
54695
|
+
return typeof receipt.id === "string" && typeof receipt.task_id === "string" && Array.isArray(receipt.message_ids) && receipt.message_ids.every((id) => typeof id === "string") && typeof receipt.status === "string" && ["queued", "dispatching", "submitted", "unconfirmed", "cancelled", "completed", "failed"].includes(receipt.status) && (receipt.turn_id === null || typeof receipt.turn_id === "string") && Number.isInteger(receipt.generation) && Number.isInteger(receipt.sequence) && (receipt.reason === undefined || receipt.reason === null || typeof receipt.reason === "string") && typeof receipt.created_at === "string" && typeof receipt.updated_at === "string";
|
|
54696
|
+
}
|
|
54690
54697
|
var masApi = {
|
|
54691
54698
|
async createTask(input, options) {
|
|
54692
54699
|
const body = await masRequest("/tasks", {
|
|
@@ -54696,6 +54703,30 @@ var masApi = {
|
|
|
54696
54703
|
});
|
|
54697
54704
|
return parseMasTaskCreateResponse(body);
|
|
54698
54705
|
},
|
|
54706
|
+
async getTaskInputGeneration(taskId) {
|
|
54707
|
+
const body = await masReadRequest(`/tasks/${encodeURIComponent(taskId)}/inputs?limit=1&include_messages=false`);
|
|
54708
|
+
if (!body || !Array.isArray(body.inputs) || typeof body.generation !== "number" || !Number.isSafeInteger(body.generation) || body.generation < 0) {
|
|
54709
|
+
throw new ApiError("MAS returned an unreadable task input generation.", undefined, body);
|
|
54710
|
+
}
|
|
54711
|
+
return body.generation;
|
|
54712
|
+
},
|
|
54713
|
+
async sendTaskInput(taskId, input) {
|
|
54714
|
+
let replayed = false;
|
|
54715
|
+
const body = await masResourceRequest(`/tasks/${encodeURIComponent(taskId)}/inputs`, { method: "POST", body: JSON.stringify(input), keepalive: false }, (headers) => {
|
|
54716
|
+
replayed = headers.get("Idempotency-Replayed") === "true";
|
|
54717
|
+
});
|
|
54718
|
+
if (!body || !isTaskInputReceipt(body.input) || body.input.id.toLowerCase() !== input.input_id.toLowerCase() || body.input.task_id.toLowerCase() !== taskId.toLowerCase() || !Array.isArray(body.messages) || typeof body.run_started !== "boolean" || !(body.accepted_turn_id === null || typeof body.accepted_turn_id === "string")) {
|
|
54719
|
+
throw new ApiError("MAS returned an unreadable follow-up receipt.", undefined, body);
|
|
54720
|
+
}
|
|
54721
|
+
return replayed ? { ...body, idempotency_replayed: true } : body;
|
|
54722
|
+
},
|
|
54723
|
+
async stopTask(taskId, expectedGeneration) {
|
|
54724
|
+
const body = await masResourceRequest(`/tasks/${encodeURIComponent(taskId)}/interrupt`, { method: "POST", body: JSON.stringify({ expected_generation: expectedGeneration }), keepalive: false });
|
|
54725
|
+
if (!body || typeof body.id !== "string" || body.id.toLowerCase() !== taskId.toLowerCase() || typeof body.status !== "string" || !Number.isInteger(body.generation) || !Array.isArray(body.cancelled_inputs) || !body.cancelled_inputs.every((input) => isTaskInputReceipt(input) && input.task_id.toLowerCase() === taskId.toLowerCase() && input.status === "cancelled")) {
|
|
54726
|
+
throw new ApiError("MAS did not confirm Stop and pending follow-up cancellation.", undefined, body);
|
|
54727
|
+
}
|
|
54728
|
+
return body;
|
|
54729
|
+
},
|
|
54699
54730
|
async listTasks(options = {}) {
|
|
54700
54731
|
const params = new URLSearchParams;
|
|
54701
54732
|
if (options.agentId)
|
|
@@ -81143,6 +81174,177 @@ function oneLine2(value) {
|
|
|
81143
81174
|
return flattened.length > 300 ? `${flattened.slice(0, 299)}…` : flattened;
|
|
81144
81175
|
}
|
|
81145
81176
|
|
|
81177
|
+
// src/cli/task-input.ts
|
|
81178
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
81179
|
+
function errorCode(error2) {
|
|
81180
|
+
if (!(error2 instanceof ApiError) || !error2.body || typeof error2.body !== "object")
|
|
81181
|
+
return;
|
|
81182
|
+
const detail = error2.body.detail;
|
|
81183
|
+
if (!detail || typeof detail !== "object")
|
|
81184
|
+
return;
|
|
81185
|
+
const code = detail.code;
|
|
81186
|
+
return typeof code === "string" ? code : undefined;
|
|
81187
|
+
}
|
|
81188
|
+
function stopRetryCommand(taskId, generation) {
|
|
81189
|
+
const argument = /^[A-Za-z0-9_-]+$/.test(taskId) ? taskId : `'${sanitizeLabel(taskId).replaceAll("'", "'\\''")}'`;
|
|
81190
|
+
return `brainbase task stop ${argument} --generation ${generation}`;
|
|
81191
|
+
}
|
|
81192
|
+
var NO_ADMISSION_CODES = new Set([
|
|
81193
|
+
"task_inputs_disabled",
|
|
81194
|
+
"task_input_recovery_unavailable",
|
|
81195
|
+
"aws_microvm_unavailable",
|
|
81196
|
+
"modal_sandboxes_unavailable",
|
|
81197
|
+
"modal_sandbox_authorization_unavailable",
|
|
81198
|
+
"bbsandbox_unavailable",
|
|
81199
|
+
"bbsandbox_authorization_unavailable",
|
|
81200
|
+
"cloudflare_unavailable",
|
|
81201
|
+
"cloudflare_authorization_unavailable"
|
|
81202
|
+
]);
|
|
81203
|
+
var STOP_RECOVERY_CODES = new Set([
|
|
81204
|
+
"turn_interrupt_unsupported",
|
|
81205
|
+
"turn_interrupt_recovery_required",
|
|
81206
|
+
"task_recovery_required"
|
|
81207
|
+
]);
|
|
81208
|
+
var INPUT_STATUS_TEXT = {
|
|
81209
|
+
queued: "Accepted; delivery pending.",
|
|
81210
|
+
dispatching: "Delivery in progress; the harness has not acknowledged it yet.",
|
|
81211
|
+
submitted: "Received by the harness; this does not confirm the instruction has been acted on.",
|
|
81212
|
+
unconfirmed: "Delivery unconfirmed. The agent may have received this message. Send it again only if you intend to retry; omit --input-id to create a new follow-up.",
|
|
81213
|
+
cancelled: "Cancelled. The text is retained and will not be replayed by a new instruction.",
|
|
81214
|
+
completed: "The execution containing this follow-up has completed.",
|
|
81215
|
+
failed: "Follow-up failed. Its text is retained."
|
|
81216
|
+
};
|
|
81217
|
+
var NEXT_TURN_REASONS = new Set([
|
|
81218
|
+
"unsupported",
|
|
81219
|
+
"closing",
|
|
81220
|
+
"stale",
|
|
81221
|
+
"native_rejected",
|
|
81222
|
+
"native_fallback"
|
|
81223
|
+
]);
|
|
81224
|
+
async function runTaskSend(taskId, message, options = {}) {
|
|
81225
|
+
if (!message.trim())
|
|
81226
|
+
throw new Error("The follow-up message must not be blank.");
|
|
81227
|
+
const inputId = options.inputId?.trim().toLowerCase() ?? randomUUID3();
|
|
81228
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(inputId)) {
|
|
81229
|
+
throw new Error("--input-id must be a UUID.");
|
|
81230
|
+
}
|
|
81231
|
+
console.error(`Input ID: ${inputId}`);
|
|
81232
|
+
let result2;
|
|
81233
|
+
let expectedGeneration = options.generation;
|
|
81234
|
+
let attempted = false;
|
|
81235
|
+
try {
|
|
81236
|
+
expectedGeneration ??= await masApi.getTaskInputGeneration(taskId);
|
|
81237
|
+
console.error([
|
|
81238
|
+
`Input generation: ${expectedGeneration}`,
|
|
81239
|
+
`If interrupted, rerun with --input-id ${inputId} and the identical message, plus --generation ${expectedGeneration}, to recover this request.`
|
|
81240
|
+
].join(`
|
|
81241
|
+
`));
|
|
81242
|
+
attempted = true;
|
|
81243
|
+
result2 = await masApi.sendTaskInput(taskId, {
|
|
81244
|
+
input_id: inputId,
|
|
81245
|
+
messages: [{ role: "user", content: message }],
|
|
81246
|
+
expected_generation: expectedGeneration
|
|
81247
|
+
});
|
|
81248
|
+
} catch (error2) {
|
|
81249
|
+
const status = error2 instanceof ApiError ? error2.status : undefined;
|
|
81250
|
+
const code = errorCode(error2);
|
|
81251
|
+
const rejected = status === 503 && NO_ADMISSION_CODES.has(code ?? "");
|
|
81252
|
+
const uncertain = attempted && !rejected && (status === undefined || status >= 500);
|
|
81253
|
+
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
81254
|
+
const retry = `rerun with --input-id ${inputId} and the identical message${expectedGeneration === undefined ? "" : `, plus --generation ${expectedGeneration}`}`;
|
|
81255
|
+
const recovery = uncertain ? ` Delivery is unconfirmed. To recover the same request safely, ${retry}.` : rejected ? ` No input was admitted by this request. After service recovery, ${retry}.` : !attempted ? ` No input was sent: the task generation could not be read. After resolving the error, ${retry}. Use --generation <n> only with a trusted current cutoff, or the original cutoff for a retry.` : "";
|
|
81256
|
+
if (options.json) {
|
|
81257
|
+
console.log(JSON.stringify({
|
|
81258
|
+
task_id: taskId,
|
|
81259
|
+
input_id: inputId,
|
|
81260
|
+
expected_generation: expectedGeneration,
|
|
81261
|
+
error: { message: `${detail}${recovery}`, status, code },
|
|
81262
|
+
...uncertain ? { delivery: "unconfirmed" } : {}
|
|
81263
|
+
}));
|
|
81264
|
+
process.exitCode = 1;
|
|
81265
|
+
return;
|
|
81266
|
+
}
|
|
81267
|
+
throw new ApiError(`Follow-up ${inputId}: ${detail}${recovery}`, status, error2 instanceof ApiError ? error2.body : undefined);
|
|
81268
|
+
}
|
|
81269
|
+
if (options.json) {
|
|
81270
|
+
console.log(JSON.stringify({ ...result2, expected_generation: expectedGeneration }, null, 2));
|
|
81271
|
+
} else {
|
|
81272
|
+
const receipt = result2.input;
|
|
81273
|
+
const lines = [
|
|
81274
|
+
`Task ID: ${sanitizeLabel(taskId)}`,
|
|
81275
|
+
`Input ID: ${sanitizeLabel(receipt.id)}`,
|
|
81276
|
+
`Status: ${receipt.status}`,
|
|
81277
|
+
receipt.status === "queued" && NEXT_TURN_REASONS.has(receipt.reason ?? "") ? "Queued for the next turn." : INPUT_STATUS_TEXT[receipt.status]
|
|
81278
|
+
];
|
|
81279
|
+
if (result2.idempotency_replayed)
|
|
81280
|
+
lines.push("Recovered an existing receipt; no new delivery was requested.");
|
|
81281
|
+
if (receipt.turn_id)
|
|
81282
|
+
lines.push(`Turn ID: ${sanitizeLabel(receipt.turn_id)}`);
|
|
81283
|
+
if (receipt.reason)
|
|
81284
|
+
lines.push(`Reason: ${sanitizeLabel(receipt.reason)}`);
|
|
81285
|
+
console.log(lines.join(`
|
|
81286
|
+
`));
|
|
81287
|
+
}
|
|
81288
|
+
if (["unconfirmed", "cancelled", "failed"].includes(result2.input.status)) {
|
|
81289
|
+
process.exitCode = 1;
|
|
81290
|
+
}
|
|
81291
|
+
}
|
|
81292
|
+
async function runTaskStop(taskId, options = {}) {
|
|
81293
|
+
let task;
|
|
81294
|
+
let expectedGeneration = options.generation;
|
|
81295
|
+
let attempted = false;
|
|
81296
|
+
try {
|
|
81297
|
+
expectedGeneration ??= await masApi.getTaskInputGeneration(taskId);
|
|
81298
|
+
console.error([
|
|
81299
|
+
`Stop generation: ${expectedGeneration}`,
|
|
81300
|
+
"If interrupted, retry the original Stop with:",
|
|
81301
|
+
stopRetryCommand(taskId, expectedGeneration)
|
|
81302
|
+
].join(`
|
|
81303
|
+
`));
|
|
81304
|
+
attempted = true;
|
|
81305
|
+
task = await masApi.stopTask(taskId, expectedGeneration);
|
|
81306
|
+
} catch (error2) {
|
|
81307
|
+
const status = error2 instanceof ApiError ? error2.status : undefined;
|
|
81308
|
+
const code = errorCode(error2);
|
|
81309
|
+
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
81310
|
+
let guidance = "";
|
|
81311
|
+
if (!attempted) {
|
|
81312
|
+
guidance = " Stop was not sent: the task generation could not be read. Use --generation <n> only with a trusted current cutoff, or the original cutoff for a retry.";
|
|
81313
|
+
} else if (status === undefined || status >= 500 || code === "turn_interrupt_pending" || STOP_RECOVERY_CODES.has(code ?? "")) {
|
|
81314
|
+
guidance = " Stop is not confirmed. Pending follow-ups may already be cancelled; their text is retained. The original execution may still be running.";
|
|
81315
|
+
const retry = stopRetryCommand(taskId, expectedGeneration);
|
|
81316
|
+
guidance += code === "turn_interrupt_unsupported" ? " If the original execution finishes, try the original Stop again. Contact support if it still cannot be confirmed. Retry with:" : STOP_RECOVERY_CODES.has(code ?? "") ? " Wait for operator recovery. Retain this command for any later retry:" : ` ${code === "turn_interrupt_pending" || code === "turn_interrupt_unconfirmed" ? "Wait at least one second, then retry" : "Check the task, then retry"} the original Stop with:`;
|
|
81317
|
+
guidance += `
|
|
81318
|
+
${retry}
|
|
81319
|
+
Omit --generation only for a deliberately new Stop.`;
|
|
81320
|
+
}
|
|
81321
|
+
const message = `${detail}${guidance}`;
|
|
81322
|
+
if (options.json) {
|
|
81323
|
+
console.log(JSON.stringify({
|
|
81324
|
+
task_id: taskId,
|
|
81325
|
+
expected_generation: expectedGeneration,
|
|
81326
|
+
error: { message, status, code }
|
|
81327
|
+
}));
|
|
81328
|
+
process.exitCode = 1;
|
|
81329
|
+
return;
|
|
81330
|
+
}
|
|
81331
|
+
throw new ApiError(message, status, error2 instanceof ApiError ? error2.body : undefined);
|
|
81332
|
+
}
|
|
81333
|
+
if (options.json) {
|
|
81334
|
+
console.log(JSON.stringify({ ...task, expected_generation: expectedGeneration }, null, 2));
|
|
81335
|
+
return;
|
|
81336
|
+
}
|
|
81337
|
+
console.log([
|
|
81338
|
+
`Task ID: ${sanitizeLabel(task.id)}`,
|
|
81339
|
+
`Status: ${sanitizeLabel(task.status)}`,
|
|
81340
|
+
`Stop generation: ${expectedGeneration}`,
|
|
81341
|
+
`Cancelled follow-ups: ${task.cancelled_inputs.length}`,
|
|
81342
|
+
...task.cancelled_inputs.map((input) => `Cancelled input: ${sanitizeLabel(input.id)}`),
|
|
81343
|
+
"Follow-ups covered by this Stop are cancelled. Their text is retained; a new instruction will not replay them."
|
|
81344
|
+
].join(`
|
|
81345
|
+
`));
|
|
81346
|
+
}
|
|
81347
|
+
|
|
81146
81348
|
// src/core/argv.ts
|
|
81147
81349
|
class ArgvParseError extends Error {
|
|
81148
81350
|
code = "invalid_arguments";
|
|
@@ -81399,6 +81601,15 @@ function requiredTaskId(positionals, usage) {
|
|
|
81399
81601
|
}
|
|
81400
81602
|
return positionals[0].trim();
|
|
81401
81603
|
}
|
|
81604
|
+
function inputGeneration(value) {
|
|
81605
|
+
if (value === undefined)
|
|
81606
|
+
return;
|
|
81607
|
+
const parsed = Number(value.trim());
|
|
81608
|
+
if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(parsed)) {
|
|
81609
|
+
throw new Error("--generation must be a non-negative safe whole number.");
|
|
81610
|
+
}
|
|
81611
|
+
return parsed;
|
|
81612
|
+
}
|
|
81402
81613
|
async function runTask(cwd2, sub, args) {
|
|
81403
81614
|
switch (sub) {
|
|
81404
81615
|
case "create": {
|
|
@@ -81430,6 +81641,44 @@ async function runTask(cwd2, sub, args) {
|
|
|
81430
81641
|
});
|
|
81431
81642
|
return;
|
|
81432
81643
|
}
|
|
81644
|
+
case "send": {
|
|
81645
|
+
const parsed = parseArgv(args, [
|
|
81646
|
+
{ key: "inputId", names: ["--input-id"], kind: "value" },
|
|
81647
|
+
{ key: "generation", names: ["--generation"], kind: "value" },
|
|
81648
|
+
{ key: "json", names: ["--json"], kind: "boolean" },
|
|
81649
|
+
{ key: "help", names: ["--help", "-h"], kind: "boolean" }
|
|
81650
|
+
]);
|
|
81651
|
+
if (booleanOption(parsed, "help")) {
|
|
81652
|
+
printHelp4();
|
|
81653
|
+
return;
|
|
81654
|
+
}
|
|
81655
|
+
if (parsed.positionals.length !== 2 || !parsed.positionals[0]?.trim()) {
|
|
81656
|
+
throw new Error("Usage: brainbase task send <task-id> <message> [--input-id <uuid>] [--generation <n>] [--json]");
|
|
81657
|
+
}
|
|
81658
|
+
await runTaskSend(parsed.positionals[0].trim(), parsed.positionals[1], {
|
|
81659
|
+
inputId: stringOption(parsed, "inputId"),
|
|
81660
|
+
generation: inputGeneration(stringOption(parsed, "generation")),
|
|
81661
|
+
json: booleanOption(parsed, "json")
|
|
81662
|
+
});
|
|
81663
|
+
return;
|
|
81664
|
+
}
|
|
81665
|
+
case "stop": {
|
|
81666
|
+
const parsed = parseArgv(args, [
|
|
81667
|
+
{ key: "generation", names: ["--generation"], kind: "value" },
|
|
81668
|
+
{ key: "json", names: ["--json"], kind: "boolean" },
|
|
81669
|
+
{ key: "help", names: ["--help", "-h"], kind: "boolean" }
|
|
81670
|
+
]);
|
|
81671
|
+
if (booleanOption(parsed, "help")) {
|
|
81672
|
+
printHelp4();
|
|
81673
|
+
return;
|
|
81674
|
+
}
|
|
81675
|
+
const taskId = requiredTaskId(parsed.positionals, "brainbase task stop <task-id> [--generation <n>] [--json]");
|
|
81676
|
+
await runTaskStop(taskId, {
|
|
81677
|
+
generation: inputGeneration(stringOption(parsed, "generation")),
|
|
81678
|
+
json: booleanOption(parsed, "json")
|
|
81679
|
+
});
|
|
81680
|
+
return;
|
|
81681
|
+
}
|
|
81433
81682
|
case "get": {
|
|
81434
81683
|
if (args.some((arg) => HELP_FLAGS.has(arg))) {
|
|
81435
81684
|
printHelp4();
|
|
@@ -81482,6 +81731,8 @@ function printHelp4() {
|
|
|
81482
81731
|
out.push(` ${import_picocolors56.default.cyan("list")} ${import_picocolors56.default.dim("recent tasks you can reach (--json for scripts)")}`);
|
|
81483
81732
|
out.push(` ${import_picocolors56.default.cyan("get")} ${import_picocolors56.default.dim("<task-id>")} ${import_picocolors56.default.dim("one task: status, agent, machine, eval verdicts")}`);
|
|
81484
81733
|
out.push(` ${import_picocolors56.default.cyan("logs")} ${import_picocolors56.default.dim("<task-id> [--follow]")} ${import_picocolors56.default.dim("one line per event in the task's transcript, or tail it live")}`);
|
|
81734
|
+
out.push(` ${import_picocolors56.default.cyan("send")} ${import_picocolors56.default.dim("<task-id> <message>")} ${import_picocolors56.default.dim("send a follow-up, including while the agent is running")}`);
|
|
81735
|
+
out.push(` ${import_picocolors56.default.cyan("stop")} ${import_picocolors56.default.dim("<task-id>")} ${import_picocolors56.default.dim("stop execution and cancel pending follow-ups, retaining their text")}`);
|
|
81485
81736
|
out.push("");
|
|
81486
81737
|
out.push(` ${import_picocolors56.default.bold("create flags")}`);
|
|
81487
81738
|
out.push(` ${import_picocolors56.default.dim("--message <text>")} required first user message`);
|
|
@@ -81499,12 +81750,24 @@ function printHelp4() {
|
|
|
81499
81750
|
out.push(` ${import_picocolors56.default.dim("--limit <n>")} stop after <n> events instead of the whole transcript; with --follow, the first page size`);
|
|
81500
81751
|
out.push(` ${import_picocolors56.default.dim("--follow, -f")} stay attached and print events as they land, until the task finishes`);
|
|
81501
81752
|
out.push("");
|
|
81753
|
+
out.push(` ${import_picocolors56.default.bold("send flags")}`);
|
|
81754
|
+
out.push(` ${import_picocolors56.default.dim("--input-id <uuid>")} recover the same request using its original ID and identical message`);
|
|
81755
|
+
out.push(` ${import_picocolors56.default.dim(" omit it to send a new follow-up; an unconfirmed receipt is never automatically replayed")}`);
|
|
81756
|
+
out.push(` ${import_picocolors56.default.dim(" admission disabled/recovery unavailable means no input was admitted; retry the same ID after recovery")}`);
|
|
81757
|
+
out.push("");
|
|
81758
|
+
out.push(` ${import_picocolors56.default.bold("send and stop flags")}`);
|
|
81759
|
+
out.push(` ${import_picocolors56.default.dim("--generation <n>")} retain the printed generation when retrying; defaults to a fresh snapshot for a new operation`);
|
|
81760
|
+
out.push(` ${import_picocolors56.default.dim(" Stop errors may follow queued-input cancellation; retry the printed command to avoid stopping newer work")}`);
|
|
81761
|
+
out.push("");
|
|
81502
81762
|
out.push(` ${import_picocolors56.default.bold("every subcommand")}`);
|
|
81503
|
-
out.push(` ${import_picocolors56.default.dim("--json")} machine-readable output — create prints task_id, agent_id and status; list, get and
|
|
81763
|
+
out.push(` ${import_picocolors56.default.dim("--json")} machine-readable output — create prints task_id, agent_id and status; list, get, logs, send and stop print the raw records`);
|
|
81764
|
+
out.push(` ${import_picocolors56.default.dim(" send and stop include expected_generation for safe retries; send also reports idempotency_replayed when present")}`);
|
|
81504
81765
|
out.push(` ${import_picocolors56.default.dim(" logs --follow --json prints one record per line, since a live stream has no end to close on")}`);
|
|
81505
81766
|
out.push("");
|
|
81506
81767
|
out.push(` ${import_picocolors56.default.dim("Exit codes with --wait:")} ${import_picocolors56.default.dim("0 succeeded · 1 failed, needs input, or timed out · 130 interrupted")}`);
|
|
81507
81768
|
out.push(` ${import_picocolors56.default.dim("Exit codes with --follow:")} ${import_picocolors56.default.dim("0 task reached a terminal state · 1 stream refused, or unreachable for 60s · 130 interrupted (Ctrl-C) · 143 terminated")}`);
|
|
81769
|
+
out.push(` ${import_picocolors56.default.dim("Exit codes with send:")} ${import_picocolors56.default.dim("0 queued, dispatching, submitted or completed · 1 unconfirmed, cancelled, failed or request error")}`);
|
|
81770
|
+
out.push(` ${import_picocolors56.default.dim("Ctrl-C while waiting or following logs detaches; use task stop to cancel remote work.")}`);
|
|
81508
81771
|
out.push("");
|
|
81509
81772
|
out.push(` ${import_picocolors56.default.dim("Flag-like values:")} use ${import_picocolors56.default.cyan("--flag=value")} or ${import_picocolors56.default.cyan("--flag -- <value>")}`);
|
|
81510
81773
|
out.push("");
|
|
@@ -85418,9 +85681,12 @@ class BenchmarkApiClient {
|
|
|
85418
85681
|
query: { baseline_target_id: baselineTargetId }
|
|
85419
85682
|
});
|
|
85420
85683
|
}
|
|
85421
|
-
exportRunResults(runId) {
|
|
85684
|
+
exportRunResults(runId, query = {}) {
|
|
85422
85685
|
return this.request(`/runs/${encodeURIComponent(runId)}/results/export`, {
|
|
85423
|
-
mode: "binary"
|
|
85686
|
+
mode: "binary",
|
|
85687
|
+
query: {
|
|
85688
|
+
include_billing_receipts: query.includeBillingReceipts || undefined
|
|
85689
|
+
}
|
|
85424
85690
|
});
|
|
85425
85691
|
}
|
|
85426
85692
|
listTrials(runId, query = {}) {
|
|
@@ -87134,6 +87400,11 @@ var DEFINITIONS = [
|
|
|
87134
87400
|
{ key: "yes", names: ["--yes", "-y"], kind: "boolean" },
|
|
87135
87401
|
{ key: "all", names: ["--all"], kind: "boolean" },
|
|
87136
87402
|
{ key: "force", names: ["--force"], kind: "boolean" },
|
|
87403
|
+
{
|
|
87404
|
+
key: "includeBillingReceipts",
|
|
87405
|
+
names: ["--include-billing-receipts"],
|
|
87406
|
+
kind: "boolean"
|
|
87407
|
+
},
|
|
87137
87408
|
{ key: "archived", names: ["--archived"], kind: "boolean" },
|
|
87138
87409
|
{ key: "preview", names: ["--preview"], kind: "boolean" },
|
|
87139
87410
|
{ key: "watch", names: ["--watch"], kind: "boolean" },
|
|
@@ -88696,11 +88967,17 @@ async function runArtifacts(cwd2, argv, deps) {
|
|
|
88696
88967
|
}, options);
|
|
88697
88968
|
}
|
|
88698
88969
|
async function runExportResults(cwd2, argv, deps) {
|
|
88699
|
-
const options = parseControlOptions(argv, [
|
|
88700
|
-
|
|
88970
|
+
const options = parseControlOptions(argv, [
|
|
88971
|
+
"output",
|
|
88972
|
+
"force",
|
|
88973
|
+
"includeBillingReceipts"
|
|
88974
|
+
]);
|
|
88975
|
+
expectPositionals(options.positionals, 1, 1, "brainbase benchmark export-results <run-id> [--output <path>] [--include-billing-receipts]");
|
|
88701
88976
|
const runId = options.positionals[0];
|
|
88702
88977
|
const { client, output } = context(cwd2, "benchmark export-results", options, deps);
|
|
88703
|
-
const response = await client.exportRunResults(runId
|
|
88978
|
+
const response = await client.exportRunResults(runId, {
|
|
88979
|
+
includeBillingReceipts: bool(options.parsed, "includeBillingReceipts")
|
|
88980
|
+
});
|
|
88704
88981
|
const fallback = safeExportFilename(response.filename, `benchmark-run-${runId}-results.zip`);
|
|
88705
88982
|
const written = writeBinaryOutput(cwd2, optionalNonblank(options.parsed, "output"), response.bytes, fallback, bool(options.parsed, "force"));
|
|
88706
88983
|
output.result({
|
|
@@ -88913,7 +89190,7 @@ function commandLevelHelp(sub, argv) {
|
|
|
88913
89190
|
case "artifacts":
|
|
88914
89191
|
return usage("brainbase benchmark artifacts <run-id> <attempt-id> [--limit <n>] [--cursor <cursor>] [--all]");
|
|
88915
89192
|
case "export-results":
|
|
88916
|
-
return usage("brainbase benchmark export-results <run-id> [--output <path>] [--force]");
|
|
89193
|
+
return usage("brainbase benchmark export-results <run-id> [--output <path>] [--force] [--include-billing-receipts]");
|
|
88917
89194
|
case "history":
|
|
88918
89195
|
return usage("brainbase benchmark history <benchmark-id> [--limit <n>]");
|
|
88919
89196
|
case "baseline":
|
|
@@ -89463,6 +89740,8 @@ function help() {
|
|
|
89463
89740
|
out.push(` ${import_picocolors61.default.cyan("task list")} ${import_picocolors61.default.dim("recent tasks you can reach")}`);
|
|
89464
89741
|
out.push(` ${import_picocolors61.default.cyan("task get")} ${import_picocolors61.default.dim("<task-id>")} ${import_picocolors61.default.dim("one task: status, agent, machine, eval verdicts")}`);
|
|
89465
89742
|
out.push(` ${import_picocolors61.default.cyan("task logs")} ${import_picocolors61.default.dim("<task-id> [--follow]")} ${import_picocolors61.default.dim("one line per event in the task's transcript, or tail it live")}`);
|
|
89743
|
+
out.push(` ${import_picocolors61.default.cyan("task send")} ${import_picocolors61.default.dim("<task-id> <message>")} ${import_picocolors61.default.dim("send a follow-up during execution, or queue it for the next turn")}`);
|
|
89744
|
+
out.push(` ${import_picocolors61.default.cyan("task stop")} ${import_picocolors61.default.dim("<task-id>")} ${import_picocolors61.default.dim("stop execution and cancel pending follow-ups, retaining their text")}`);
|
|
89466
89745
|
out.push("");
|
|
89467
89746
|
out.push(divider("SANDBOXES"));
|
|
89468
89747
|
out.push("");
|
|
@@ -89556,7 +89835,7 @@ function help() {
|
|
|
89556
89835
|
out.push(` ${import_picocolors61.default.dim("BRAINBASE_HOME")} override the local config dir (default ~/.brainbase)`);
|
|
89557
89836
|
out.push(` ${import_picocolors61.default.dim("BRAINBASE_DEBUG")} print full stack traces on error (any value; unset to disable)`);
|
|
89558
89837
|
out.push(` ${import_picocolors61.default.dim("BRAINBASE_WEB_URL")} override the web app URL used by login`);
|
|
89559
|
-
out.push(` ${import_picocolors61.default.dim("BRAINBASE_CONTROL_PLANE_URL")} override the MAS host (/v2/cli; task
|
|
89838
|
+
out.push(` ${import_picocolors61.default.dim("BRAINBASE_CONTROL_PLANE_URL")} override the MAS host (/v2/cli; task commands use /v2/tasks)`);
|
|
89560
89839
|
out.push(` ${import_picocolors61.default.dim("BRAINBASE_API_URL")} legacy KLS host override (uses /api/cli; proxy/registry fallback)`);
|
|
89561
89840
|
out.push(` ${import_picocolors61.default.dim("BRAINBASE_PROXY_URL")} override the model-proxy URL used by harness traffic (default https://api.v1.brainbaselabs.com)`);
|
|
89562
89841
|
out.push(` ${import_picocolors61.default.dim("BRAINBASE_REGISTRY_URL")} override the registry API URL (default https://api.v1.brainbaselabs.com)`);
|