@bivy/bivy 0.18.11-staging.7 → 0.18.11-staging.8
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/bin/acp-shim.mjs +31 -4
- package/bin/bivy.mjs +84 -1
- package/dist/agent-instructions.js +3 -0
- package/dist/agents/claude-code/runtime.js +11 -0
- package/dist/apps/styles.css +3 -1
- package/dist/run-tools.js +8 -0
- package/dist/runtime/protocol.js +43 -0
- package/dist/runtime/tool-call-map.js +23 -0
- package/dist/server.js +62 -0
- package/package.json +1 -1
package/bin/acp-shim.mjs
CHANGED
|
@@ -136,6 +136,11 @@ let initialized = false;
|
|
|
136
136
|
let modelConfigId = "model";
|
|
137
137
|
// A model chosen before the ACP session existed, applied once it does.
|
|
138
138
|
let pendingModel = null;
|
|
139
|
+
// True while session/load replays history (see session.resume).
|
|
140
|
+
let replayingHistory = false;
|
|
141
|
+
// The config-option id carrying reasoning effort (ACP category "thought_level"),
|
|
142
|
+
// when the agent advertises one.
|
|
143
|
+
let thoughtConfigId = null;
|
|
139
144
|
// toolCallId -> { requestId, options } so a later bivy tool.decision answers the
|
|
140
145
|
// right ACP permission request with a concrete optionId.
|
|
141
146
|
const permissionRequests = new Map();
|
|
@@ -369,19 +374,25 @@ function publishModels(result) {
|
|
|
369
374
|
// ACP model ids are `provider/model`; split the provider so Bivy can group and
|
|
370
375
|
// scope provider-specific settings the same way it does for other runtimes.
|
|
371
376
|
.map((m) => ({ ...m, provider: m.id.includes("/") ? m.id.split("/")[0] : "agent" }));
|
|
372
|
-
|
|
373
|
-
|
|
377
|
+
// Reasoning effort rides the same config-option surface under ACP's
|
|
378
|
+
// `thought_level` category (Grok: reasoning_effort xhigh/high/medium/low).
|
|
379
|
+
const thoughtOption = options.find((o) => String(o?.category ?? "") === "thought_level");
|
|
380
|
+
const levels = (Array.isArray(thoughtOption?.options) ? thoughtOption.options : []).map((o) => String(o?.value ?? "")).filter(Boolean);
|
|
381
|
+
if (!models.length && !levels.length) return;
|
|
382
|
+
if (models.length) modelConfigId = String(modelOption?.id ?? "model");
|
|
383
|
+
if (levels.length) thoughtConfigId = String(thoughtOption.id);
|
|
374
384
|
bivy({
|
|
375
385
|
type: "runtime.models",
|
|
376
386
|
models,
|
|
377
387
|
...(modelOption?.currentValue ? { currentModel: String(modelOption.currentValue) } : {}),
|
|
388
|
+
...(levels.length ? { thinking: { levels, ...(thoughtOption?.currentValue ? { current: String(thoughtOption.currentValue) } : {}) } } : {}),
|
|
378
389
|
});
|
|
379
390
|
}
|
|
380
391
|
|
|
381
392
|
// --- ACP → bivy: streamed session/update notifications ----------------------
|
|
382
393
|
function onSessionUpdate(params) {
|
|
383
394
|
const u = params?.update;
|
|
384
|
-
if (!u || typeof u !== "object") return;
|
|
395
|
+
if (!u || typeof u !== "object" || replayingHistory) return;
|
|
385
396
|
// Any update arriving while the turn is draining means the agent is still
|
|
386
397
|
// emitting the tail of this turn (see the drain note above) — reset the quiet
|
|
387
398
|
// window so session.done waits for it instead of sealing history short.
|
|
@@ -600,7 +611,14 @@ async function onBivyCommand(msg) {
|
|
|
600
611
|
// hanging the reopen with no watchdog to recover it (resume isn't a
|
|
601
612
|
// "working" turn). On timeout/failure we fall back to a fresh session so
|
|
602
613
|
// the chat still opens instead of spinning on "Fetching transcript…".
|
|
603
|
-
|
|
614
|
+
// ACP agents replay the whole conversation as session/update during
|
|
615
|
+
// session/load. Bivy already has that history; forwarding the replay
|
|
616
|
+
// re-recorded past thinking (and could fold old text onto the last
|
|
617
|
+
// reply) as if it were live.
|
|
618
|
+
replayingHistory = true;
|
|
619
|
+
let res;
|
|
620
|
+
try { res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers }, { timeoutMs: 30_000 }); }
|
|
621
|
+
finally { replayingHistory = false; }
|
|
604
622
|
sessionId = res?.sessionId ?? ref;
|
|
605
623
|
publishModels(res);
|
|
606
624
|
} catch (error) {
|
|
@@ -683,6 +701,15 @@ async function onBivyCommand(msg) {
|
|
|
683
701
|
bivy({ replyTo: id, ok: true });
|
|
684
702
|
return;
|
|
685
703
|
}
|
|
704
|
+
case "thinking.set": {
|
|
705
|
+
const level = String(msg.level ?? "").trim();
|
|
706
|
+
if (sessionId && thoughtConfigId && level) {
|
|
707
|
+
try { await agentRequest("session/set_config_option", { sessionId, configId: thoughtConfigId, value: level }, { timeoutMs: 15_000 }); }
|
|
708
|
+
catch (e) { bivy({ type: "runtime.debug", message: `acp set thought level failed: ${e instanceof Error ? e.message : String(e)}` }); }
|
|
709
|
+
}
|
|
710
|
+
if (id !== undefined) bivy({ replyTo: id, ok: true });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
686
713
|
case "session.abort": {
|
|
687
714
|
// A pending drain must not fire session.done after the user cancelled —
|
|
688
715
|
// the turn is being torn down, not finishing on its own.
|
package/bin/bivy.mjs
CHANGED
|
@@ -1827,7 +1827,7 @@ function cmdCompletions(args = []) {
|
|
|
1827
1827
|
const shell = (args[0] || "").toLowerCase();
|
|
1828
1828
|
const commands = [
|
|
1829
1829
|
"run", "runs", "sessions", "ls", "resume", "promote", "rename", "nodes", "agent", "agents", "agents:install", "shim", "takeover", "token", "exec",
|
|
1830
|
-
"send", "attach", "suggest", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
|
|
1830
|
+
"send", "attach", "suggest", "delegate", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
|
|
1831
1831
|
"update", "update:log", "audit", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
|
|
1832
1832
|
"github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
|
|
1833
1833
|
];
|
|
@@ -2462,6 +2462,86 @@ async function cmdSuggest(args = []) {
|
|
|
2462
2462
|
console.log(c.green("Suggested in the chat. The user can start it in one tap."));
|
|
2463
2463
|
}
|
|
2464
2464
|
|
|
2465
|
+
// Cross-agent / cross-machine delegation (docs/agent-delegation.md): hand a
|
|
2466
|
+
// self-contained task from this session to another agent, optionally on another
|
|
2467
|
+
// account machine, and get its answer back. Agent-agnostic — any agent with a
|
|
2468
|
+
// shell can call it — and meant for when the user asks for another agent/machine.
|
|
2469
|
+
const DELEGATE_USAGE = `Usage: bivy delegate "<task>" [--agent <id>] [--machine <name>] [--model <model>] [--repo owner/repo] [--wait [seconds]] [--json]
|
|
2470
|
+
bivy delegate status <run-id> [--json]
|
|
2471
|
+
bivy delegate wait <run-id> [--timeout <seconds>] [--json]
|
|
2472
|
+
|
|
2473
|
+
Hands a self-contained task to another agent (and/or another machine on your account) as a
|
|
2474
|
+
delegated Run tied to this session ($BIVY_SESSION_ID). With --wait (or 'wait'), prints the
|
|
2475
|
+
child's final answer and any branch/PR it produced once it finishes. 'bivy nodes' lists machines.`;
|
|
2476
|
+
|
|
2477
|
+
async function cmdDelegate(args = []) {
|
|
2478
|
+
if (!args.length || args.some((a) => a === "-h" || a === "--help")) { console.log(DELEGATE_USAGE); return; }
|
|
2479
|
+
const flagsWithValue = new Set(["--session", "--agent", "--machine", "--model", "--repo", "--timeout"]);
|
|
2480
|
+
const flag = (name) => {
|
|
2481
|
+
const i = args.indexOf(name);
|
|
2482
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
2483
|
+
};
|
|
2484
|
+
const json = args.includes("--json");
|
|
2485
|
+
const waitIndex = args.indexOf("--wait");
|
|
2486
|
+
const waitArg = waitIndex >= 0 && /^\d+$/.test(args[waitIndex + 1] ?? "") ? args[waitIndex + 1] : undefined;
|
|
2487
|
+
const positional = args.filter((a, i) => !a.startsWith("--") && !(i > 0 && flagsWithValue.has(args[i - 1])) && !(waitArg && i === waitIndex + 1));
|
|
2488
|
+
const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
|
|
2489
|
+
if (!sessionId) { console.error(c.red("No session id. Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.")); process.exit(1); return; }
|
|
2490
|
+
const config = loadConfig();
|
|
2491
|
+
if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
|
|
2492
|
+
let token;
|
|
2493
|
+
try { token = await localDeviceToken(config); } catch { token = undefined; }
|
|
2494
|
+
const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/delegated-runs`;
|
|
2495
|
+
const call = async (path, body) => {
|
|
2496
|
+
const res = await fetch(`${base}${path}`, {
|
|
2497
|
+
method: body ? "POST" : "GET",
|
|
2498
|
+
headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
2499
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
2500
|
+
});
|
|
2501
|
+
const data = await res.json().catch(() => ({}));
|
|
2502
|
+
if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
|
|
2503
|
+
return data;
|
|
2504
|
+
};
|
|
2505
|
+
const terminal = (run) => ["succeeded", "failed", "cancelled"].includes(run?.status);
|
|
2506
|
+
// The node caps one wait at 300s; loop until the child ends or our budget does.
|
|
2507
|
+
const waitFor = async (runId, seconds) => {
|
|
2508
|
+
const deadline = Date.now() + seconds * 1000;
|
|
2509
|
+
let run = await call(`/${encodeURIComponent(runId)}`);
|
|
2510
|
+
while (!terminal(run) && Date.now() < deadline) {
|
|
2511
|
+
const slice = Math.max(1, Math.min(300, Math.ceil((deadline - Date.now()) / 1000)));
|
|
2512
|
+
run = await call(`/${encodeURIComponent(runId)}/wait`, { timeoutSeconds: slice });
|
|
2513
|
+
}
|
|
2514
|
+
return run;
|
|
2515
|
+
};
|
|
2516
|
+
const print = (run) => {
|
|
2517
|
+
if (json) { console.log(JSON.stringify(run, null, 2)); return; }
|
|
2518
|
+
console.log(`Run ${run.runId}: ${run.status}${terminal(run) ? "" : " (still running — 'bivy delegate wait " + run.runId + "')"}`);
|
|
2519
|
+
const refs = run.references || {};
|
|
2520
|
+
if (refs.failure) console.log(`Failure: ${refs.failure}`);
|
|
2521
|
+
if (refs.branch) console.log(`Branch: ${refs.branch}`);
|
|
2522
|
+
if (refs.prUrl) console.log(`PR: ${refs.prUrl}`);
|
|
2523
|
+
if (refs.sessionId) console.log(`Child session: ${refs.sessionId}`);
|
|
2524
|
+
if (run.answer) console.log(`\nAnswer:\n${run.answer}`);
|
|
2525
|
+
};
|
|
2526
|
+
try {
|
|
2527
|
+
const [action, runId] = positional;
|
|
2528
|
+
if (action === "status" && runId) return print(await call(`/${encodeURIComponent(runId)}`));
|
|
2529
|
+
if (action === "wait" && runId) return print(await waitFor(runId, Number(flag("--timeout") ?? 600)));
|
|
2530
|
+
const instructions = positional.join(" ").trim();
|
|
2531
|
+
if (!instructions) { console.error(c.red(DELEGATE_USAGE)); process.exit(1); return; }
|
|
2532
|
+
const started = await call("", { instructions, agent: flag("--agent"), machine: flag("--machine"), model: flag("--model"), repo: flag("--repo") });
|
|
2533
|
+
if (waitIndex < 0) {
|
|
2534
|
+
if (!json) console.log(`Delegated as run ${started.runId} (${started.status}). Follow it with 'bivy delegate wait ${started.runId}'.`);
|
|
2535
|
+
else print(started);
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
print(await waitFor(started.runId, Number(waitArg ?? 600)));
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
console.error(c.red(`Delegate failed: ${error?.message || String(error)}`));
|
|
2541
|
+
process.exit(1);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2465
2545
|
// Universal app publishing: every agent that can write JSON and run a command
|
|
2466
2546
|
// uses the same contract. Relative static directories are manifest-relative.
|
|
2467
2547
|
async function cmdApp(args = []) {
|
|
@@ -5468,6 +5548,9 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
|
|
|
5468
5548
|
case "suggest":
|
|
5469
5549
|
await cmdSuggest(args);
|
|
5470
5550
|
break;
|
|
5551
|
+
case "delegate":
|
|
5552
|
+
await cmdDelegate(args);
|
|
5553
|
+
break;
|
|
5471
5554
|
case "completions":
|
|
5472
5555
|
case "completion":
|
|
5473
5556
|
cmdCompletions(args);
|
|
@@ -42,6 +42,9 @@ export const BIVY_AGENT_NOTE = [
|
|
|
42
42
|
'`bivy suggest "<complete instruction>" [--title "short label"]` instead of only listing them. The user can ' +
|
|
43
43
|
"start each in one tap, in this session or in a parallel one that works in its own copy of the project, so " +
|
|
44
44
|
"write it to stand on its own, with paths relative to the project root.",
|
|
45
|
+
"- Only when the user asks for another agent or another of their machines to take part: " +
|
|
46
|
+
'`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
|
|
47
|
+
"its answer and any branch/PR (`bivy nodes` lists machines). Use your own sub-agents for everything else.",
|
|
45
48
|
].join("\n");
|
|
46
49
|
const PREAMBLE = "The user's global Bivy instructions follow. They apply to every workspace; where they conflict with " +
|
|
47
50
|
"instructions from the current repository (AGENTS.md, CLAUDE.md, …), the repository's instructions win.";
|
|
@@ -1294,6 +1294,17 @@ class ClaudeSession {
|
|
|
1294
1294
|
// falls through so its capabilities/commands reach the client.)
|
|
1295
1295
|
if (message?.subtype === "task_notification" || message?.subtype === "compact_boundary")
|
|
1296
1296
|
break;
|
|
1297
|
+
// Banners Claude Code shows the user: a hook's block reason, slash-command
|
|
1298
|
+
// output, warnings (SDK 0.3.283+). Show them as the chat's system notice
|
|
1299
|
+
// and keep them in the transcript; "info" is transcript-mode-only upstream.
|
|
1300
|
+
if (message?.subtype === "informational") {
|
|
1301
|
+
const text = typeof message.content === "string" ? message.content.trim() : "";
|
|
1302
|
+
if (text && message.level !== "info") {
|
|
1303
|
+
this.messages.push({ role: "system", content: text, timestamp: Date.now() });
|
|
1304
|
+
this.emit({ type: "session.notice", level: message.level === "warning" ? "warning" : "info", message: text });
|
|
1305
|
+
}
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1297
1308
|
this.emit({ type: String(message?.type ?? "unknown"), raw: message });
|
|
1298
1309
|
break;
|
|
1299
1310
|
}
|
package/dist/apps/styles.css
CHANGED
|
@@ -1193,7 +1193,9 @@ button.sheet-action:last-child { border-bottom: 0; }
|
|
|
1193
1193
|
margin: 0;
|
|
1194
1194
|
max-height: calc(var(--app-h, 100dvh) - (2 * var(--space-5)));
|
|
1195
1195
|
}
|
|
1196
|
-
|
|
1196
|
+
/* Lifted above the sheet body so a menu opened from the header (the model
|
|
1197
|
+
picker's reasoning menu) layers over the body's sticky search field. */
|
|
1198
|
+
.sheet-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); flex: none; touch-action: none; position: relative; z-index: calc(var(--z-sticky) + 1); }
|
|
1197
1199
|
.sheet-title { font-weight: 700; flex: 1; }
|
|
1198
1200
|
.sheet-close { border: 0; background: transparent; color: var(--muted); font-size: 22px; line-height: 1; min-width: var(--space-7); min-height: var(--space-7); }
|
|
1199
1201
|
.sheet-close:active { transform: scale(0.9); }
|
package/dist/run-tools.js
CHANGED
|
@@ -8,6 +8,8 @@ export const RUN_TOOL_LIMITS = {
|
|
|
8
8
|
maxWaitSeconds: 300,
|
|
9
9
|
minPollMs: 1_000,
|
|
10
10
|
maxIdempotencyKey: 128,
|
|
11
|
+
/** The child's final answer returned to the parent (characters). */
|
|
12
|
+
maxAnswer: 8_000,
|
|
11
13
|
};
|
|
12
14
|
const TERMINAL = new Set(["succeeded", "failed", "cancelled"]);
|
|
13
15
|
const STATUSES = new Set(["pending", "claimed", "running", "waiting", "needs_attention", "succeeded", "failed", "cancelled"]);
|
|
@@ -107,6 +109,12 @@ export class RunDelegationService {
|
|
|
107
109
|
if (!raw || provenance?.parentSessionId !== sessionId)
|
|
108
110
|
throw new Error("delegated Run not found");
|
|
109
111
|
const value = safeRun(raw, provenance);
|
|
112
|
+
if (TERMINAL.has(value.status) && this.backend.answer) {
|
|
113
|
+
// Best-effort: an unreachable machine still leaves status + references.
|
|
114
|
+
const answer = cached?.value.answer ?? await this.backend.answer(raw).catch(() => undefined);
|
|
115
|
+
if (answer)
|
|
116
|
+
value.answer = answer.length > RUN_TOOL_LIMITS.maxAnswer ? `${answer.slice(0, RUN_TOOL_LIMITS.maxAnswer - 1)}…` : answer;
|
|
117
|
+
}
|
|
110
118
|
this.cache.set(`${sessionId}:${runId}`, { at: this.now(), value });
|
|
111
119
|
return value;
|
|
112
120
|
}
|
package/dist/runtime/protocol.js
CHANGED
|
@@ -274,6 +274,32 @@ class ProtocolSession {
|
|
|
274
274
|
* applied to the spawned child and reused by the per-turn preflight. */
|
|
275
275
|
prepareEnv = {};
|
|
276
276
|
getModels() { return this.models; }
|
|
277
|
+
// Reasoning-effort levels published by the shim (see runtime.models); empty
|
|
278
|
+
// when the agent has none, which keeps the picker hidden.
|
|
279
|
+
thinkingLevels = [];
|
|
280
|
+
thinkingLevel;
|
|
281
|
+
thinkingChosen = false;
|
|
282
|
+
supportsThinking() { return this.thinkingLevels.length > 0; }
|
|
283
|
+
getAvailableThinkingLevels() { return this.thinkingLevels; }
|
|
284
|
+
getThinkingLevel() { return this.thinkingLevel; }
|
|
285
|
+
setThinkingLevel(level) {
|
|
286
|
+
const next = level.trim();
|
|
287
|
+
if (!this.thinkingLevels.includes(next))
|
|
288
|
+
return;
|
|
289
|
+
this.thinkingLevel = next;
|
|
290
|
+
this.thinkingChosen = true;
|
|
291
|
+
this.sendThinkingLevel(next);
|
|
292
|
+
}
|
|
293
|
+
/** Fire-and-forget like the pipe runtimes' flag. With no live agent yet the
|
|
294
|
+
* choice is kept and sent once the agent publishes its levels. */
|
|
295
|
+
sendThinkingLevel(level) {
|
|
296
|
+
try {
|
|
297
|
+
void this.command("thinking.set", { sessionId: this.id, level }).catch(() => { });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// Agent not running; runtime.models re-applies the kept choice.
|
|
301
|
+
}
|
|
302
|
+
}
|
|
277
303
|
getCurrentModel() {
|
|
278
304
|
if (!this.currentModelId)
|
|
279
305
|
return undefined;
|
|
@@ -622,6 +648,23 @@ class ProtocolSession {
|
|
|
622
648
|
if (typeof msg.currentModel === "string")
|
|
623
649
|
this.currentModelId = msg.currentModel;
|
|
624
650
|
}
|
|
651
|
+
// Reasoning-effort levels the agent advertises (an ACP `thought_level`
|
|
652
|
+
// config option), driven by the same picker as the pipe runtimes' flags.
|
|
653
|
+
const thinking = msg.thinking;
|
|
654
|
+
const levels = Array.isArray(thinking?.levels) ? thinking.levels.filter((l) => typeof l === "string" && Boolean(l)) : [];
|
|
655
|
+
if (levels.length) {
|
|
656
|
+
this.thinkingLevels = levels;
|
|
657
|
+
// A level picked before the agent was up is applied now; otherwise adopt
|
|
658
|
+
// the agent's own current level.
|
|
659
|
+
if (this.thinkingChosen && this.thinkingLevel && levels.includes(this.thinkingLevel))
|
|
660
|
+
this.sendThinkingLevel(this.thinkingLevel);
|
|
661
|
+
else if (typeof thinking?.current === "string")
|
|
662
|
+
this.thinkingLevel = thinking.current;
|
|
663
|
+
}
|
|
664
|
+
// The catalog arrived after the client asked for it (it lands with the
|
|
665
|
+
// agent's session): let the node push the fresh model/effort picker.
|
|
666
|
+
if (models.length || levels.length)
|
|
667
|
+
this.emit({ type: "runtime.models" });
|
|
625
668
|
return;
|
|
626
669
|
}
|
|
627
670
|
if (type === "message.delta") {
|
|
@@ -103,6 +103,26 @@ const DELEGATE = new Set([
|
|
|
103
103
|
"subagentactivity",
|
|
104
104
|
]);
|
|
105
105
|
const PATH_KEYS = ["path", "file_path", "filePath", "filename", "fileName", "file", "target_file", "targetFile"];
|
|
106
|
+
/**
|
|
107
|
+
* `bivy delegate "<task>" --agent codex --machine mac` run from any agent's
|
|
108
|
+
* shell hands work to another agent/machine (docs/agent-delegation.md). Show it
|
|
109
|
+
* as the delegation it is — "Delegated · codex @ mac" — not as a shell command.
|
|
110
|
+
* `status` / `wait` follow-ups stay ordinary commands.
|
|
111
|
+
*/
|
|
112
|
+
function bivyDelegation(command) {
|
|
113
|
+
const match = /^\s*bivy\s+delegate\b(.*)$/s.exec(command);
|
|
114
|
+
if (!match || /^\s+(status|wait|--help|-h)\b/.test(match[1]))
|
|
115
|
+
return undefined;
|
|
116
|
+
const rest = match[1];
|
|
117
|
+
const flag = (name) => new RegExp(`--${name}(?:=|\\s+)("([^"]*)"|'([^']*)'|(\\S+))`).exec(rest);
|
|
118
|
+
const value = (m) => m ? (m[2] ?? m[3] ?? m[4]) : undefined;
|
|
119
|
+
const agent = value(flag("agent"));
|
|
120
|
+
const machine = value(flag("machine"));
|
|
121
|
+
const quoted = /(?:^|\s)("([^"]+)"|'([^']+)')/.exec(rest.replace(/--[a-z-]+(?:=|\s+)("[^"]*"|'[^']*'|\S+)/g, ""));
|
|
122
|
+
const description = quoted ? (quoted[2] ?? quoted[3]) : undefined;
|
|
123
|
+
const label = [agent, machine && `@ ${machine}`].filter(Boolean).join(" ") || undefined;
|
|
124
|
+
return { ...(label ? { label } : {}), ...(description ? { description } : {}) };
|
|
125
|
+
}
|
|
106
126
|
/**
|
|
107
127
|
* Classify one tool call into a normalized ToolCallDetail, or undefined when it
|
|
108
128
|
* doesn't map to a known kind (the caller then leaves the tool block opaque).
|
|
@@ -114,6 +134,9 @@ export function mapToolCall(toolName, input, context = {}) {
|
|
|
114
134
|
const o = asRecord(input);
|
|
115
135
|
if (inToolSet(SHELL, toolName, key)) {
|
|
116
136
|
const command = str(o, "command", "cmd", "script", "input", "args");
|
|
137
|
+
const delegated = command ? bivyDelegation(command) : undefined;
|
|
138
|
+
if (delegated)
|
|
139
|
+
return decorate({ kind: "delegation", ...delegated }, toolName, input, context);
|
|
117
140
|
return command ? decorate({ kind: "shell", command, ...(str(o, "cwd", "workdir", "workingDir", "directory") ? { cwd: str(o, "cwd", "workdir", "workingDir", "directory") } : {}) }, toolName, input, context) : undefined;
|
|
118
141
|
}
|
|
119
142
|
if (inToolSet(EDIT, toolName, key)) {
|
package/dist/server.js
CHANGED
|
@@ -144,6 +144,7 @@ import { attachmentsFrom, materializeAttachments, sanitizeAttachmentFilename } f
|
|
|
144
144
|
import { AttachmentStore, isValidAttachmentHash } from "./session/attachment-store.js";
|
|
145
145
|
import { planAttachment, isAttachPlanError, MAX_AGENT_ATTACHMENT_BYTES } from "./session/attach-to-chat.js";
|
|
146
146
|
import { ReplicationService } from "./session/replication-service.js";
|
|
147
|
+
import { SiblingClient } from "./session/sibling-client.js";
|
|
147
148
|
import { createSessionNewDedupe } from "./session/session-new-dedupe.js";
|
|
148
149
|
import { computeSessionContract } from "./session/session-contract.js";
|
|
149
150
|
import { SecretVault, resolveSecret } from "./secrets.js";
|
|
@@ -550,6 +551,31 @@ const runDelegation = new RunDelegationService({
|
|
|
550
551
|
}),
|
|
551
552
|
});
|
|
552
553
|
},
|
|
554
|
+
// The child's reply is read from the machine that ran it — locally, or over
|
|
555
|
+
// the sealed node-to-node relay channel. It never travels through the Run queue.
|
|
556
|
+
answer: async (raw) => {
|
|
557
|
+
const output = raw.output && typeof raw.output === "object" ? raw.output : {};
|
|
558
|
+
const sessionId = typeof output.sessionId === "string" ? output.sessionId : "";
|
|
559
|
+
if (!sessionId)
|
|
560
|
+
return undefined;
|
|
561
|
+
const routing = raw.routing && typeof raw.routing === "object" ? raw.routing : {};
|
|
562
|
+
const machine = typeof routing.nodeLabel === "string" && routing.nodeLabel ? routing.nodeLabel : identity.name;
|
|
563
|
+
if (machine === identity.name)
|
|
564
|
+
return delegationAnswer(sessionId);
|
|
565
|
+
const nodes = await delegatedRunRequest("/nodes");
|
|
566
|
+
const target = Array.isArray(nodes) ? nodes.find((node) => node && typeof node === "object" && node.name === machine) : undefined;
|
|
567
|
+
if (typeof target?.id !== "string" || !sessionAdvertiseTarget)
|
|
568
|
+
return undefined;
|
|
569
|
+
const client = new SiblingClient({ controlPlaneUrl: sessionAdvertiseTarget.controlPlaneUrl, enrollmentToken: sessionAdvertiseTarget.enrollmentToken, siblingNodeId: target.id, label: "Bivy delegation" });
|
|
570
|
+
try {
|
|
571
|
+
await client.connect();
|
|
572
|
+
const reply = await client.request({ kind: "delegation.answer", sessionId }, 20_000);
|
|
573
|
+
return typeof reply.answer === "string" ? reply.answer : undefined;
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
client.close();
|
|
577
|
+
}
|
|
578
|
+
},
|
|
553
579
|
});
|
|
554
580
|
// Delegated Runs remain available through the explicit Session API, but are not
|
|
555
581
|
// exposed as agent tools. Coding agents should use their own native sub-agent
|
|
@@ -2406,6 +2432,13 @@ const RELAY_COMMANDS = {
|
|
|
2406
2432
|
if (record)
|
|
2407
2433
|
replayPendingInteractions(record.id);
|
|
2408
2434
|
},
|
|
2435
|
+
// A co-owned node asking for a delegated child's answer over the sealed
|
|
2436
|
+
// node-to-node channel (see runDelegation.answer). Replies to the requester.
|
|
2437
|
+
async "delegation.answer"(msg, ctx) {
|
|
2438
|
+
const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
|
|
2439
|
+
const answer = sessionId ? await delegationAnswer(sessionId).catch(() => undefined) : undefined;
|
|
2440
|
+
ctx.reply({ type: "delegation.answer", requestId: msg.requestId, sessionId, ...(answer ? { answer } : {}) });
|
|
2441
|
+
},
|
|
2409
2442
|
async "sessions.list"() {
|
|
2410
2443
|
// Relay clients request the list on every connect/reconnect. Replay even an
|
|
2411
2444
|
// empty update state so a banner from the previous daemon is cleared.
|
|
@@ -5162,6 +5195,30 @@ async function waitForSessionIdle(record, capMs = 2 * 60 * 60 * 1000) {
|
|
|
5162
5195
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
5163
5196
|
}
|
|
5164
5197
|
}
|
|
5198
|
+
/**
|
|
5199
|
+
* A delegated child's result for its parent (docs/agent-delegation.md): the
|
|
5200
|
+
* session's final assistant reply, from the node's own transcript. Opened from
|
|
5201
|
+
* disk when the session isn't live. Undefined for an unknown session.
|
|
5202
|
+
*/
|
|
5203
|
+
async function delegationAnswer(sessionId) {
|
|
5204
|
+
let record = resolveSession(sessionId);
|
|
5205
|
+
if (!record) {
|
|
5206
|
+
const summary = (await listAllSessions()).find((s) => s.id === sessionId);
|
|
5207
|
+
if (!summary?.path)
|
|
5208
|
+
return undefined;
|
|
5209
|
+
record = await createSession(defaultWorkspace, summary.path, { runtimeId: summary.agent, makeActive: false });
|
|
5210
|
+
}
|
|
5211
|
+
const messages = transcripts.forkMessages(record);
|
|
5212
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
5213
|
+
const message = messages[i];
|
|
5214
|
+
if (message?.role !== "assistant")
|
|
5215
|
+
continue;
|
|
5216
|
+
const text = runtimeContentText(message.content).trim();
|
|
5217
|
+
if (text)
|
|
5218
|
+
return text;
|
|
5219
|
+
}
|
|
5220
|
+
return undefined;
|
|
5221
|
+
}
|
|
5165
5222
|
/** Text of the most recent user message in a session's transcript, if any. */
|
|
5166
5223
|
function lastUserMessageText(record) {
|
|
5167
5224
|
const messages = record.session.getMessages();
|
|
@@ -7789,6 +7846,11 @@ function attachSessionListeners(record) {
|
|
|
7789
7846
|
// sheet for the right provider alongside the inline error bubble.
|
|
7790
7847
|
maybeSignalAuthRequired(record, String(event.error ?? ""));
|
|
7791
7848
|
}
|
|
7849
|
+
if (event.type === "runtime.models") {
|
|
7850
|
+
// A protocol agent published its model/effort catalog late (with its
|
|
7851
|
+
// session). Push it so an open picker fills in without a refresh.
|
|
7852
|
+
void modelsListEventFor(record).then((listed) => broadcast(listed)).catch(() => { });
|
|
7853
|
+
}
|
|
7792
7854
|
if (event.type === "runtime.commands") {
|
|
7793
7855
|
// The agent learned its own slash commands mid-session (e.g. Claude Code's
|
|
7794
7856
|
// system/init reports slash_commands only after the first turn starts).
|
package/package.json
CHANGED