@bivy/bivy 0.18.11-staging.6 → 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/controllers/fork-commands.js +2 -2
- 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 +64 -0
- package/dist/session/event-log.js +24 -2
- package/dist/session/fork-standup.js +5 -0
- package/dist/session/fork.js +5 -1
- 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); }
|
|
@@ -72,7 +72,7 @@ export function createForkCommands(deps) {
|
|
|
72
72
|
// When the client has already picked a target agent, pass it so the
|
|
73
73
|
// bundle omits the native payload for a cross-runtime fork (it could
|
|
74
74
|
// never be replayed there — see buildForkBundle). Unset => keep it.
|
|
75
|
-
const bundle = buildForkBundle({ runtime: deps.getRuntime(rec.runtimeId), sessionFile: rec.sessionFile, record: forkRecord, dirtyPatch, workspaceSnapshot, targetRuntimeId: deps.agentFrom(msg), liveMessages: deps.forkMessages(rec), state: deps.forkInFlightState(rec) });
|
|
75
|
+
const bundle = buildForkBundle({ runtime: deps.getRuntime(rec.runtimeId), sessionFile: rec.sessionFile, record: forkRecord, dirtyPatch, workspaceSnapshot, targetRuntimeId: deps.agentFrom(msg), liveMessages: deps.forkMessages(rec), display: deps.forkDisplay(rec), state: deps.forkInFlightState(rec) });
|
|
76
76
|
ctx.reply({ type: "session.fork.bundle", requestId, bundle });
|
|
77
77
|
}
|
|
78
78
|
catch (error) {
|
|
@@ -150,7 +150,7 @@ export function createForkCommands(deps) {
|
|
|
150
150
|
? captureWorkspaceSnapshot(sourceCwd, { maxBytes: deps.forkWorkspaceMaxBytes() })
|
|
151
151
|
: undefined;
|
|
152
152
|
// Same runtime → the bundle carries the native payload → full fidelity.
|
|
153
|
-
const bundle = buildForkBundle({ runtime, sessionFile: rec.sessionFile, record: forkRecord, dirtyPatch, workspaceSnapshot, targetRuntimeId: rec.runtimeId, liveMessages: deps.forkMessages(rec), state: deps.forkInFlightState(rec) });
|
|
153
|
+
const bundle = buildForkBundle({ runtime, sessionFile: rec.sessionFile, record: forkRecord, dirtyPatch, workspaceSnapshot, targetRuntimeId: rec.runtimeId, liveMessages: deps.forkMessages(rec), display: deps.forkDisplay(rec), state: deps.forkInFlightState(rec) });
|
|
154
154
|
// Cut a fresh fork branch (the source still holds its own); skip prereq
|
|
155
155
|
// detection (same node + same runtime ⇒ agent and repo are present).
|
|
156
156
|
const outcome = await deps.standUpFork({
|
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
|
|
@@ -2105,6 +2131,7 @@ const RELAY_COMMANDS = {
|
|
|
2105
2131
|
...createForkCommands({
|
|
2106
2132
|
broadcast,
|
|
2107
2133
|
forkMessages: (rec) => transcripts.forkMessages(rec),
|
|
2134
|
+
forkDisplay: (rec) => transcripts.conversationMessages(rec),
|
|
2108
2135
|
resolveSession: (sessionId) => resolveSession(sessionId),
|
|
2109
2136
|
getRuntime: (runtimeId) => getRuntime(runtimeId),
|
|
2110
2137
|
forkRecordFor,
|
|
@@ -2405,6 +2432,13 @@ const RELAY_COMMANDS = {
|
|
|
2405
2432
|
if (record)
|
|
2406
2433
|
replayPendingInteractions(record.id);
|
|
2407
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
|
+
},
|
|
2408
2442
|
async "sessions.list"() {
|
|
2409
2443
|
// Relay clients request the list on every connect/reconnect. Replay even an
|
|
2410
2444
|
// empty update state so a banner from the previous daemon is cleared.
|
|
@@ -5161,6 +5195,30 @@ async function waitForSessionIdle(record, capMs = 2 * 60 * 60 * 1000) {
|
|
|
5161
5195
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
5162
5196
|
}
|
|
5163
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
|
+
}
|
|
5164
5222
|
/** Text of the most recent user message in a session's transcript, if any. */
|
|
5165
5223
|
function lastUserMessageText(record) {
|
|
5166
5224
|
const messages = record.session.getMessages();
|
|
@@ -7788,6 +7846,11 @@ function attachSessionListeners(record) {
|
|
|
7788
7846
|
// sheet for the right provider alongside the inline error bubble.
|
|
7789
7847
|
maybeSignalAuthRequired(record, String(event.error ?? ""));
|
|
7790
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
|
+
}
|
|
7791
7854
|
if (event.type === "runtime.commands") {
|
|
7792
7855
|
// The agent learned its own slash commands mid-session (e.g. Claude Code's
|
|
7793
7856
|
// system/init reports slash_commands only after the first turn starts).
|
|
@@ -8919,6 +8982,7 @@ const forkStandUp = createForkStandUp({
|
|
|
8919
8982
|
createSession,
|
|
8920
8983
|
broadcast,
|
|
8921
8984
|
persistSessionMetadata,
|
|
8985
|
+
recordForkDisplay: (record, messages, forkedAt) => eventLog.appendForkDisplay(record.id, messages, forkedAt),
|
|
8922
8986
|
scheduleAdvertise,
|
|
8923
8987
|
bivySessionEnvelope,
|
|
8924
8988
|
applyRequestedModel,
|
|
@@ -209,6 +209,12 @@ function isBase(value) {
|
|
|
209
209
|
const record = value;
|
|
210
210
|
return record.bivyKind === "base" && Array.isArray(record.messages) && typeof record.reset === "boolean";
|
|
211
211
|
}
|
|
212
|
+
function isForkDisplay(value) {
|
|
213
|
+
if (!value || typeof value !== "object")
|
|
214
|
+
return false;
|
|
215
|
+
const record = value;
|
|
216
|
+
return record.bivyKind === "fork-display" && Array.isArray(record.messages) && typeof record.forkedAt === "number";
|
|
217
|
+
}
|
|
212
218
|
function isAttachment(value) {
|
|
213
219
|
if (!value || typeof value !== "object")
|
|
214
220
|
return false;
|
|
@@ -255,7 +261,7 @@ function isAppReviewEntry(value) {
|
|
|
255
261
|
return entry.bivyKind === "app-review" && typeof entry.id === "string" && typeof entry.createdAt === "number" && typeof entry.afterMessageCount === "number" && isAppReview(entry.review);
|
|
256
262
|
}
|
|
257
263
|
function isRecord(value) {
|
|
258
|
-
return isOverlay(value) || isBase(value) || isAttachment(value) || isOutboundAttachment(value) || isInlineImage(value) || isAppPublication(value) || isAppReviewEntry(value) || isSuggestionEntry(value);
|
|
264
|
+
return isOverlay(value) || isBase(value) || isForkDisplay(value) || isAttachment(value) || isOutboundAttachment(value) || isInlineImage(value) || isAppPublication(value) || isAppReviewEntry(value) || isSuggestionEntry(value);
|
|
259
265
|
}
|
|
260
266
|
/**
|
|
261
267
|
* Fold attachment records into a text→refs list: last write wins per text (a
|
|
@@ -743,7 +749,23 @@ export class EventLog {
|
|
|
743
749
|
* the display overlays — what a fork carries to another agent. */
|
|
744
750
|
deriveBase(id, runtimeBase) {
|
|
745
751
|
const logged = this.readBase(id);
|
|
746
|
-
|
|
752
|
+
const base = runtimeBase && runtimeBase.length ? mergeBases(logged, runtimeBase) : logged;
|
|
753
|
+
const display = this.entries(id).find(isForkDisplay);
|
|
754
|
+
if (!display)
|
|
755
|
+
return base;
|
|
756
|
+
const replayed = (m) => {
|
|
757
|
+
const ts = m.timestamp;
|
|
758
|
+
return typeof ts === "number" && ts <= display.forkedAt;
|
|
759
|
+
};
|
|
760
|
+
return [...display.messages, ...base.filter((m) => !replayed(m))];
|
|
761
|
+
}
|
|
762
|
+
/** Record a fork's source transcript for display (see ForkDisplayLogEntry). */
|
|
763
|
+
appendForkDisplay(id, messages, forkedAt) {
|
|
764
|
+
if (!messages.length)
|
|
765
|
+
return;
|
|
766
|
+
this.load(id);
|
|
767
|
+
const record = { bivyKind: "fork-display", createdAt: Date.now(), forkedAt, messages: JSON.parse(JSON.stringify(messages)) };
|
|
768
|
+
this.enqueue(id, this.syntheticKey(id), record);
|
|
747
769
|
}
|
|
748
770
|
/** Full ordered record list (already-flushed followed by pending). */
|
|
749
771
|
entries(id) {
|
|
@@ -217,6 +217,11 @@ export function createForkStandUp(deps) {
|
|
|
217
217
|
const record = plan.kind === "resume"
|
|
218
218
|
? await deps.createSession(cwd, plan.sessionFile, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false, newSession: true })
|
|
219
219
|
: await deps.createSession(cwd, undefined, { runtimeId: targetRuntimeId, source: bundle.record.source, sandbox: forkSandbox, makeActive: false, newSession: true });
|
|
220
|
+
// A replayed or seeded destination resumes from a text rendering of the
|
|
221
|
+
// conversation. Show the source's own transcript (tool cards, sub-agent
|
|
222
|
+
// nesting) in its place; everything the fork does from now on follows it.
|
|
223
|
+
if (plan.fidelity !== "full" && bundle.display?.length)
|
|
224
|
+
deps.recordForkDisplay(record, bundle.display, Date.now());
|
|
220
225
|
// Mark the new session as a fork of its source, so the run card can show
|
|
221
226
|
// "Forked from …" and the lineage survives a reload (persisted below).
|
|
222
227
|
record.forkedFrom = bundle.record.sourceSessionId;
|
package/dist/session/fork.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { normalizeMessages, buildSeedPrompt, buildForkHistory, } from "./transcript-normal.js";
|
|
2
|
+
/** Cap on the display transcript a bundle carries; larger sessions fall back to
|
|
3
|
+
* showing the destination agent's own (text) replay. */
|
|
4
|
+
export const FORK_DISPLAY_MAX_BYTES = 2 * 1024 * 1024;
|
|
2
5
|
/**
|
|
3
6
|
* Capture a fork bundle on the source node. Always includes the normalized
|
|
4
7
|
* transcript (the cross-runtime seed, and the fallback when a same-runtime
|
|
@@ -49,7 +52,8 @@ export function buildForkBundle(opts) {
|
|
|
49
52
|
// transcript below still supports replay or a seeded continuation.
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
|
-
|
|
55
|
+
const display = opts.display?.length && JSON.stringify(opts.display).length <= FORK_DISPLAY_MAX_BYTES ? [...opts.display] : undefined;
|
|
56
|
+
return { record, normalized, ...(native ? { native } : {}), ...(display ? { display } : {}), ...(opts.dirtyPatch ? { dirtyPatch: opts.dirtyPatch } : {}), ...(opts.workspaceSnapshot ? { workspaceSnapshot: opts.workspaceSnapshot } : {}), ...(opts.state ? { state: opts.state } : {}) };
|
|
53
57
|
}
|
|
54
58
|
/**
|
|
55
59
|
* Decide the best fidelity a fork of `bundle` into `targetRuntime` can achieve:
|
package/package.json
CHANGED