@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 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
- if (!models.length) return;
373
- modelConfigId = String(modelOption?.id ?? "model");
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
- const res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers }, { timeoutMs: 30_000 });
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
  }
@@ -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
- .sheet-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); flex: none; touch-action: none; }
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.18.11-staging.7",
3
+ "version": "0.18.11-staging.8",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",