@zachwill/pi-orchestrate 0.9.0 → 0.10.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.
@@ -1,771 +0,0 @@
1
- import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
- import type {
3
- ExtensionAPI,
4
- ExtensionContext,
5
- Theme,
6
- } from "@earendil-works/pi-coding-agent";
7
- import {
8
- Container,
9
- Markdown,
10
- Spacer,
11
- Text,
12
- truncateToWidth,
13
- wrapTextWithAnsi,
14
- type Component,
15
- } from "@earendil-works/pi-tui";
16
- import {
17
- formatSize,
18
- getAgentDir,
19
- getMarkdownTheme,
20
- keyHint,
21
- truncateHead,
22
- } from "@earendil-works/pi-coding-agent";
23
- import { Result, Schema } from "effect";
24
- import { Type } from "typebox";
25
- import {
26
- MAX_WORKER_INSTRUCTIONS_LENGTH,
27
- MAX_WORKER_TITLE_LENGTH,
28
- type CatalogDiagnostic,
29
- type RunRecord,
30
- type WorkerCatalog,
31
- type WorkerDefinition,
32
- type WorkerOutcome,
33
- type WorkerRecord,
34
- type WorkerUsage,
35
- } from "./domain.js";
36
- import type {
37
- AbortTarget,
38
- AcceptedRun,
39
- CompletedRun,
40
- OrchestrationContext,
41
- RunResult,
42
- RuntimeSnapshot,
43
- SettlementListener,
44
- } from "./runtime.js";
45
- import type { OrchestratorRuntime } from "./host.js";
46
- import {
47
- disposeComponent,
48
- formatElapsed,
49
- resultAppearance,
50
- WidthBoundComponent,
51
- } from "./tui.js";
52
- import {
53
- decodeInlineWorkerToolDetails,
54
- encodeInlineWorkerToolDetails,
55
- type InlineWorkerSettlementDetails,
56
- WorkerSettlementDetails,
57
- type WorkerSettlement,
58
- } from "./worker-settlement.js";
59
-
60
- const STRICT_OBJECT = { additionalProperties: false } as const;
61
- const MAX_INSTRUCTION_PREVIEW_LINES = 2;
62
- const shortTextSchema = Type.String({
63
- pattern: "\\S",
64
- maxLength: MAX_WORKER_TITLE_LENGTH,
65
- });
66
- const instructionsSchema = Type.String({
67
- pattern: "\\S",
68
- maxLength: MAX_WORKER_INSTRUCTIONS_LENGTH,
69
- });
70
- const workerIdSchema = Type.String({ pattern: "^worker-\\S+$" });
71
-
72
- const AcceptedRunRenderDetails = Schema.Struct({
73
- mode: Schema.Literal("async"),
74
- run_id: WorkerSettlementDetails.fields.runId,
75
- worker_id: WorkerSettlementDetails.fields.workerId,
76
- });
77
- const UnavailableWorkerRenderDetails = Schema.Union([
78
- Schema.Struct({ result: Schema.Unknown }),
79
- Schema.Struct({ worker_id: Schema.Unknown }),
80
- ]);
81
- const decodeAcceptedRunRenderDetails = Schema.decodeUnknownResult(
82
- AcceptedRunRenderDetails,
83
- );
84
- const decodeUnavailableWorkerRenderDetails = Schema.decodeUnknownResult(
85
- UnavailableWorkerRenderDetails,
86
- );
87
-
88
- const taskSchema = Type.Object(
89
- {
90
- worker: shortTextSchema,
91
- title: shortTextSchema,
92
- instructions: instructionsSchema,
93
- },
94
- STRICT_OBJECT,
95
- );
96
-
97
- const statusSchema = Type.Object({}, STRICT_OBJECT);
98
-
99
- const interactiveSendSchema = Type.Object(
100
- {
101
- worker_id: workerIdSchema,
102
- instructions: instructionsSchema,
103
- },
104
- STRICT_OBJECT,
105
- );
106
-
107
- const workerAbortSchema = Type.Union([
108
- Type.Object(
109
- {
110
- worker_ids: Type.Array(workerIdSchema, { minItems: 1 }),
111
- },
112
- STRICT_OBJECT,
113
- ),
114
- Type.Object(
115
- {
116
- all: Type.Literal(true),
117
- },
118
- STRICT_OBJECT,
119
- ),
120
- ]);
121
-
122
- const interactiveCloseSchema = Type.Object(
123
- {
124
- worker_id: workerIdSchema,
125
- },
126
- STRICT_OBJECT,
127
- );
128
-
129
- export interface DispatchDecision {
130
- readonly mode: "async" | "inline";
131
- readonly synthesisGroup?: {
132
- readonly id: string;
133
- readonly size: number;
134
- };
135
- }
136
-
137
- export interface OrchestrationToolDependencies {
138
- readonly runtime: OrchestratorRuntime;
139
- getCatalog(ctx: ExtensionContext): WorkerCatalog;
140
- getDispatchDecision(toolCallId: string): DispatchDecision;
141
- }
142
-
143
- export function registerOrchestrationTools(
144
- pi: ExtensionAPI,
145
- deps: OrchestrationToolDependencies,
146
- ): void {
147
- pi.registerTool({
148
- name: "orchestrate",
149
- label: "Orchestrate",
150
- description:
151
- "Dispatch fully briefed worker scopes. Pi executes native sibling tools concurrently; Pi Orchestrate treats a successfully admitted sole orchestrate call or pure sibling group as async. Mixing orchestrate with another tool makes it inline and blocking.",
152
- promptSnippet: "Dispatch fully briefed parallel worker scopes",
153
- promptGuidelines: [
154
- "Spin up as many workers as needed to cover every useful parallel scope and distinct validation perspective. Treat user-named workers or counts as a floor unless explicitly capped, and reuse the same worker role across multiple calls when useful.",
155
- "For an intended async wave of N workers, the next assistant response must contain exactly N separate, fully briefed orchestrate calls; one call is valid only when N=1. To run it asynchronously, include no other tool calls; harmless response text does not affect runtime classification.",
156
- "When a parallel tool dispatcher is available, use it once with exactly N orchestrate entries and no other tools; for example, put N functions.orchestrate entries in multi_tool_use.parallel. Otherwise emit N native sibling orchestrate calls in one assistant response.",
157
- "Form all N calls before emitting or finalizing the response. Never emit one call and wait for its result before forming the rest of the wave: a successfully admitted sole async orchestrate call returns terminate=true and ends the turn.",
158
- ],
159
- executionMode: "parallel",
160
- parameters: taskSchema,
161
- renderCall(args, theme, { expanded }) {
162
- return renderDispatchCall(theme, args, expanded);
163
- },
164
- renderResult(result, { isPartial, expanded }, theme, context) {
165
- return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
166
- },
167
- async execute(toolCallId, params, signal, onUpdate, ctx) {
168
- const decision = deps.getDispatchDecision(toolCallId);
169
- const mode = decision.mode;
170
- const runtimeContext = buildRuntimeContext(ctx, deps, decision.synthesisGroup);
171
- if (mode === "async") {
172
- const acceptedRun = await deps.runtime.orchestrate(
173
- runtimeContext,
174
- params,
175
- "async",
176
- signal,
177
- );
178
- const readable = acceptedRunDetails(acceptedRun);
179
- return {
180
- ...readableToolResult(`Accepted async run ${readable.run_id}.`, readable),
181
- terminate: true,
182
- };
183
- }
184
-
185
- const completedRun = await deps.runtime.orchestrate(
186
- runtimeContext,
187
- params,
188
- "inline",
189
- signal,
190
- createInlineSettlementListener(onUpdate),
191
- );
192
- const readable = completedRunDetails(completedRun);
193
- return readableToolResult(
194
- `Completed inline run ${readable.run_id}.`,
195
- readable,
196
- );
197
- },
198
- });
199
-
200
- pi.registerTool({
201
- name: "worker_status",
202
- label: "Worker Status",
203
- description:
204
- "Diagnostics and recovery only: inspect trusted catalog entries, catalog diagnostics, and this session's runtime state. Never poll for completion.",
205
- promptSnippet: "Inspect owned worker state for diagnostics or recovery",
206
- promptGuidelines: [
207
- "Use worker_status only for diagnostics or recovery; never poll it for completion.",
208
- ],
209
- parameters: statusSchema,
210
- renderCall(_args, theme) {
211
- return new Text(theme.fg("toolTitle", theme.bold("worker_status")), 0, 0);
212
- },
213
- renderResult(result, { isPartial }, theme) {
214
- return renderDiagnosticsResult(result, isPartial, theme);
215
- },
216
- async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
217
- const ownerSessionId = ctx.sessionManager.getSessionId();
218
- const catalog = deps.getCatalog(ctx);
219
- const snapshot = await deps.runtime.snapshot(ownerSessionId);
220
- const readable = statusDetails(catalog, snapshot);
221
- return readableToolResult(
222
- "Worker diagnostics and recovery snapshot.",
223
- readable,
224
- );
225
- },
226
- });
227
-
228
- pi.registerTool({
229
- name: "interactive_send",
230
- label: "Interactive Send",
231
- description:
232
- "Send follow-up instructions only to an owned lifecycle interactive worker whose status is ready. Never use for one-shot or completed workers; one-shot sessions terminate automatically. A sole tool call runs asynchronously; sibling tool calls make it inline and blocking.",
233
- promptSnippet: "Use only for an owned lifecycle interactive worker with status ready; never one-shot/completed because one-shot sessions terminate automatically",
234
- promptGuidelines: [
235
- "Use interactive_send only for an owned lifecycle interactive worker whose status is ready; never use it for one-shot or completed workers because one-shot sessions terminate automatically.",
236
- ],
237
- parameters: interactiveSendSchema,
238
- renderCall(args, theme, { expanded }) {
239
- return renderInteractiveMessageCall(theme, "interactive_send", args.worker_id, args.instructions, expanded);
240
- },
241
- renderResult(result, { isPartial, expanded }, theme, context) {
242
- return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
243
- },
244
- async execute(toolCallId, params, signal, onUpdate, ctx) {
245
- const workerId = params.worker_id;
246
- const mode = deps.getDispatchDecision(toolCallId).mode;
247
- const runtimeContext = buildRuntimeContext(ctx, deps);
248
- if (mode === "async") {
249
- const acceptedRun = await deps.runtime.sendInteractive(
250
- runtimeContext,
251
- workerId,
252
- params.instructions,
253
- "async",
254
- signal,
255
- );
256
- const readable = acceptedRunDetails(acceptedRun);
257
- return {
258
- ...readableToolResult(`Accepted async run ${readable.run_id}.`, readable),
259
- terminate: true,
260
- };
261
- }
262
-
263
- const completedRun = await deps.runtime.sendInteractive(
264
- runtimeContext,
265
- workerId,
266
- params.instructions,
267
- "inline",
268
- signal,
269
- createInlineSettlementListener(onUpdate),
270
- );
271
- const readable = completedRunDetails(completedRun);
272
- return readableToolResult(
273
- `Completed inline run ${readable.run_id}.`,
274
- readable,
275
- );
276
- },
277
- });
278
-
279
- pi.registerTool({
280
- name: "worker_abort",
281
- label: "Worker Abort",
282
- description:
283
- "Abort owned active work by worker IDs or all active owned workers. Use interactive_close for owned lifecycle interactive workers whose status is ready.",
284
- promptSnippet: "Abort active owned workers by worker IDs or all",
285
- promptGuidelines: [
286
- "Use worker_abort only for active work; use interactive_close only for an owned lifecycle interactive worker whose status is ready, never for one-shot or completed workers because one-shot sessions terminate automatically.",
287
- ],
288
- parameters: workerAbortSchema,
289
- renderCall(args, theme) {
290
- const target = "worker_ids" in args
291
- ? `${args.worker_ids.length} worker${args.worker_ids.length === 1 ? "" : "s"}`
292
- : "all workers";
293
- return renderCompactCall(theme, "worker_abort", target);
294
- },
295
- renderResult(result, { isPartial }, theme) {
296
- return renderSimpleResult(result, isPartial ? "Requesting worker stop…" : "Worker stop requested", theme, "warning");
297
- },
298
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
299
- const ownerSessionId = ctx.sessionManager.getSessionId();
300
- const target = normalizeAbortTarget(params);
301
- await deps.runtime.abort(ownerSessionId, target);
302
- const readable = {
303
- target: "worker_ids" in params
304
- ? { worker_ids: params.worker_ids }
305
- : { all: params.all },
306
- };
307
- return readableToolResult("Abort request completed.", readable);
308
- },
309
- });
310
-
311
- pi.registerTool({
312
- name: "interactive_close",
313
- label: "Interactive Close",
314
- description: "Close only an owned lifecycle interactive worker whose status is ready. Never use for one-shot or completed workers; one-shot sessions terminate automatically.",
315
- promptSnippet: "Use only for an owned lifecycle interactive worker with status ready; never one-shot/completed because one-shot sessions terminate automatically",
316
- promptGuidelines: [
317
- "Use interactive_close only for an owned lifecycle interactive worker whose status is ready; never use it for one-shot or completed workers because one-shot sessions terminate automatically.",
318
- ],
319
- parameters: interactiveCloseSchema,
320
- renderCall(args, theme) {
321
- return renderCompactCall(theme, "interactive_close", args.worker_id);
322
- },
323
- renderResult(result, { isPartial }, theme) {
324
- return renderSimpleResult(result, isPartial ? "Closing worker…" : "✓ Worker closed", theme);
325
- },
326
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
327
- const ownerSessionId = ctx.sessionManager.getSessionId();
328
- const workerId = params.worker_id;
329
- await deps.runtime.closeInteractive(ownerSessionId, workerId);
330
- const readable = { worker_id: workerId };
331
- return readableToolResult(`Closed worker ${workerId}.`, readable);
332
- },
333
- });
334
- }
335
-
336
- function buildRuntimeContext(
337
- ctx: ExtensionContext,
338
- deps: OrchestrationToolDependencies,
339
- synthesisGroup?: DispatchDecision["synthesisGroup"],
340
- ): OrchestrationContext {
341
- return {
342
- ownerSessionId: ctx.sessionManager.getSessionId(),
343
- cwd: ctx.cwd,
344
- agentDir: getAgentDir(),
345
- parentSessionFile: ctx.sessionManager.getSessionFile(),
346
- projectTrusted: ctx.isProjectTrusted(),
347
- catalog: deps.getCatalog(ctx),
348
- parentModel: ctx.model,
349
- modelRegistry: ctx.modelRegistry,
350
- ...(synthesisGroup ? { synthesisGroup } : {}),
351
- };
352
- }
353
-
354
- function createInlineSettlementListener(
355
- onUpdate: ((result: AgentToolResult<unknown>) => void) | undefined,
356
- ): SettlementListener {
357
- return (settlement) => {
358
- onUpdate?.({
359
- content: [{ type: "text", text: "Worker response received." }],
360
- details: encodeInlineWorkerToolDetails({
361
- mode: "inline",
362
- result: inlineResultValue(settlement),
363
- }),
364
- });
365
- };
366
- }
367
-
368
- function normalizeAbortTarget(params: {
369
- worker_ids?: string[];
370
- all?: boolean;
371
- }): AbortTarget {
372
- return {
373
- ...(params.worker_ids !== undefined ? { workerIds: params.worker_ids } : {}),
374
- ...(params.all !== undefined ? { all: params.all } : {}),
375
- };
376
- }
377
-
378
- function acceptedRunDetails(run: AcceptedRun) {
379
- return {
380
- mode: "async" as const,
381
- run_id: run.id,
382
- worker_id: run.workerId,
383
- };
384
- }
385
-
386
- function completedRunDetails(run: CompletedRun) {
387
- return encodeInlineWorkerToolDetails({
388
- mode: "inline",
389
- runId: run.id,
390
- ownerSessionId: run.ownerSessionId,
391
- result: inlineResultValue(run.result),
392
- });
393
- }
394
-
395
- function inlineResultValue(
396
- result: RunResult | WorkerSettlement,
397
- ): InlineWorkerSettlementDetails {
398
- return {
399
- workerId: result.workerId,
400
- worker: result.worker,
401
- title: result.title,
402
- status: result.status,
403
- outcome: result.outcome,
404
- usage: result.usage,
405
- startedAt: result.startedAt,
406
- settledAt: result.settledAt,
407
- ...(result.sessionFile === undefined
408
- ? {}
409
- : { sessionFile: result.sessionFile }),
410
- };
411
- }
412
-
413
- function statusDetails(catalog: WorkerCatalog, snapshot: RuntimeSnapshot) {
414
- return {
415
- catalog: {
416
- workers: catalog.workers.map(catalogWorkerDetails),
417
- diagnostics: catalog.diagnostics.map(diagnosticDetails),
418
- },
419
- state: {
420
- runs: snapshot.runs.map(runDetails),
421
- workers: snapshot.workers.map(workerDetails),
422
- },
423
- };
424
- }
425
-
426
- function catalogWorkerDetails(worker: WorkerDefinition) {
427
- return {
428
- name: worker.name,
429
- description: worker.description,
430
- lifecycle: worker.lifecycle,
431
- source: {
432
- kind: worker.source.kind,
433
- file_path: worker.source.filePath,
434
- },
435
- tools: [...worker.tools],
436
- skills: worker.skills === undefined ? undefined : [...worker.skills],
437
- model: worker.model
438
- ? { provider: worker.model.provider, model_id: worker.model.modelId }
439
- : undefined,
440
- thinking: worker.thinking,
441
- compaction: worker.compaction
442
- ? {
443
- enabled: worker.compaction.enabled,
444
- reserve_tokens: worker.compaction.reserveTokens,
445
- keep_recent_tokens: worker.compaction.keepRecentTokens,
446
- }
447
- : undefined,
448
- };
449
- }
450
-
451
- function diagnosticDetails(diagnostic: CatalogDiagnostic) {
452
- return {
453
- severity: diagnostic.severity,
454
- source: diagnostic.source,
455
- message: diagnostic.message,
456
- file_path: diagnostic.filePath,
457
- };
458
- }
459
-
460
- function runDetails(run: RunRecord) {
461
- return {
462
- run_id: run.id,
463
- owner_session_id: run.ownerSessionId,
464
- worker_id: run.workerId,
465
- mode: run.mode,
466
- state: run.state,
467
- created_at: run.createdAt,
468
- };
469
- }
470
-
471
- function workerDetails(worker: WorkerRecord) {
472
- return {
473
- worker_id: worker.id,
474
- worker: worker.worker,
475
- owner_session_id: worker.ownerSessionId,
476
- run_id: worker.runId,
477
- title: worker.title,
478
- lifecycle: worker.lifecycle,
479
- status: worker.status,
480
- activity: worker.activity,
481
- usage: usageDetails(worker.usage),
482
- outcome: worker.outcome ? outcomeDetails(worker.outcome) : undefined,
483
- session_file: worker.sessionFile,
484
- };
485
- }
486
-
487
- function usageDetails(usage: WorkerUsage) {
488
- return {
489
- input: usage.input,
490
- output: usage.output,
491
- cache_read: usage.cacheRead,
492
- cache_write: usage.cacheWrite,
493
- cost: usage.cost,
494
- context_tokens: usage.contextTokens,
495
- turns: usage.turns,
496
- };
497
- }
498
-
499
- function outcomeDetails(outcome: WorkerOutcome) {
500
- switch (outcome.status) {
501
- case "completed":
502
- case "ready":
503
- return {
504
- status: outcome.status,
505
- assistant_text: outcome.assistantText,
506
- };
507
- case "failed":
508
- case "aborted":
509
- return {
510
- status: outcome.status,
511
- message: outcome.message,
512
- assistant_text: outcome.assistantText,
513
- };
514
- case "closed":
515
- return { status: outcome.status };
516
- }
517
- }
518
-
519
- function readableToolResult<T>(title: string, details: T) {
520
- return {
521
- content: [{ type: "text" as const, text: readableDetails(title, details) }],
522
- details,
523
- };
524
- }
525
-
526
- function readableDetails(title: string, details: unknown): string {
527
- const content = `${title}\n\n${JSON.stringify(details, null, 2)}`;
528
- const truncation = truncateHead(content);
529
- if (!truncation.truncated) return content;
530
-
531
- return `${truncation.content}\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}. Full structured details remain available.]`;
532
- }
533
-
534
- interface RenderableTask {
535
- readonly worker?: unknown;
536
- readonly title?: unknown;
537
- readonly instructions?: unknown;
538
- }
539
-
540
- function renderDispatchCall(
541
- theme: Theme,
542
- task: RenderableTask,
543
- expanded: boolean,
544
- ): Component {
545
- const container = new Container();
546
- container.addChild(new Text(
547
- theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", safeTerminalText(task.worker)),
548
- 0, 0,
549
- ));
550
- container.addChild(new Text(
551
- `${theme.fg("accent", "→")} ${theme.fg("text", theme.bold(safeTerminalText(task.title)))}`,
552
- 0, 0,
553
- ));
554
- if (expanded) {
555
- container.addChild(new Text(safeTerminalText(task.instructions), 2, 0));
556
- return new WidthBoundComponent(container);
557
- }
558
- container.addChild(new InstructionPreview(task.instructions, theme));
559
- container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
560
- return new WidthBoundComponent(container);
561
- }
562
-
563
- class InstructionPreview implements Component {
564
- constructor(
565
- private readonly instructions: unknown,
566
- private readonly theme: Theme,
567
- ) {}
568
- render(width: number): string[] {
569
- const bounded = Math.max(1, width);
570
- const contentWidth = Math.max(1, bounded - 2);
571
- const characterLimit = Math.max(256, Math.min(4096, contentWidth * 3));
572
- const preview = compactInstructionPreview(this.instructions, characterLimit);
573
- if (!preview.text) return [];
574
-
575
- const wrapped = wrapTextWithAnsi(preview.text, contentWidth);
576
- const previewLines = wrapped.slice(0, MAX_INSTRUCTION_PREVIEW_LINES);
577
- if (preview.truncated || wrapped.length > MAX_INSTRUCTION_PREVIEW_LINES) {
578
- const lastIndex = previewLines.length - 1;
579
- previewLines[lastIndex] = truncateToWidth(`${previewLines[lastIndex] ?? ""}…`, contentWidth, "…");
580
- }
581
- return previewLines.map((line) =>
582
- truncateToWidth(this.theme.fg("dim", ` ${line}`), bounded, "…")
583
- );
584
- }
585
- invalidate(): void {}
586
- }
587
-
588
- function renderInteractiveMessageCall(
589
- theme: Theme,
590
- tool: string,
591
- workerId: unknown,
592
- instructions: unknown,
593
- expanded: boolean,
594
- ): Component {
595
- const container = new Container();
596
- container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(workerId)), 0, 0));
597
- if (expanded) container.addChild(new Text(safeTerminalText(instructions), 2, 0));
598
- else {
599
- container.addChild(new Text(`${theme.fg("accent", "→")} ${truncateInstruction(instructions, 240)}`, 0, 0));
600
- container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full message")), 0, 0));
601
- }
602
- return new WidthBoundComponent(container);
603
- }
604
-
605
- function safeTerminalText(value: unknown): string {
606
- const text = typeof value === "string" ? value : value == null ? "" : String(value);
607
- return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
608
- const code = character.charCodeAt(0);
609
- return code === 0x7f ? "␡" : String.fromCodePoint(0x2400 + code);
610
- });
611
- }
612
-
613
- function compactInstructionPreview(instructions: unknown, characterLimit: number): { text: string; truncated: boolean } {
614
- const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
615
- const source = text.slice(0, characterLimit);
616
- return {
617
- text: safeTerminalText(source).replace(/\s+/g, " ").trim(),
618
- truncated: source.length < text.length,
619
- };
620
- }
621
-
622
- function firstInstructionLine(instructions: unknown): string | undefined {
623
- const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
624
- return text.split(/\r\n?|\n/).find((line) => line.trim().length > 0);
625
- }
626
-
627
- function truncateInstruction(instructions: unknown, limit: number): string {
628
- const first = firstInstructionLine(instructions) ?? "";
629
- return first.length > limit ? `${first.slice(0, limit - 1)}…` : first;
630
- }
631
-
632
- function renderCompactCall(theme: Theme, tool: string, target: unknown): Text {
633
- return new Text(
634
- theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(target)),
635
- 0,
636
- 0,
637
- );
638
- }
639
-
640
- function renderOrchestrationResult(
641
- result: AgentToolResult<unknown>,
642
- isPartial: boolean,
643
- expanded: boolean,
644
- theme: Theme,
645
- lastComponent: unknown,
646
- ): Component {
647
- const details = result.details;
648
- if (Result.isSuccess(decodeAcceptedRunRenderDetails(details))) {
649
- return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
650
- }
651
- const inlineResult = readInlineResult(details);
652
- if (inlineResult) {
653
- const component = lastComponent instanceof InlineResultComponent
654
- ? lastComponent
655
- : new InlineResultComponent(theme);
656
- component.update(inlineResult, isPartial, expanded);
657
- return component;
658
- }
659
- if (Result.isSuccess(decodeUnavailableWorkerRenderDetails(details))) {
660
- return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
661
- }
662
- if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
663
- return new WidthBoundComponent(renderSimpleResult(result, firstResultLine(result) || "Work sent", theme, "warning"));
664
- }
665
-
666
- interface RenderedInlineSettlement {
667
- worker: string;
668
- title: string;
669
- status: InlineWorkerSettlementDetails["status"];
670
- response: string;
671
- elapsed?: string;
672
- }
673
-
674
- class InlineResultComponent implements Component {
675
- private result: RenderedInlineSettlement | undefined;
676
- private partial = false;
677
- private expanded = false;
678
- private child: Component = new Container();
679
- constructor(private readonly theme: Theme) {}
680
- update(result: RenderedInlineSettlement, partial: boolean, expanded: boolean): void {
681
- this.result = result;
682
- this.partial = partial;
683
- this.expanded = expanded;
684
- this.rebuild();
685
- }
686
- render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
687
- invalidate(): void { this.rebuild(); }
688
- dispose(): void { disposeComponent(this.child); }
689
- private rebuild(): void {
690
- disposeComponent(this.child);
691
- const container = new Container();
692
- const result = this.result;
693
- if (!result) {
694
- this.child = container;
695
- return;
696
- }
697
- const appearance = resultAppearance(result.status, "ready for follow-up");
698
- const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
699
- const title = this.theme.bold(result.title);
700
- const workerName = this.theme.fg("muted", this.theme.italic(result.worker));
701
- const header = [
702
- this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
703
- workerName,
704
- ...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
705
- ].join(" · ");
706
- container.addChild(new WidthBoundComponent(new Text(header, 0, 0), 1));
707
- if (result.response) {
708
- const markdown = new Markdown(result.response, this.expanded ? 2 : 0, 0, getMarkdownTheme());
709
- container.addChild(new WidthBoundComponent(markdown, this.expanded ? undefined : 2));
710
- }
711
- container.addChild(new Spacer(1));
712
- if (this.partial) container.addChild(new Text(this.theme.fg("warning", "Receiving worker response…"), 0, 0));
713
- else if (!this.expanded) container.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to inspect full response")), 0, 0));
714
- this.child = container;
715
- }
716
- }
717
-
718
- function readInlineResult(details: unknown): RenderedInlineSettlement | undefined {
719
- const decoded = decodeInlineWorkerToolDetails(details);
720
- if (Result.isFailure(decoded)) return undefined;
721
- const settlement = decoded.success.result;
722
- const outcome = settlement.outcome;
723
- const message = outcome.status === "failed" || outcome.status === "aborted"
724
- ? outcome.message
725
- : undefined;
726
- const assistantText = outcome.assistantText;
727
- const response = [message, assistantText]
728
- .filter((item): item is string => typeof item === "string" && item.length > 0)
729
- .join("\n\n");
730
- return {
731
- worker: settlement.worker,
732
- title: settlement.title,
733
- status: settlement.status,
734
- response,
735
- elapsed: formatElapsed(settlement.settledAt - settlement.startedAt),
736
- };
737
- }
738
-
739
- function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
740
- if (isPartial) return new Text(theme.fg("muted", "Reading worker diagnostics…"), 0, 0);
741
- const details = result.details;
742
- if (isRecord(details) && isRecord(details.state) && Array.isArray(details.state.workers)) {
743
- const workers = details.state.workers.filter(isRecord);
744
- const active = workers.filter((worker) => ["starting", "running", "stopping"].includes(String(worker.status))).length;
745
- const ready = workers.filter((worker) => worker.status === "ready").length;
746
- const diagnostics = isRecord(details.catalog) && Array.isArray(details.catalog.diagnostics) ? details.catalog.diagnostics.length : 0;
747
- const facts = [active ? `${active} active` : "No active workers", ready ? `${ready} available for follow-up` : undefined, diagnostics ? `${diagnostics} catalog diagnostic${diagnostics === 1 ? "" : "s"}` : undefined].filter(Boolean);
748
- return new Text(theme.fg("muted", facts.join(" · ")), 0, 0);
749
- }
750
- return new Text(theme.fg("muted", firstResultLine(result) || "Diagnostics unavailable"), 0, 0);
751
- }
752
-
753
- function renderSimpleResult(
754
- result: AgentToolResult<unknown>,
755
- message: string,
756
- theme: Theme,
757
- normalColor: "success" | "warning" = "success",
758
- ): Text {
759
- const failed = "isError" in result && result.isError === true;
760
- return new Text(theme.fg(failed ? "error" : normalColor, failed ? firstResultLine(result) || message : message), 0, 0);
761
- }
762
-
763
- function firstResultLine(result: AgentToolResult<unknown>): string | undefined {
764
- const first = result.content[0];
765
- if (first?.type !== "text") return undefined;
766
- return first.text.split("\n").find((line) => line.trim())?.trim();
767
- }
768
-
769
- function isRecord(value: unknown): value is Record<string, unknown> {
770
- return typeof value === "object" && value !== null;
771
- }