@pi-unipi/fusion 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,301 @@
1
+ import { spawn as defaultSpawn, type ChildProcess } from "node:child_process";
2
+ import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { getPiSpawnCommand } from "@pi-unipi/subagents/src/pi-spawn.js";
7
+ import type { EffortLevel, ModelKey } from "./preset.js";
8
+
9
+ export interface SidekickSpawnConfig {
10
+ cwd: string;
11
+ model: ModelKey;
12
+ thinking: EffortLevel;
13
+ sessionFile: string;
14
+ systemPrompt: string;
15
+ spawn?: typeof defaultSpawn;
16
+ command?: { command: string; args: string[] };
17
+ }
18
+
19
+ export interface SidekickUsage {
20
+ input: number;
21
+ output: number;
22
+ cacheRead: number;
23
+ cacheWrite: number;
24
+ cost: number;
25
+ }
26
+
27
+ export interface HandoffProgress {
28
+ toolCalls: number;
29
+ recentTools: string[];
30
+ textTail: string;
31
+ startedAt: number;
32
+ }
33
+
34
+ export interface HandoffReport {
35
+ id: string;
36
+ status: "completed" | "aborted" | "error" | "interrupted";
37
+ text: string;
38
+ usage: SidekickUsage;
39
+ toolCalls: number;
40
+ durationMs: number;
41
+ error?: string;
42
+ }
43
+
44
+ interface PendingHandoff {
45
+ id: string;
46
+ startedAt: number;
47
+ usage: SidekickUsage;
48
+ progress: HandoffProgress;
49
+ resolve: (report: HandoffReport) => void;
50
+ reject: (error: Error) => void;
51
+ }
52
+
53
+ const emptyUsage = (): SidekickUsage => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
54
+
55
+ export class SidekickRuntime {
56
+ private readonly cfg: SidekickSpawnConfig;
57
+ private child: ChildProcess | undefined;
58
+ private promptPath: string | undefined;
59
+ private pending: PendingHandoff | undefined;
60
+ private latestHandoff: { id: string; done: Promise<HandoffReport>; report?: HandoffReport } | undefined;
61
+ private responseText: ((text: string) => void) | undefined;
62
+ private responseError: ((error: Error) => void) | undefined;
63
+ private stderrTail = "";
64
+ private inputBuffer = "";
65
+ private abortRequested = false;
66
+ private pendingError: string | undefined;
67
+ readonly reports = new Map<string, HandoffReport>();
68
+ readonly usage = emptyUsage();
69
+
70
+ constructor(cfg: SidekickSpawnConfig) {
71
+ this.cfg = cfg;
72
+ }
73
+
74
+ isAlive(): boolean {
75
+ return this.child !== undefined && (this.child.exitCode === null || this.child.exitCode === undefined) && !this.child.killed;
76
+ }
77
+
78
+ isBusy(): boolean {
79
+ return this.pending !== undefined;
80
+ }
81
+
82
+ private send(value: Record<string, unknown>): void {
83
+ if (!this.child?.stdin?.writable) throw new Error("Sidekick process is not writable");
84
+ this.child.stdin.write(`${JSON.stringify(value)}\n`);
85
+ }
86
+
87
+ private cleanupPrompt(): void {
88
+ if (!this.promptPath) return;
89
+ try {
90
+ unlinkSync(this.promptPath);
91
+ } catch {
92
+ /* already removed */
93
+ }
94
+ this.promptPath = undefined;
95
+ }
96
+
97
+ private spawn(): void {
98
+ mkdirSync(dirname(this.cfg.sessionFile), { recursive: true });
99
+ this.promptPath = join(tmpdir(), `unipi-fusion-${randomUUID()}.txt`);
100
+ writeFileSync(this.promptPath, this.cfg.systemPrompt, "utf8");
101
+ const command = this.cfg.command ?? getPiSpawnCommand([
102
+ "--mode", "rpc",
103
+ "--session", this.cfg.sessionFile,
104
+ "--model", this.cfg.model,
105
+ "--thinking", this.cfg.thinking,
106
+ "--append-system-prompt", this.promptPath,
107
+ "--no-skills",
108
+ ]);
109
+ const spawn = this.cfg.spawn ?? defaultSpawn;
110
+ const child = spawn(command.command, command.args, {
111
+ cwd: this.cfg.cwd,
112
+ env: { ...process.env, UNIPI_FUSION_CHILD: "1", UNIPI_SUBAGENT_CHILD: "1" },
113
+ stdio: ["pipe", "pipe", "pipe"],
114
+ });
115
+ this.child = child;
116
+ child.stdout?.on("data", (data: Buffer | string) => this.readStdout(String(data)));
117
+ child.stderr?.on("data", (data: Buffer | string) => {
118
+ this.stderrTail = `${this.stderrTail}${String(data)}`.slice(-2048);
119
+ });
120
+ child.on("close", () => {
121
+ this.cleanupPrompt();
122
+ if (this.pending !== undefined) this.finish("error", undefined, this.stderrTail || "Sidekick process exited");
123
+ });
124
+ child.on("error", (error) => {
125
+ if (this.pending !== undefined) this.finish("error", undefined, error.message);
126
+ });
127
+ }
128
+
129
+ private readStdout(data: string): void {
130
+ this.inputBuffer += data;
131
+ const lines = this.inputBuffer.split("\n");
132
+ this.inputBuffer = lines.pop() ?? "";
133
+ for (const raw of lines) {
134
+ const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
135
+ if (line.length === 0) continue;
136
+ try {
137
+ this.handleMessage(JSON.parse(line) as Record<string, unknown>);
138
+ } catch {
139
+ continue;
140
+ }
141
+ }
142
+ }
143
+
144
+ private handleMessage(message: Record<string, unknown>): void {
145
+ if (message.type === "response") {
146
+ const command = message.command;
147
+ if (command === "prompt" && message.success === false) {
148
+ const error = new Error(String(message.error ?? "Sidekick prompt rejected"));
149
+ const current = this.pending;
150
+ this.pending = undefined;
151
+ this.responseError = undefined;
152
+ this.responseText = undefined;
153
+ current?.reject(error);
154
+ } else if (command === "get_last_assistant_text") {
155
+ const data = message.data as Record<string, unknown> | undefined;
156
+ const text = typeof data?.text === "string" ? data.text : this.pending?.progress.textTail ?? "";
157
+ this.responseText?.(text);
158
+ this.responseText = undefined;
159
+ this.responseError = undefined;
160
+ }
161
+ return;
162
+ }
163
+ if (message.type === "extension_ui_request") {
164
+ const method = message.method;
165
+ if (method === "select" || method === "confirm" || method === "input" || method === "editor") {
166
+ this.send({ type: "extension_ui_response", id: message.id, cancelled: true });
167
+ }
168
+ return;
169
+ }
170
+ if (this.pending === undefined) return;
171
+ if (message.type === "tool_execution_start") {
172
+ this.pending.progress.toolCalls += 1;
173
+ const args = message.args === undefined ? "" : JSON.stringify(message.args).replace(/\s+/gu, " ");
174
+ const summary = `${String(message.toolName ?? "tool")}(${args})`.slice(0, 40);
175
+ this.pending.progress.recentTools = [...this.pending.progress.recentTools, summary].slice(-6);
176
+ } else if (message.type === "message_update") {
177
+ const event = (message.assistantMessageEvent ?? message) as Record<string, unknown>;
178
+ if (event.type === "text_delta") {
179
+ const delta = typeof event.delta === "string" ? event.delta : typeof event.text === "string" ? event.text : "";
180
+ this.pending.progress.textTail = `${this.pending.progress.textTail}${delta}`.slice(-400);
181
+ }
182
+ } else if (message.type === "message_end") {
183
+ const msg = message.message as Record<string, unknown> | undefined;
184
+ if (msg?.role === "assistant") {
185
+ if (msg.stopReason === "error" && typeof msg.errorMessage === "string") this.pendingError = msg.errorMessage;
186
+ const usage = msg.usage as Record<string, unknown> | undefined;
187
+ if (usage) {
188
+ const cost = usage.cost as Record<string, unknown> | undefined;
189
+ this.addUsage(this.pending.usage, {
190
+ input: usage.input,
191
+ output: usage.output,
192
+ cacheRead: usage.cacheRead,
193
+ cacheWrite: usage.cacheWrite,
194
+ cost: cost?.total,
195
+ });
196
+ }
197
+ }
198
+ } else if (message.type === "agent_settled") {
199
+ this.responseText = undefined;
200
+ this.responseError = undefined;
201
+ this.responseText = (text) => this.finish(this.abortRequested ? "aborted" : this.pendingError === undefined ? "completed" : "error", text, this.pendingError);
202
+ this.responseError = (error) => this.finish("error", undefined, error.message);
203
+ try {
204
+ this.send({ type: "get_last_assistant_text" });
205
+ } catch (error) {
206
+ this.finish("error", undefined, error instanceof Error ? error.message : String(error));
207
+ }
208
+ }
209
+ }
210
+
211
+ private addUsage(target: SidekickUsage, raw: Record<string, unknown>): void {
212
+ const number = (value: unknown) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
213
+ target.input += number(raw.input);
214
+ target.output += number(raw.output);
215
+ target.cacheRead += number(raw.cacheRead);
216
+ target.cacheWrite += number(raw.cacheWrite);
217
+ target.cost += number(raw.cost);
218
+ if (target !== this.usage) this.addUsage(this.usage, raw);
219
+ }
220
+
221
+ private finish(status: HandoffReport["status"], text?: string, error?: string): void {
222
+ const current = this.pending;
223
+ if (current === undefined) return;
224
+ this.pending = undefined;
225
+ this.responseText = undefined;
226
+ this.responseError = undefined;
227
+ const report: HandoffReport = {
228
+ id: current.id,
229
+ status,
230
+ text: text ?? current.progress.textTail,
231
+ usage: { ...current.usage },
232
+ toolCalls: current.progress.toolCalls,
233
+ durationMs: Date.now() - current.startedAt,
234
+ ...(error === undefined ? {} : { error }),
235
+ };
236
+ this.reports.set(report.id, report);
237
+ if (this.latestHandoff?.id === report.id) this.latestHandoff.report = report;
238
+ current.resolve(report);
239
+ this.abortRequested = false;
240
+ this.pendingError = undefined;
241
+ }
242
+
243
+ handoff(message: string): { id: string; done: Promise<HandoffReport> } {
244
+ if (this.pending !== undefined) {
245
+ this.send({ type: "steer", message });
246
+ return { id: this.pending.id, done: this.latestHandoff?.done ?? Promise.reject(new Error("Missing handoff")) };
247
+ }
248
+ if (!this.isAlive()) this.spawn();
249
+ const id = randomUUID();
250
+ const startedAt = Date.now();
251
+ let resolve!: (report: HandoffReport) => void;
252
+ let reject!: (error: Error) => void;
253
+ const done = new Promise<HandoffReport>((res, rej) => {
254
+ resolve = res;
255
+ reject = rej;
256
+ });
257
+ this.pendingError = undefined;
258
+ this.pending = {
259
+ id,
260
+ startedAt,
261
+ usage: emptyUsage(),
262
+ progress: { toolCalls: 0, recentTools: [], textTail: "", startedAt },
263
+ resolve,
264
+ reject,
265
+ };
266
+ this.latestHandoff = { id, done };
267
+ try {
268
+ this.send({ id, type: "prompt", message });
269
+ } catch (error) {
270
+ this.finish("error", undefined, error instanceof Error ? error.message : String(error));
271
+ }
272
+ return { id, done };
273
+ }
274
+
275
+ progress(id?: string): HandoffProgress | undefined {
276
+ if (this.pending !== undefined && (id === undefined || id === this.pending.id)) return { ...this.pending.progress, recentTools: [...this.pending.progress.recentTools] };
277
+ return undefined;
278
+ }
279
+
280
+ latest(): { id: string; done: Promise<HandoffReport>; report?: HandoffReport } | undefined {
281
+ return this.latestHandoff;
282
+ }
283
+
284
+ async abort(): Promise<void> {
285
+ if (!this.pending) return;
286
+ this.abortRequested = true;
287
+ this.send({ type: "abort" });
288
+ }
289
+
290
+ kill(): void {
291
+ const child = this.child;
292
+ if (!child) return;
293
+ this.cleanupPrompt();
294
+ child.kill("SIGTERM");
295
+ const timer = setTimeout(() => {
296
+ if (child.exitCode === null || child.exitCode === undefined) child.kill("SIGKILL");
297
+ }, 2000);
298
+ timer.unref();
299
+ this.child = undefined;
300
+ }
301
+ }
package/src/slider.ts ADDED
@@ -0,0 +1,37 @@
1
+ export interface SliderCost {
2
+ input: number;
3
+ output: number;
4
+ }
5
+
6
+ export function blendedPrice(cost: SliderCost | undefined): number | undefined {
7
+ return cost === undefined ? undefined : (cost.input + cost.output) * 0.5;
8
+ }
9
+
10
+ export function sliderPosition(price: number, min: number, max: number, cells: number): number {
11
+ if (cells <= 1 || max <= min) return 0;
12
+ const value = Math.log10(Math.max(price, Number.EPSILON));
13
+ const low = Math.log10(Math.max(min, Number.EPSILON));
14
+ const high = Math.log10(Math.max(max, Number.EPSILON));
15
+ return Math.max(0, Math.min(cells - 1, Math.round(((value - low) / (high - low)) * (cells - 1))));
16
+ }
17
+
18
+ function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
19
+ const c = v * s;
20
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
21
+ const m = v - c;
22
+ const [r, g, b] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x];
23
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
24
+ }
25
+
26
+ export function renderSlider(cells: number, markerIndex: number | undefined): string {
27
+ const count = Math.max(0, cells);
28
+ let out = "";
29
+ for (let i = 0; i < count; i++) {
30
+ if (i === markerIndex) out += "\x1b[97m●";
31
+ else {
32
+ const [r, g, b] = hsvToRgb(count <= 1 ? 0 : (270 * i) / (count - 1), 0.85, 0.95);
33
+ out += `\x1b[38;2;${String(r)};${String(g)};${String(b)}m━`;
34
+ }
35
+ }
36
+ return `${out}\x1b[39m`;
37
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,163 @@
1
+ import { Box, Text } from "@earendil-works/pi-tui";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import type { SidekickRuntime, HandoffReport } from "./sidekick-runtime.js";
5
+
6
+ const SidekickParams = Type.Object({
7
+ message: Type.String({ description: "A concrete implementation or verification brief for the sidekick" }),
8
+ block: Type.Optional(Type.Boolean({ description: "Wait for completion (default true)" })),
9
+ });
10
+ const ReadSubagentParams = Type.Object({
11
+ agent_id: Type.Optional(Type.String({ description: "Handoff id; omit to use the latest handoff" })),
12
+ block: Type.Optional(Type.Boolean({ description: "Wait for completion" })),
13
+ timeout: Type.Optional(Type.Number({ description: "Maximum wait in seconds" })),
14
+ });
15
+
16
+ export interface FusionToolDeps {
17
+ getRuntime: (ctx: ExtensionContext) => SidekickRuntime | undefined;
18
+ onReport?: (ctx: ExtensionContext, report: HandoffReport) => void;
19
+ }
20
+
21
+ function duration(ms: number): string {
22
+ return `${(ms / 1000).toFixed(1)}s`;
23
+ }
24
+
25
+ function progressText(runtime: SidekickRuntime, id: string): string {
26
+ const progress = runtime.progress(id);
27
+ if (!progress) return "No active handoff progress.";
28
+ const elapsed = duration(Date.now() - progress.startedAt);
29
+ const tools = progress.recentTools.length > 0 ? `\n${progress.recentTools.map((tool) => ` ${tool}`).join("\n")}` : "";
30
+ const tail = progress.textTail.length > 0 ? `\n ${progress.textTail}` : "";
31
+ return `◆ sidekick working · ${String(progress.toolCalls)} tool calls · ${elapsed}${tools}${tail}`;
32
+ }
33
+
34
+ function progressKey(runtime: SidekickRuntime, id: string): string {
35
+ const progress = runtime.progress(id);
36
+ return progress === undefined ? "" : `${String(progress.toolCalls)}|${progress.recentTools.join("|")}|${progress.textTail}`;
37
+ }
38
+
39
+ function reportText(report: HandoffReport): string {
40
+ return `${report.text}\n\n--- sidekick ${report.id} · ${report.status} · ${String(report.toolCalls)} tool calls · ${duration(report.durationMs)} · in ${String(report.usage.input)} / out ${String(report.usage.output)} tokens`;
41
+ }
42
+
43
+ function result(text: string, details?: unknown, isError = false): { content: Array<{ type: "text"; text: string }>; details: unknown; isError: boolean } {
44
+ return { content: [{ type: "text", text }], details: details ?? {}, isError };
45
+ }
46
+
47
+ async function waitForReport(
48
+ runtime: SidekickRuntime,
49
+ id: string,
50
+ done: Promise<HandoffReport>,
51
+ signal: AbortSignal | undefined,
52
+ ctx: ExtensionContext,
53
+ onUpdate?: (update: unknown) => void,
54
+ timeoutMs = 2700000,
55
+ ): Promise<{ report?: HandoffReport; interrupted?: string; aborted?: boolean }> {
56
+ const started = Date.now();
57
+ let lastProgressKey = "";
58
+ while (true) {
59
+ if (signal?.aborted) {
60
+ await runtime.abort();
61
+ return { aborted: true };
62
+ }
63
+ if (ctx.hasPendingMessages?.()) return { interrupted: progressText(runtime, id) };
64
+ const remaining = timeoutMs - (Date.now() - started);
65
+ if (remaining <= 0) return {};
66
+ const timer = new Promise<undefined>((resolve) => setTimeout(() => resolve(undefined), Math.min(500, remaining)));
67
+ const report = await Promise.race([done, timer]);
68
+ if (report !== undefined) return { report };
69
+ const progress = progressText(runtime, id);
70
+ const key = progressKey(runtime, id);
71
+ if (key !== lastProgressKey) {
72
+ lastProgressKey = key;
73
+ onUpdate?.({ content: [{ type: "text", text: progress }] });
74
+ }
75
+ }
76
+ }
77
+
78
+ function completionMessage(report: HandoffReport): { customType: string; content: string; display: boolean; details: HandoffReport } {
79
+ return {
80
+ customType: "sidekick-completion",
81
+ content: `<subagent_completion_notification agent_id="${report.id}" status="${report.status}">\n${report.text}\n</subagent_completion_notification>`,
82
+ display: true,
83
+ details: report,
84
+ };
85
+ }
86
+
87
+ type ThemeLike = {
88
+ fg: (color: string, text: string) => string;
89
+ bg: (color: string, text: string) => string;
90
+ bold: (text: string) => string;
91
+ };
92
+
93
+ function renderCompletionCard(theme: ThemeLike, report: HandoffReport | undefined): Box {
94
+ const tone = report?.status === "completed" ? "toolSuccessBg" : "toolErrorBg";
95
+ const head = report
96
+ ? `${theme.fg(report.status === "completed" ? "success" : "error", "◆")} ${theme.fg("accent", theme.bold(`sidekick done · ${report.id}`))} ${theme.fg("dim", `· ${String(report.toolCalls)} tool calls · ${duration(report.durationMs)}`)}`
97
+ : `${theme.fg("accent", "◆")} ${theme.fg("accent", theme.bold("sidekick done"))}`;
98
+ const lines = [head, ...(report?.text.split("\n").slice(0, 6).map((line) => theme.fg("dim", line)) ?? [])];
99
+ const box = new Box(1, 0, (text) => theme.bg(tone, text));
100
+ box.addChild(new Text(lines.join("\n"), 0, 0));
101
+ return box;
102
+ }
103
+
104
+ export function registerFusionTools(pi: ExtensionAPI, deps: FusionToolDeps): void {
105
+ pi.registerMessageRenderer("sidekick-completion", (message: { details?: HandoffReport }, _options, theme) => renderCompletionCard(theme as unknown as ThemeLike, message.details));
106
+
107
+ pi.registerTool({
108
+ name: "sidekick",
109
+ label: "Sidekick",
110
+ description: "Hand off work to your persistent sidekick subagent (one per session; context and shells persist across handoffs; runs on the same machine). block:true (default) waits and returns the report. block:false returns immediately and the report arrives later as a <subagent_completion_notification>. Calling again while a handoff is running injects the message as an interrupt rather than starting a second sidekick.",
111
+ parameters: SidekickParams,
112
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
113
+ const runtime = deps.getRuntime(ctx);
114
+ if (!runtime) return result("Fusion is not active — pick a Fusion pair with /unipi:model.", undefined, true);
115
+ const handoff = runtime.handoff(params.message);
116
+ if (params.block === false) {
117
+ void handoff.done.then((report) => {
118
+ deps.onReport?.(ctx, report);
119
+ pi.sendMessage(completionMessage(report) as never, { deliverAs: "followUp", triggerTurn: true } as never);
120
+ }).catch(() => undefined);
121
+ return result(`Handoff ${handoff.id} started in the background. You will receive a <subagent_completion_notification agent_id="${handoff.id}"> when it finishes; use read_subagent to wait.`);
122
+ }
123
+ const waited = await waitForReport(runtime, handoff.id, handoff.done, signal, ctx, onUpdate ? (update) => onUpdate(update as never) : undefined);
124
+ if (waited.report) {
125
+ deps.onReport?.(ctx, waited.report);
126
+ return result(reportText(waited.report), waited.report, waited.report.status !== "completed");
127
+ }
128
+ if (waited.aborted) return result(`${progressText(runtime, handoff.id)}\nHandoff ${handoff.id} aborted.`, undefined, true);
129
+ if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${handoff.id}) was working. The handoff continues in the background. Act on the user's message first, then call read_subagent({agent_id:"${handoff.id}", block:true}) to collect the report or sidekick({message}) to redirect it.\n${waited.interrupted}`);
130
+ return result(`Handoff ${handoff.id} is still running.\n${progressText(runtime, handoff.id)}`);
131
+ },
132
+ });
133
+
134
+ pi.registerTool({
135
+ name: "read_subagent",
136
+ label: "Read Sidekick",
137
+ description: "Read a sidekick handoff report by agent_id (omit for the latest). block:true waits for completion (default timeout 2700s when omitted); block:false returns the current progress snapshot immediately.",
138
+ parameters: ReadSubagentParams,
139
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
140
+ const runtime = deps.getRuntime(ctx);
141
+ if (!runtime) return result("Fusion is not active — pick a Fusion pair with /unipi:model.", undefined, true);
142
+ const latest = runtime.latest();
143
+ if (!latest) return result("No sidekick handoff has run yet.", undefined, true);
144
+ const id = params.agent_id ?? latest.id;
145
+ const selected = runtime.reports.get(id);
146
+ if (selected) return result(reportText(selected), selected, selected.status !== "completed");
147
+ if (id !== latest.id) return result(`No sidekick handoff found for ${id}.`, undefined, true);
148
+ if (params.block !== true) return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`);
149
+ const timeoutMs = (params.timeout ?? 2700) * 1000;
150
+ const waited = await waitForReport(runtime, id, latest.done, signal, ctx, onUpdate ? (update) => onUpdate(update as never) : undefined, timeoutMs);
151
+ if (waited.report) {
152
+ deps.onReport?.(ctx, waited.report);
153
+ return result(reportText(waited.report), waited.report, waited.report.status !== "completed");
154
+ }
155
+ if (waited.aborted) return result(`Handoff ${id} aborted.`, undefined, true);
156
+ if (waited.interrupted) return result(`A user message arrived while the sidekick (agent_id ${id}) was working.\n${waited.interrupted}`);
157
+ return result(`Handoff ${id} is still running.\n${progressText(runtime, id)}`);
158
+ },
159
+ });
160
+
161
+ }
162
+
163
+ export { reportText, progressText };