@thedevlabs-io/labd 0.5.0 → 0.6.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/AGENTS.md +35 -12
- package/README.md +20 -15
- package/dist/labd.js +384 -96
- package/dist/labd.js.map +4 -4
- package/package.json +1 -1
package/dist/labd.js
CHANGED
|
@@ -433,6 +433,36 @@ var EXEC_TOOLS = ["run_command"];
|
|
|
433
433
|
function isExecTool(name) {
|
|
434
434
|
return EXEC_TOOLS.includes(name);
|
|
435
435
|
}
|
|
436
|
+
var AGENT_TOOLS = ["spawn_agent"];
|
|
437
|
+
function isAgentTool(name) {
|
|
438
|
+
return AGENT_TOOLS.includes(name);
|
|
439
|
+
}
|
|
440
|
+
var MAX_SUBAGENTS = 3;
|
|
441
|
+
var SUBAGENT_REPORT_CHARS = 8e3;
|
|
442
|
+
function subAgentCapabilities(parent) {
|
|
443
|
+
return parent.filter((c) => !isAgentTool(c));
|
|
444
|
+
}
|
|
445
|
+
function subAgentConversationId(parentId, callId) {
|
|
446
|
+
return parentId ? `${parentId}~helper~${callId}` : void 0;
|
|
447
|
+
}
|
|
448
|
+
function subAgentBrief(task) {
|
|
449
|
+
return `You are working as a helper for labd, the assistant the learner is talking to. It has handed you one piece of a larger job and is waiting on your report. The learner is the same person, in the same project, under the same rules: the same tools, the same approvals, and the same things you will not do.
|
|
450
|
+
|
|
451
|
+
Your task:
|
|
452
|
+
|
|
453
|
+
${task.trim()}
|
|
454
|
+
|
|
455
|
+
How to work:
|
|
456
|
+
- You start with none of the parent conversation, so the task above is all you know about it. If it is not enough to act on, say what is missing in your report rather than guessing at the intent.
|
|
457
|
+
- Do the task, and only the task. The parent decides what happens next, so do not widen the job, and do not ask the learner questions - your report goes to the parent, not to them.
|
|
458
|
+
- If a write or command you need is declined, do not look for another way round it. Stop and say so.
|
|
459
|
+
- End with a report the parent can act on without redoing your work: what you found or changed, file by file and with line numbers where they matter, and anything you could not finish and why. Keep it tight. Leave out how you went about it.`;
|
|
460
|
+
}
|
|
461
|
+
function subAgentReport(text) {
|
|
462
|
+
const clean = text.trim() || "The helper finished without a report.";
|
|
463
|
+
return clean.length <= SUBAGENT_REPORT_CHARS ? clean : `${clean.slice(0, SUBAGENT_REPORT_CHARS)}
|
|
464
|
+
... the helper's report was longer than this and was trimmed.`;
|
|
465
|
+
}
|
|
436
466
|
var AGENT_MODES = ["ask", "auto", "agent"];
|
|
437
467
|
var DEFAULT_AGENT_MODE = "auto";
|
|
438
468
|
function toAgentMode(raw) {
|
|
@@ -1319,6 +1349,154 @@ function trim(output) {
|
|
|
1319
1349
|
].join("");
|
|
1320
1350
|
}
|
|
1321
1351
|
|
|
1352
|
+
// packages/core/src/calls.ts
|
|
1353
|
+
var TOOL_OUTPUT_KEPT = 8e3;
|
|
1354
|
+
function keepOutput(text) {
|
|
1355
|
+
const clean = text.replace(/\s+$/, "");
|
|
1356
|
+
return clean.length <= TOOL_OUTPUT_KEPT ? clean : `${clean.slice(0, TOOL_OUTPUT_KEPT)}
|
|
1357
|
+
\u2026 trimmed here.`;
|
|
1358
|
+
}
|
|
1359
|
+
async function executeCall(call, env) {
|
|
1360
|
+
let preview;
|
|
1361
|
+
if (isWriteTool(call.name) || isExecTool(call.name)) {
|
|
1362
|
+
try {
|
|
1363
|
+
preview = previewTool(call, env.toolCtx);
|
|
1364
|
+
env.step({ status: "running", preview });
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1367
|
+
env.step({ status: "error", detail: message });
|
|
1368
|
+
return { id: call.id, content: message, isError: true };
|
|
1369
|
+
}
|
|
1370
|
+
} else {
|
|
1371
|
+
env.step({ status: "running" });
|
|
1372
|
+
}
|
|
1373
|
+
if (needsApproval(env.mode, call)) {
|
|
1374
|
+
env.step({ status: "awaiting", preview });
|
|
1375
|
+
const ok = env.approve ? await env.approve(call, preview) : false;
|
|
1376
|
+
if (env.signal.aborted) return "aborted";
|
|
1377
|
+
if (!ok) {
|
|
1378
|
+
env.step({ status: "skipped", preview });
|
|
1379
|
+
return {
|
|
1380
|
+
id: call.id,
|
|
1381
|
+
content: env.approve ? "The learner declined this, so nothing was done. Do not retry it. Explain what you would have done and why, and let them decide what to do next." : "This needed the learner to approve it and labd is running unattended, so nothing was done. Say what you would have done; do not try another way round it."
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (env.signal.aborted) return "aborted";
|
|
1386
|
+
try {
|
|
1387
|
+
const content = await runTool(call, env.toolCtx);
|
|
1388
|
+
if (isWriteTool(call.name)) {
|
|
1389
|
+
const input = call.input;
|
|
1390
|
+
env.wrote(String(input.path ?? input.from ?? ""));
|
|
1391
|
+
}
|
|
1392
|
+
if (isExecTool(call.name)) env.ran(String(call.input.command ?? ""));
|
|
1393
|
+
env.step({ status: isWriteTool(call.name) ? "applied" : "done", preview, output: keepOutput(content) });
|
|
1394
|
+
return { id: call.id, content };
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1397
|
+
env.step({ status: "error", preview, detail: message });
|
|
1398
|
+
return { id: call.id, content: message, isError: true };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// packages/core/src/helper.ts
|
|
1403
|
+
var STALL_ROUNDS = 3;
|
|
1404
|
+
var LABEL_MAX = 80;
|
|
1405
|
+
function helperLabel(call) {
|
|
1406
|
+
const description = call.input.description;
|
|
1407
|
+
const text = typeof description === "string" ? description.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim() : "";
|
|
1408
|
+
return text ? text.length > LABEL_MAX ? `${text.slice(0, LABEL_MAX - 1)}\u2026` : text : "helper";
|
|
1409
|
+
}
|
|
1410
|
+
async function runHelper(call, env) {
|
|
1411
|
+
const task = call.input.task;
|
|
1412
|
+
if (typeof task !== "string" || !task.trim()) {
|
|
1413
|
+
return { id: call.id, content: "spawn_agent needs a `task`: the complete brief for the helper.", isError: true };
|
|
1414
|
+
}
|
|
1415
|
+
const label2 = helperLabel(call);
|
|
1416
|
+
const helper = { id: call.id, label: label2 };
|
|
1417
|
+
const ask = env.approve;
|
|
1418
|
+
const capabilities = subAgentCapabilities(env.capabilities);
|
|
1419
|
+
const conversationId = subAgentConversationId(env.sessionId, call.id);
|
|
1420
|
+
let wire = [{ role: "user", text: subAgentBrief(task) }];
|
|
1421
|
+
let report = "";
|
|
1422
|
+
const asked = /* @__PURE__ */ new Set();
|
|
1423
|
+
let stalled = 0;
|
|
1424
|
+
for (; ; ) {
|
|
1425
|
+
let text = "";
|
|
1426
|
+
const calls = [];
|
|
1427
|
+
let needTools = false;
|
|
1428
|
+
let failure = null;
|
|
1429
|
+
const stream = env.client.stream(
|
|
1430
|
+
{
|
|
1431
|
+
protocol: PROTOCOL_VERSION,
|
|
1432
|
+
messages: wire,
|
|
1433
|
+
context: buildContext(env.root),
|
|
1434
|
+
capabilities,
|
|
1435
|
+
...conversationId ? { conversationId } : {},
|
|
1436
|
+
...env.effort ? { effort: env.effort } : {}
|
|
1437
|
+
},
|
|
1438
|
+
env.signal
|
|
1439
|
+
);
|
|
1440
|
+
for await (const event of stream) {
|
|
1441
|
+
if (env.signal.aborted) break;
|
|
1442
|
+
if (event.type === "text") text += event.text;
|
|
1443
|
+
else if (event.type === "tool_call") calls.push(event.call);
|
|
1444
|
+
else if (event.type === "need_tools") needTools = true;
|
|
1445
|
+
else if (event.type === "error") failure = event.message;
|
|
1446
|
+
}
|
|
1447
|
+
if (env.signal.aborted) return "aborted";
|
|
1448
|
+
if (failure) {
|
|
1449
|
+
const sofar = report ? `
|
|
1450
|
+
|
|
1451
|
+
What it had reported before that:
|
|
1452
|
+
${subAgentReport(report)}` : "";
|
|
1453
|
+
return { id: call.id, content: `The helper stopped: ${failure}${sofar}`, isError: true };
|
|
1454
|
+
}
|
|
1455
|
+
wire = [...wire, { role: "assistant", text, ...calls.length ? { toolCalls: calls } : {} }];
|
|
1456
|
+
if (text.trim()) report = text;
|
|
1457
|
+
if (!needTools || !calls.length) break;
|
|
1458
|
+
const fresh = calls.some((c) => {
|
|
1459
|
+
if (isWriteTool(c.name) || isExecTool(c.name)) return true;
|
|
1460
|
+
const key = `${c.name}:${JSON.stringify(c.input)}`;
|
|
1461
|
+
if (asked.has(key)) return false;
|
|
1462
|
+
asked.add(key);
|
|
1463
|
+
return true;
|
|
1464
|
+
});
|
|
1465
|
+
stalled = fresh ? 0 : stalled + 1;
|
|
1466
|
+
if (stalled >= STALL_ROUNDS) {
|
|
1467
|
+
report = `${report}
|
|
1468
|
+
|
|
1469
|
+
(The helper kept asking for what it had already read, so it was stopped.)`.trim();
|
|
1470
|
+
break;
|
|
1471
|
+
}
|
|
1472
|
+
const results = [];
|
|
1473
|
+
for (const c of calls) {
|
|
1474
|
+
if (env.signal.aborted) return "aborted";
|
|
1475
|
+
if (isAgentTool(c.name)) {
|
|
1476
|
+
results.push({ id: c.id, content: "A helper cannot start helpers of its own. Do this part yourself.", isError: true });
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
const outcome = await executeCall(c, {
|
|
1480
|
+
mode: env.mode(),
|
|
1481
|
+
// Routed to this call's row: two helpers can run commands at once.
|
|
1482
|
+
toolCtx: { ...env.toolCtx, onCommandOutput: (line) => env.toolCtx.onCommandOutput?.(line, `${call.id}/${c.id}`) },
|
|
1483
|
+
signal: env.signal,
|
|
1484
|
+
approve: ask ? (pending, preview) => ask(pending, preview, label2) : void 0,
|
|
1485
|
+
step: (s) => env.step({ ...s, call: c, helper }),
|
|
1486
|
+
wrote: env.wrote,
|
|
1487
|
+
ran: env.ran
|
|
1488
|
+
});
|
|
1489
|
+
if (outcome === "aborted") {
|
|
1490
|
+
env.step({ call: c, helper, status: "skipped", detail: "Stopped." });
|
|
1491
|
+
return "aborted";
|
|
1492
|
+
}
|
|
1493
|
+
results.push(outcome);
|
|
1494
|
+
}
|
|
1495
|
+
wire = [...wire, { role: "tool", results }];
|
|
1496
|
+
}
|
|
1497
|
+
return { id: call.id, content: subAgentReport(report) };
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1322
1500
|
// packages/core/src/agent.ts
|
|
1323
1501
|
var CLI_CAPABILITIES = [
|
|
1324
1502
|
"read_file",
|
|
@@ -1328,18 +1506,15 @@ var CLI_CAPABILITIES = [
|
|
|
1328
1506
|
"create_file",
|
|
1329
1507
|
"move_file",
|
|
1330
1508
|
"delete_file",
|
|
1331
|
-
"run_command"
|
|
1509
|
+
"run_command",
|
|
1510
|
+
// Helpers run here, in this process, on the same rules - see helper.ts.
|
|
1511
|
+
"spawn_agent"
|
|
1332
1512
|
];
|
|
1333
|
-
var
|
|
1334
|
-
function keepOutput(text) {
|
|
1335
|
-
const clean = text.replace(/\s+$/, "");
|
|
1336
|
-
return clean.length <= TOOL_OUTPUT_KEPT ? clean : `${clean.slice(0, TOOL_OUTPUT_KEPT)}
|
|
1337
|
-
\u2026 trimmed here.`;
|
|
1338
|
-
}
|
|
1339
|
-
var STALL_ROUNDS = 3;
|
|
1513
|
+
var STALL_ROUNDS2 = 3;
|
|
1340
1514
|
var COMPACT_AT = 75;
|
|
1341
1515
|
async function runTurn(question, opts) {
|
|
1342
|
-
const { session, client, ui
|
|
1516
|
+
const { session, client, ui } = opts;
|
|
1517
|
+
const modeNow = () => opts.currentMode?.() ?? opts.mode;
|
|
1343
1518
|
const turnId = randomUUID2();
|
|
1344
1519
|
const controller = new AbortController();
|
|
1345
1520
|
opts.signal?.addEventListener("abort", () => controller.abort(), { once: true });
|
|
@@ -1348,10 +1523,17 @@ async function runTurn(question, opts) {
|
|
|
1348
1523
|
checkpoints: opts.checkpoints,
|
|
1349
1524
|
turn: { sessionId: session.id, turnId },
|
|
1350
1525
|
commandTimeoutMs: opts.commandTimeoutMs ?? 12e4,
|
|
1351
|
-
onCommandOutput: (
|
|
1526
|
+
onCommandOutput: (line2, key) => ui.commandOutput?.(line2, key)
|
|
1352
1527
|
};
|
|
1353
1528
|
session.add({ kind: "user", text: question, ...opts.attachments?.length ? { attachments: opts.attachments.map((a) => ({ path: a.path })) } : {} }, turnId);
|
|
1354
1529
|
const result = { text: "", turnId, wrote: [], ran: [], aborted: false };
|
|
1530
|
+
const ask = ui.approve;
|
|
1531
|
+
let line = Promise.resolve();
|
|
1532
|
+
const approve = ask ? (call, preview, helper) => {
|
|
1533
|
+
const answer = line.then(() => controller.signal.aborted ? false : ask(call, preview, helper));
|
|
1534
|
+
line = answer.catch(() => void 0);
|
|
1535
|
+
return answer;
|
|
1536
|
+
} : void 0;
|
|
1355
1537
|
const asked = /* @__PURE__ */ new Set();
|
|
1356
1538
|
let stalled = 0;
|
|
1357
1539
|
for (let round = 0; ; round++) {
|
|
@@ -1430,20 +1612,79 @@ async function runTurn(question, opts) {
|
|
|
1430
1612
|
return true;
|
|
1431
1613
|
});
|
|
1432
1614
|
stalled = fresh ? 0 : stalled + 1;
|
|
1433
|
-
if (stalled >=
|
|
1615
|
+
if (stalled >= STALL_ROUNDS2) {
|
|
1434
1616
|
ui.error(
|
|
1435
1617
|
`labd asked for the same things ${stalled} rounds running, so it stopped rather than keep going round. Tell it which file to start in, or ask for a narrower piece of this.`
|
|
1436
1618
|
);
|
|
1437
1619
|
session.add({ kind: "tool", results: calls.map((c) => stopped(c, "error", "Stopped: labd was repeating itself.")) }, turnId);
|
|
1438
1620
|
return result;
|
|
1439
1621
|
}
|
|
1440
|
-
const results =
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1622
|
+
const results = new Array(calls.length);
|
|
1623
|
+
const working = [];
|
|
1624
|
+
try {
|
|
1625
|
+
let helpers = 0;
|
|
1626
|
+
calls.forEach((call, at2) => {
|
|
1627
|
+
if (!isAgentTool(call.name)) return;
|
|
1628
|
+
const label2 = helperLabel(call);
|
|
1629
|
+
if (helpers++ >= MAX_SUBAGENTS) {
|
|
1630
|
+
const why = `Only ${MAX_SUBAGENTS} helpers can run at once, so this one was not started. Do it yourself, or start it once the others have reported.`;
|
|
1631
|
+
ui.tool({ call, status: "error", detail: why });
|
|
1632
|
+
results[at2] = { id: call.id, content: why, isError: true };
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
ui.tool({ call, status: "running", detail: label2 });
|
|
1636
|
+
working.push(
|
|
1637
|
+
runHelper(call, {
|
|
1638
|
+
client,
|
|
1639
|
+
root: opts.root,
|
|
1640
|
+
mode: modeNow,
|
|
1641
|
+
effort: opts.effort,
|
|
1642
|
+
capabilities: CLI_CAPABILITIES,
|
|
1643
|
+
sessionId: session.id,
|
|
1644
|
+
toolCtx,
|
|
1645
|
+
signal: controller.signal,
|
|
1646
|
+
approve,
|
|
1647
|
+
step: (step) => ui.tool(step),
|
|
1648
|
+
wrote: (path) => result.wrote.push(path),
|
|
1649
|
+
ran: (command) => result.ran.push(command)
|
|
1650
|
+
}).catch((err) => ({
|
|
1651
|
+
id: call.id,
|
|
1652
|
+
content: `The helper failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1653
|
+
isError: true
|
|
1654
|
+
})).then((outcome) => {
|
|
1655
|
+
if (outcome === "aborted") {
|
|
1656
|
+
results[at2] = stopped(call);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
results[at2] = outcome;
|
|
1660
|
+
ui.tool({ call, status: outcome.isError ? "error" : "done", detail: label2, output: keepOutput(outcome.content) });
|
|
1661
|
+
})
|
|
1662
|
+
);
|
|
1663
|
+
});
|
|
1664
|
+
for (const [at2, call] of calls.entries()) {
|
|
1665
|
+
if (isAgentTool(call.name)) continue;
|
|
1666
|
+
if (controller.signal.aborted) {
|
|
1667
|
+
results[at2] = stopped(call);
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
const outcome = await executeCall(call, {
|
|
1671
|
+
mode: modeNow(),
|
|
1672
|
+
// Output names its row, so it cannot land in a helper's command
|
|
1673
|
+
// running at the same moment.
|
|
1674
|
+
toolCtx: { ...toolCtx, onCommandOutput: (line2) => ui.commandOutput?.(line2, call.id) },
|
|
1675
|
+
signal: controller.signal,
|
|
1676
|
+
approve,
|
|
1677
|
+
step: (step) => ui.tool({ ...step, call }),
|
|
1678
|
+
wrote: (path) => result.wrote.push(path),
|
|
1679
|
+
ran: (command) => result.ran.push(command)
|
|
1680
|
+
});
|
|
1681
|
+
results[at2] = outcome === "aborted" ? stopped(call) : outcome;
|
|
1445
1682
|
}
|
|
1446
|
-
|
|
1683
|
+
await Promise.all(working);
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
controller.abort();
|
|
1686
|
+
await Promise.allSettled(working);
|
|
1687
|
+
throw err;
|
|
1447
1688
|
}
|
|
1448
1689
|
session.add({ kind: "tool", results }, turnId);
|
|
1449
1690
|
if (controller.signal.aborted) {
|
|
@@ -1455,52 +1696,6 @@ async function runTurn(question, opts) {
|
|
|
1455
1696
|
ui.tool({ call, status, detail: why });
|
|
1456
1697
|
return { id: call.id, content: why, isError: true };
|
|
1457
1698
|
}
|
|
1458
|
-
async function runOne(call) {
|
|
1459
|
-
let preview;
|
|
1460
|
-
if (isWriteTool(call.name) || isExecTool(call.name)) {
|
|
1461
|
-
try {
|
|
1462
|
-
preview = previewTool(call, toolCtx);
|
|
1463
|
-
ui.tool({ call, status: "running", preview });
|
|
1464
|
-
} catch (err) {
|
|
1465
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1466
|
-
ui.tool({ call, status: "error", detail: message });
|
|
1467
|
-
return { id: call.id, content: message, isError: true };
|
|
1468
|
-
}
|
|
1469
|
-
} else {
|
|
1470
|
-
ui.tool({ call, status: "running" });
|
|
1471
|
-
}
|
|
1472
|
-
if (needsApproval(mode, call)) {
|
|
1473
|
-
ui.tool({ call, status: "awaiting", preview });
|
|
1474
|
-
const ok = ui.approve ? await ui.approve(call, preview) : false;
|
|
1475
|
-
if (controller.signal.aborted) return stopped(call);
|
|
1476
|
-
if (!ok) {
|
|
1477
|
-
ui.tool({ call, status: "skipped", preview });
|
|
1478
|
-
return {
|
|
1479
|
-
id: call.id,
|
|
1480
|
-
content: ui.approve ? "The learner declined this, so nothing was done. Do not retry it. Explain what you would have done and why, and let them decide what to do next." : "This needed the learner to approve it and labd is running unattended, so nothing was done. Say what you would have done; do not try another way round it."
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
try {
|
|
1485
|
-
const content = await runTool(call, toolCtx);
|
|
1486
|
-
if (isWriteTool(call.name)) {
|
|
1487
|
-
const input = call.input;
|
|
1488
|
-
result.wrote.push(String(input.path ?? input.from ?? ""));
|
|
1489
|
-
}
|
|
1490
|
-
if (isExecTool(call.name)) result.ran.push(String(call.input.command ?? ""));
|
|
1491
|
-
ui.tool({
|
|
1492
|
-
call,
|
|
1493
|
-
status: isWriteTool(call.name) ? "applied" : "done",
|
|
1494
|
-
preview,
|
|
1495
|
-
output: keepOutput(content)
|
|
1496
|
-
});
|
|
1497
|
-
return { id: call.id, content };
|
|
1498
|
-
} catch (err) {
|
|
1499
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1500
|
-
ui.tool({ call, status: "error", preview, detail: message });
|
|
1501
|
-
return { id: call.id, content: message, isError: true };
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
1699
|
}
|
|
1505
1700
|
async function compact(opts) {
|
|
1506
1701
|
const controller = new AbortController();
|
|
@@ -1898,9 +2093,16 @@ function setColor(value) {
|
|
|
1898
2093
|
on = value;
|
|
1899
2094
|
}
|
|
1900
2095
|
var wrap = (open, close = "0") => (text) => on ? `${ESC}[${open}m${text}${ESC}[${close}m` : text;
|
|
2096
|
+
function lightBackground() {
|
|
2097
|
+
const bg = Number(process.env.COLORFGBG?.split(";").at(-1));
|
|
2098
|
+
return bg === 7 || bg === 15;
|
|
2099
|
+
}
|
|
1901
2100
|
var paint = {
|
|
1902
|
-
/**
|
|
1903
|
-
|
|
2101
|
+
/**
|
|
2102
|
+
* labd's accent. 214 is the nearest xterm cube entry to the brand orange.
|
|
2103
|
+
* Closes the foreground only, so it can sit inside a `band` without ending it.
|
|
2104
|
+
*/
|
|
2105
|
+
accent: wrap("38;5;214", "39"),
|
|
1904
2106
|
bold: wrap("1", "22"),
|
|
1905
2107
|
dim: wrap("2", "22"),
|
|
1906
2108
|
muted: wrap("90"),
|
|
@@ -1912,6 +2114,11 @@ var paint = {
|
|
|
1912
2114
|
/** For the one place a background is worth it: the cursor cell in the box. */
|
|
1913
2115
|
invert: wrap("7", "27")
|
|
1914
2116
|
};
|
|
2117
|
+
function band(text, visible, width) {
|
|
2118
|
+
if (!on) return text;
|
|
2119
|
+
const bg = lightBackground() ? "48;5;254" : "48;5;236";
|
|
2120
|
+
return `${ESC}[${bg}m${text}${" ".repeat(Math.max(0, width - visible))}${ESC}[49m`;
|
|
2121
|
+
}
|
|
1915
2122
|
function stripColor(text) {
|
|
1916
2123
|
return text.replace(new RegExp(`${ESC}\\[[0-9;]*m`, "g"), "");
|
|
1917
2124
|
}
|
|
@@ -2073,7 +2280,8 @@ var LABELS = {
|
|
|
2073
2280
|
create_file: "Create",
|
|
2074
2281
|
move_file: "Move",
|
|
2075
2282
|
delete_file: "Delete",
|
|
2076
|
-
run_command: "Run"
|
|
2283
|
+
run_command: "Run",
|
|
2284
|
+
spawn_agent: "Helper"
|
|
2077
2285
|
};
|
|
2078
2286
|
function approvalCard(preview, call, mode, width) {
|
|
2079
2287
|
const out = [""];
|
|
@@ -3203,6 +3411,17 @@ var Transcript = class {
|
|
|
3203
3411
|
this.entries.push(entry);
|
|
3204
3412
|
return entry;
|
|
3205
3413
|
}
|
|
3414
|
+
/**
|
|
3415
|
+
* Move a block to the bottom. For an approval card, which must stay the last
|
|
3416
|
+
* thing on screen while it waits: a helper that keeps working adds rows below
|
|
3417
|
+
* it, and a card pushed up past the fold is one approved without being read.
|
|
3418
|
+
*/
|
|
3419
|
+
toEnd(entry) {
|
|
3420
|
+
const at2 = this.entries.indexOf(entry);
|
|
3421
|
+
if (at2 === -1 || at2 === this.entries.length - 1) return;
|
|
3422
|
+
this.entries.splice(at2, 1);
|
|
3423
|
+
this.entries.push(entry);
|
|
3424
|
+
}
|
|
3206
3425
|
/** Say a block changed, so its cached lines are dropped on the next render. */
|
|
3207
3426
|
touch(entry) {
|
|
3208
3427
|
entry.version++;
|
|
@@ -3281,7 +3500,10 @@ function renderBlock(block, width) {
|
|
|
3281
3500
|
switch (block.kind) {
|
|
3282
3501
|
case "user":
|
|
3283
3502
|
return block.text.split("\n").flatMap(
|
|
3284
|
-
(line, i) => wrapPlain(line, inner - 2).map((piece, j) =>
|
|
3503
|
+
(line, i) => wrapPlain(line, inner - 2).map((piece, j) => {
|
|
3504
|
+
const text = i === 0 && j === 0 ? `${paint.accent(">")} ${piece}` : ` ${piece}`;
|
|
3505
|
+
return band(text, visibleWidth(text), inner);
|
|
3506
|
+
})
|
|
3285
3507
|
);
|
|
3286
3508
|
case "assistant":
|
|
3287
3509
|
return [];
|
|
@@ -3295,7 +3517,10 @@ function renderBlock(block, width) {
|
|
|
3295
3517
|
return out;
|
|
3296
3518
|
}
|
|
3297
3519
|
case "approval": {
|
|
3298
|
-
const out =
|
|
3520
|
+
const out = [
|
|
3521
|
+
...block.helper ? [` ${paint.accent("from a helper:")} ${paint.muted(block.helper)}`] : [],
|
|
3522
|
+
...approvalCard(block.preview, block.call, block.mode, inner)
|
|
3523
|
+
];
|
|
3299
3524
|
out.push(
|
|
3300
3525
|
block.answer === null ? ` ${paint.yellow("y")} ${paint.muted("to allow \xB7")} ${paint.yellow("n")} ${paint.muted("or esc to decline")}` : ` ${block.answer === "allowed" ? paint.green("allowed") : paint.muted("declined")}`
|
|
3301
3526
|
);
|
|
@@ -4469,11 +4694,18 @@ function fileTag(line) {
|
|
|
4469
4694
|
return { at: at2, partial };
|
|
4470
4695
|
}
|
|
4471
4696
|
function label(step) {
|
|
4472
|
-
|
|
4697
|
+
if (step.call.name === "spawn_agent") return `working with a helper: ${detail(step)}`;
|
|
4698
|
+
const own = `${step.call.name.replace(/_/g, " ")} ${detail(step)}`;
|
|
4699
|
+
return step.helper ? `${step.helper.label}: ${own}` : own;
|
|
4700
|
+
}
|
|
4701
|
+
function stepKey(step) {
|
|
4702
|
+
return step.helper ? `${step.helper.id}/${step.call.id}` : step.call.id;
|
|
4473
4703
|
}
|
|
4474
4704
|
function detail(step) {
|
|
4475
4705
|
const input = step.call.input;
|
|
4476
|
-
|
|
4706
|
+
if (step.call.name === "spawn_agent") return String(input.description ?? "helper");
|
|
4707
|
+
const own = String(input.why ?? input.path ?? input.from ?? input.query ?? "");
|
|
4708
|
+
return step.helper ? `\u21B3 ${step.helper.label} \xB7 ${own}` : own;
|
|
4477
4709
|
}
|
|
4478
4710
|
function verbatim(call) {
|
|
4479
4711
|
const i = call.input;
|
|
@@ -4489,6 +4721,10 @@ function verbatim(call) {
|
|
|
4489
4721
|
return `${str("from")} \u2192 ${str("to")}`;
|
|
4490
4722
|
case "list_dir":
|
|
4491
4723
|
return str("path") || "/";
|
|
4724
|
+
// The whole brief: what the helper was actually told is what a learner
|
|
4725
|
+
// wants when its report surprises them.
|
|
4726
|
+
case "spawn_agent":
|
|
4727
|
+
return str("task");
|
|
4492
4728
|
default:
|
|
4493
4729
|
return str("path");
|
|
4494
4730
|
}
|
|
@@ -4580,6 +4816,8 @@ async function runInteractive(runtime) {
|
|
|
4580
4816
|
abort = new AbortController();
|
|
4581
4817
|
const markdown = new MarkdownStream(renderer.contentWidth);
|
|
4582
4818
|
let opened = false;
|
|
4819
|
+
let held = null;
|
|
4820
|
+
const say = (line) => held ? held.push(line) : renderer.print(line);
|
|
4583
4821
|
const ui = {
|
|
4584
4822
|
text(chunk) {
|
|
4585
4823
|
const ready = markdown.push(chunk);
|
|
@@ -4601,7 +4839,7 @@ async function runInteractive(runtime) {
|
|
|
4601
4839
|
}
|
|
4602
4840
|
}
|
|
4603
4841
|
if (step.status === "running" || step.status === "awaiting") {
|
|
4604
|
-
startSpinner(label(step));
|
|
4842
|
+
if (!held) startSpinner(label(step));
|
|
4605
4843
|
return;
|
|
4606
4844
|
}
|
|
4607
4845
|
for (const line of toolBlock(
|
|
@@ -4611,19 +4849,27 @@ async function runInteractive(runtime) {
|
|
|
4611
4849
|
{ input: verbatim(step.call), output: step.output ?? step.detail },
|
|
4612
4850
|
renderer.contentWidth
|
|
4613
4851
|
)) {
|
|
4614
|
-
|
|
4852
|
+
say(line);
|
|
4615
4853
|
}
|
|
4616
4854
|
},
|
|
4617
|
-
async approve(call, preview) {
|
|
4855
|
+
async approve(call, preview, helper) {
|
|
4618
4856
|
stopSpinner();
|
|
4857
|
+
if (helper) renderer.print(` ${paint.accent("from a helper:")} ${paint.muted(helper)}`);
|
|
4619
4858
|
for (const line of approvalCard(preview, call, state.mode, renderer.contentWidth)) {
|
|
4620
4859
|
renderer.print(line);
|
|
4621
4860
|
}
|
|
4622
4861
|
const verb = call.name === "run_command" ? "run this command" : "apply this change";
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4862
|
+
held = [];
|
|
4863
|
+
try {
|
|
4864
|
+
const answer = await prompt.read(`${verb}? [y/N]`);
|
|
4865
|
+
const ok = answer.kind === "line" && /^y(es)?$/i.test(answer.text.trim());
|
|
4866
|
+
renderer.print(` ${ok ? paint.green("allowed") : paint.muted("declined")}`);
|
|
4867
|
+
return ok;
|
|
4868
|
+
} finally {
|
|
4869
|
+
const waiting = held;
|
|
4870
|
+
held = null;
|
|
4871
|
+
for (const line of waiting) renderer.print(line);
|
|
4872
|
+
}
|
|
4627
4873
|
},
|
|
4628
4874
|
error(message) {
|
|
4629
4875
|
stopSpinner();
|
|
@@ -4634,7 +4880,7 @@ async function runInteractive(runtime) {
|
|
|
4634
4880
|
if (info2.credits) state.credits = info2.credits;
|
|
4635
4881
|
},
|
|
4636
4882
|
commandOutput(line) {
|
|
4637
|
-
|
|
4883
|
+
say(paint.dim(` | ${line.replace(/\n/g, "\n | ")}`));
|
|
4638
4884
|
}
|
|
4639
4885
|
};
|
|
4640
4886
|
startSpinner("thinking");
|
|
@@ -4817,6 +5063,14 @@ async function runScreen(runtime, opts) {
|
|
|
4817
5063
|
const history = new History();
|
|
4818
5064
|
const state = {
|
|
4819
5065
|
mode: runtime.settings.mode,
|
|
5066
|
+
/**
|
|
5067
|
+
* The mode the running turn is held to, when shift+tab has moved `mode` up
|
|
5068
|
+
* to `agent` mid-answer. Going up does not apply until the next question -
|
|
5069
|
+
* a habit keypress must not let the very next command run unasked - while
|
|
5070
|
+
* going down applies at once, being the safe direction. Null when the turn
|
|
5071
|
+
* simply follows `mode`.
|
|
5072
|
+
*/
|
|
5073
|
+
heldMode: null,
|
|
4820
5074
|
effort: runtime.settings.effort,
|
|
4821
5075
|
context: null,
|
|
4822
5076
|
credits: null,
|
|
@@ -4952,7 +5206,8 @@ async function runScreen(runtime, opts) {
|
|
|
4952
5206
|
`${paint.muted(basename5(runtime.root))}${runtime.branch ? ` ${paint.muted(runtime.branch)}` : ""}`,
|
|
4953
5207
|
paint.dim(`session ${session}`)
|
|
4954
5208
|
].join(paint.dim(" \xB7 "));
|
|
4955
|
-
const
|
|
5209
|
+
const shown = state.mode === "agent" ? paint.red(paint.bold("agent")) : state.mode === "auto" ? paint.accent("auto") : paint.muted("ask");
|
|
5210
|
+
const mode = state.heldMode ? `${shown}${paint.dim(" (next question)")}` : shown;
|
|
4956
5211
|
const right = `${mode}${state.effort ? paint.dim(` \xB7 think ${state.effort}`) : ""} `;
|
|
4957
5212
|
return spread(` ${left}`, right, width);
|
|
4958
5213
|
}
|
|
@@ -4966,7 +5221,7 @@ async function runScreen(runtime, opts) {
|
|
|
4966
5221
|
else if (state.popup) left = d("\u2191\u2193 move \xB7 tab/enter accept \xB7 esc close");
|
|
4967
5222
|
else {
|
|
4968
5223
|
const described = describe(editor.currentLine, runtime);
|
|
4969
|
-
left = described.hint ? described.hint : d("enter send \xB7 shift+
|
|
5224
|
+
left = described.hint ? described.hint : d("enter send \xB7 shift+tab mode \xB7 / commands \xB7 @ files \xB7 pgup/pgdn scroll \xB7 ctrl+c stop \xB7 ctrl+d quit");
|
|
4970
5225
|
}
|
|
4971
5226
|
const bits = [];
|
|
4972
5227
|
if (state.context) {
|
|
@@ -5050,6 +5305,7 @@ async function runScreen(runtime, opts) {
|
|
|
5050
5305
|
if (key.sequence === "n" || key.sequence === "N" || key.name === "escape") return answerApproval(false);
|
|
5051
5306
|
return;
|
|
5052
5307
|
}
|
|
5308
|
+
if (key.name === "tab" && key.shift) return cycleMode();
|
|
5053
5309
|
if (key.ctrl && key.name === "d") {
|
|
5054
5310
|
if (editor.isEmpty) quit();
|
|
5055
5311
|
return;
|
|
@@ -5127,6 +5383,25 @@ async function runScreen(runtime, opts) {
|
|
|
5127
5383
|
state.ctrlCAt = now;
|
|
5128
5384
|
notice("press ctrl+c again to quit");
|
|
5129
5385
|
}
|
|
5386
|
+
function effectiveMode() {
|
|
5387
|
+
return state.heldMode ?? state.mode;
|
|
5388
|
+
}
|
|
5389
|
+
function cycleMode() {
|
|
5390
|
+
const previous = effectiveMode();
|
|
5391
|
+
const next = AGENT_MODES[(AGENT_MODES.indexOf(state.mode) + 1) % AGENT_MODES.length];
|
|
5392
|
+
state.mode = next;
|
|
5393
|
+
const running = state.abort !== null;
|
|
5394
|
+
state.heldMode = next === "agent" && running ? previous : null;
|
|
5395
|
+
notice(`${next}: ${MODE_CAPTION[next]}`, 4e3);
|
|
5396
|
+
if (next === "agent") {
|
|
5397
|
+
note(
|
|
5398
|
+
` ${paint.red(paint.bold("agent"))} ${paint.muted(
|
|
5399
|
+
running ? `- from your next question. This answer stays on ${previous}.` : "- commands now run without asking, and cannot be put back."
|
|
5400
|
+
)}
|
|
5401
|
+
${paint.muted("For this session only. shift+tab again to leave it.")}`
|
|
5402
|
+
);
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5130
5405
|
function answerApproval(ok) {
|
|
5131
5406
|
const approval = state.approval;
|
|
5132
5407
|
if (!approval) return;
|
|
@@ -5241,7 +5516,10 @@ async function runScreen(runtime, opts) {
|
|
|
5241
5516
|
});
|
|
5242
5517
|
if (outcome.farewell) state.farewell = outcome.farewell;
|
|
5243
5518
|
if (outcome.exit) return quit();
|
|
5244
|
-
if (outcome.mode)
|
|
5519
|
+
if (outcome.mode) {
|
|
5520
|
+
state.mode = outcome.mode;
|
|
5521
|
+
state.heldMode = null;
|
|
5522
|
+
}
|
|
5245
5523
|
if (outcome.effort !== void 0) state.effort = outcome.effort;
|
|
5246
5524
|
schedule();
|
|
5247
5525
|
}
|
|
@@ -5254,6 +5532,7 @@ async function runScreen(runtime, opts) {
|
|
|
5254
5532
|
["shift+enter, ctrl+j, or a trailing \\", "new line"],
|
|
5255
5533
|
["/ and @", "commands and files, with a list"],
|
|
5256
5534
|
["tab", "complete"],
|
|
5535
|
+
["shift+tab", "next mode: ask, auto, agent"],
|
|
5257
5536
|
["pgup / pgdn, wheel", "scroll the transcript"],
|
|
5258
5537
|
["home / end", "top / bottom (with an empty box)"],
|
|
5259
5538
|
["esc", "close a list or dialog; jump back to the bottom"],
|
|
@@ -5336,6 +5615,7 @@ async function runScreen(runtime, opts) {
|
|
|
5336
5615
|
}
|
|
5337
5616
|
}
|
|
5338
5617
|
async function ask(input) {
|
|
5618
|
+
state.heldMode = null;
|
|
5339
5619
|
transcript.add({ kind: "user", text: input });
|
|
5340
5620
|
state.asked = true;
|
|
5341
5621
|
state.answer = null;
|
|
@@ -5372,7 +5652,8 @@ async function runScreen(runtime, opts) {
|
|
|
5372
5652
|
}
|
|
5373
5653
|
}
|
|
5374
5654
|
closeAnswer();
|
|
5375
|
-
|
|
5655
|
+
const key = stepKey(step);
|
|
5656
|
+
let entry = state.toolBlocks.get(key);
|
|
5376
5657
|
if (!entry) {
|
|
5377
5658
|
entry = transcript.add({
|
|
5378
5659
|
kind: "tool",
|
|
@@ -5382,7 +5663,11 @@ async function runScreen(runtime, opts) {
|
|
|
5382
5663
|
input: verbatim(step.call) || void 0,
|
|
5383
5664
|
live: []
|
|
5384
5665
|
});
|
|
5385
|
-
state.toolBlocks.set(
|
|
5666
|
+
state.toolBlocks.set(key, entry);
|
|
5667
|
+
if (state.approval) {
|
|
5668
|
+
transcript.toEnd(state.approval.entry);
|
|
5669
|
+
if (viewport.follow) viewport.end();
|
|
5670
|
+
}
|
|
5386
5671
|
}
|
|
5387
5672
|
entry.block.status = step.status;
|
|
5388
5673
|
if (step.status !== "running" && step.status !== "awaiting") {
|
|
@@ -5393,10 +5678,10 @@ async function runScreen(runtime, opts) {
|
|
|
5393
5678
|
if (step.status === "running" || step.status === "awaiting") setBusy(label(step));
|
|
5394
5679
|
schedule();
|
|
5395
5680
|
},
|
|
5396
|
-
approve(call, preview) {
|
|
5681
|
+
approve(call, preview, helper) {
|
|
5397
5682
|
setBusy(null);
|
|
5398
5683
|
closeAnswer();
|
|
5399
|
-
const entry = transcript.add({ kind: "approval", call, preview, mode:
|
|
5684
|
+
const entry = transcript.add({ kind: "approval", call, preview, helper, mode: effectiveMode(), answer: null });
|
|
5400
5685
|
viewport.end();
|
|
5401
5686
|
return new Promise((resolve2) => {
|
|
5402
5687
|
state.approval = { entry, resolve: resolve2 };
|
|
@@ -5413,11 +5698,8 @@ async function runScreen(runtime, opts) {
|
|
|
5413
5698
|
if (info2.credits) state.credits = info2.credits;
|
|
5414
5699
|
schedule();
|
|
5415
5700
|
},
|
|
5416
|
-
commandOutput(line) {
|
|
5417
|
-
|
|
5418
|
-
for (const entry of state.toolBlocks.values()) {
|
|
5419
|
-
if (entry.block.name === "run_command" && entry.block.status === "running") target = entry;
|
|
5420
|
-
}
|
|
5701
|
+
commandOutput(line, key) {
|
|
5702
|
+
const target = key ? state.toolBlocks.get(key) : void 0;
|
|
5421
5703
|
if (!target) return;
|
|
5422
5704
|
target.block.live.push(...line.replace(/\n$/, "").split("\n"));
|
|
5423
5705
|
transcript.touch(target);
|
|
@@ -5431,6 +5713,7 @@ async function runScreen(runtime, opts) {
|
|
|
5431
5713
|
client: runtime.client,
|
|
5432
5714
|
root: runtime.root,
|
|
5433
5715
|
mode: state.mode,
|
|
5716
|
+
currentMode: effectiveMode,
|
|
5434
5717
|
checkpoints: runtime.checkpoints,
|
|
5435
5718
|
effort: state.effort,
|
|
5436
5719
|
attachments,
|
|
@@ -5453,6 +5736,7 @@ async function runScreen(runtime, opts) {
|
|
|
5453
5736
|
transcript.add({ kind: "error", text: err instanceof Error ? err.message : String(err) });
|
|
5454
5737
|
} finally {
|
|
5455
5738
|
if (state.abort === abort) state.abort = null;
|
|
5739
|
+
state.heldMode = null;
|
|
5456
5740
|
if (state.approval) answerApproval(false);
|
|
5457
5741
|
setBusy(null);
|
|
5458
5742
|
}
|
|
@@ -5637,6 +5921,7 @@ async function runPrint(runtime, opts) {
|
|
|
5637
5921
|
emit({
|
|
5638
5922
|
type: "tool",
|
|
5639
5923
|
name: step.call.name,
|
|
5924
|
+
...step.helper ? { helper: step.helper } : {},
|
|
5640
5925
|
status: step.status,
|
|
5641
5926
|
input: step.call.input,
|
|
5642
5927
|
detail: step.detail,
|
|
@@ -5644,7 +5929,8 @@ async function runPrint(runtime, opts) {
|
|
|
5644
5929
|
output: step.output
|
|
5645
5930
|
});
|
|
5646
5931
|
} else if (step.status !== "running") {
|
|
5647
|
-
|
|
5932
|
+
const who = step.helper ? `${step.helper.label} > ` : "";
|
|
5933
|
+
toStderr(`${paint.dim(`${who}${step.call.name}: ${step.status}${step.detail ? ` - ${step.detail}` : ""}`)}
|
|
5648
5934
|
`);
|
|
5649
5935
|
}
|
|
5650
5936
|
if (step.status === "skipped") declined = true;
|
|
@@ -5803,6 +6089,8 @@ async function runRpc(runtime) {
|
|
|
5803
6089
|
tool(step) {
|
|
5804
6090
|
notify("labd/tool", {
|
|
5805
6091
|
name: step.call.name,
|
|
6092
|
+
// Which helper made the call, when one did: its spawn_agent call id and name.
|
|
6093
|
+
...step.helper ? { helper: step.helper } : {},
|
|
5806
6094
|
status: step.status,
|
|
5807
6095
|
input: step.call.input,
|
|
5808
6096
|
detail: step.detail,
|
|
@@ -5816,8 +6104,8 @@ async function runRpc(runtime) {
|
|
|
5816
6104
|
done(info2) {
|
|
5817
6105
|
notify("labd/done", info2);
|
|
5818
6106
|
},
|
|
5819
|
-
commandOutput(line) {
|
|
5820
|
-
notify("labd/command_output", { line });
|
|
6107
|
+
commandOutput(line, key) {
|
|
6108
|
+
notify("labd/command_output", { line, ...key ? { key } : {} });
|
|
5821
6109
|
},
|
|
5822
6110
|
/**
|
|
5823
6111
|
* Ask the host. A host that never answers gets a refusal after a timeout
|
|
@@ -5825,7 +6113,7 @@ async function runRpc(runtime) {
|
|
|
5825
6113
|
* alternative is writing to someone's files because their integration was
|
|
5826
6114
|
* asleep.
|
|
5827
6115
|
*/
|
|
5828
|
-
approve(call, preview) {
|
|
6116
|
+
approve(call, preview, helper) {
|
|
5829
6117
|
return new Promise((resolve2) => {
|
|
5830
6118
|
if (closed) return resolve2(false);
|
|
5831
6119
|
const id = nextId++;
|
|
@@ -5836,7 +6124,7 @@ async function runRpc(runtime) {
|
|
|
5836
6124
|
clearTimeout(timer);
|
|
5837
6125
|
resolve2(approved);
|
|
5838
6126
|
});
|
|
5839
|
-
out({ jsonrpc: "2.0", id, method: "labd/approve", params: { call, preview } });
|
|
6127
|
+
out({ jsonrpc: "2.0", id, method: "labd/approve", params: { call, preview, ...helper ? { helper } : {} } });
|
|
5840
6128
|
});
|
|
5841
6129
|
}
|
|
5842
6130
|
};
|
|
@@ -5866,7 +6154,7 @@ async function runRpc(runtime) {
|
|
|
5866
6154
|
}
|
|
5867
6155
|
|
|
5868
6156
|
// packages/cli/src/index.ts
|
|
5869
|
-
var VERSION = true ? "0.
|
|
6157
|
+
var VERSION = true ? "0.6.0" : "0.0.0-dev";
|
|
5870
6158
|
async function main(argv) {
|
|
5871
6159
|
const args = parse(argv);
|
|
5872
6160
|
if (args.color !== void 0) setColor(args.color);
|