@zachwill/pi-orchestrate 0.1.0 → 0.1.1
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/README.md +5 -3
- package/extension/contract.ts +3 -1
- package/extension/delivery.ts +137 -89
- package/extension/domain.ts +2 -0
- package/extension/host.ts +41 -5
- package/extension/presentation.ts +304 -462
- package/extension/runtime.ts +190 -27
- package/extension/tools.ts +296 -19
- package/extension/worker-session.ts +7 -2
- package/package.json +1 -1
package/extension/tools.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
1
2
|
import type {
|
|
2
3
|
ExtensionAPI,
|
|
3
4
|
ExtensionContext,
|
|
5
|
+
Theme,
|
|
4
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Container, Markdown, Spacer, Text, truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
5
8
|
import {
|
|
6
9
|
formatSize,
|
|
7
10
|
getAgentDir,
|
|
11
|
+
getMarkdownTheme,
|
|
12
|
+
keyHint,
|
|
8
13
|
truncateHead,
|
|
9
14
|
} from "@earendil-works/pi-coding-agent";
|
|
10
15
|
import { Type } from "typebox";
|
|
@@ -110,15 +115,30 @@ export function registerOrchestrationTools(
|
|
|
110
115
|
"Use orchestrate for one independent worker wave, with a complete brief for every task.",
|
|
111
116
|
],
|
|
112
117
|
parameters: orchestrateSchema,
|
|
113
|
-
|
|
118
|
+
renderCall(args, theme, { expanded }) {
|
|
119
|
+
return renderDispatchCall(theme, args.tasks, expanded);
|
|
120
|
+
},
|
|
121
|
+
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
122
|
+
return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
|
|
123
|
+
},
|
|
124
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
114
125
|
const mode = deps.getDispatchMode(toolCallId);
|
|
115
126
|
const runtimeContext = await buildRuntimeContext(ctx, deps);
|
|
127
|
+
const settlements: unknown[] = [];
|
|
128
|
+
const onSettlement = mode === "inline" ? (settlement: unknown) => {
|
|
129
|
+
settlements.push(settlement);
|
|
130
|
+
onUpdate?.({
|
|
131
|
+
content: [{ type: "text", text: `${settlements.length} worker response(s) received.` }],
|
|
132
|
+
details: { mode: "inline", settlements: [...settlements] },
|
|
133
|
+
});
|
|
134
|
+
} : undefined;
|
|
116
135
|
const wave = await orchestrateWithMode(
|
|
117
136
|
deps.runtime,
|
|
118
137
|
runtimeContext,
|
|
119
138
|
params.tasks,
|
|
120
139
|
mode,
|
|
121
140
|
signal,
|
|
141
|
+
onSettlement,
|
|
122
142
|
);
|
|
123
143
|
|
|
124
144
|
if (mode === "async") {
|
|
@@ -163,6 +183,12 @@ export function registerOrchestrationTools(
|
|
|
163
183
|
"Use orchestration_status only for diagnostics or recovery; never poll it for completion.",
|
|
164
184
|
],
|
|
165
185
|
parameters: statusSchema,
|
|
186
|
+
renderCall(_args, theme) {
|
|
187
|
+
return new Text(theme.fg("toolTitle", theme.bold("orchestration_status")), 0, 0);
|
|
188
|
+
},
|
|
189
|
+
renderResult(result, { isPartial }, theme) {
|
|
190
|
+
return renderDiagnosticsResult(result, isPartial, theme);
|
|
191
|
+
},
|
|
166
192
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
167
193
|
const ownerSessionId = requireNonblank(
|
|
168
194
|
"owner session ID",
|
|
@@ -195,10 +221,24 @@ export function registerOrchestrationTools(
|
|
|
195
221
|
"Use worker_send only for follow-up work on an owned ready reusable worker.",
|
|
196
222
|
],
|
|
197
223
|
parameters: workerSendSchema,
|
|
198
|
-
|
|
224
|
+
renderCall(args, theme, { expanded }) {
|
|
225
|
+
return renderWorkerMessageCall(theme, "worker_send", args.worker_id, args.instructions, expanded);
|
|
226
|
+
},
|
|
227
|
+
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
228
|
+
return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
|
|
229
|
+
},
|
|
230
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
199
231
|
const workerId = asWorkerId(params.worker_id);
|
|
200
232
|
const mode = deps.getDispatchMode(toolCallId);
|
|
201
233
|
const runtimeContext = await buildRuntimeContext(ctx, deps);
|
|
234
|
+
const settlements: unknown[] = [];
|
|
235
|
+
const onSettlement = mode === "inline" ? (settlement: unknown) => {
|
|
236
|
+
settlements.push(settlement);
|
|
237
|
+
onUpdate?.({
|
|
238
|
+
content: [{ type: "text", text: `${settlements.length} worker response(s) received.` }],
|
|
239
|
+
details: { mode: "inline", settlements: [...settlements] },
|
|
240
|
+
});
|
|
241
|
+
} : undefined;
|
|
202
242
|
const wave = await sendWithMode(
|
|
203
243
|
deps.runtime,
|
|
204
244
|
runtimeContext,
|
|
@@ -206,6 +246,7 @@ export function registerOrchestrationTools(
|
|
|
206
246
|
params.instructions,
|
|
207
247
|
mode,
|
|
208
248
|
signal,
|
|
249
|
+
onSettlement,
|
|
209
250
|
);
|
|
210
251
|
|
|
211
252
|
if (mode === "async") {
|
|
@@ -250,6 +291,17 @@ export function registerOrchestrationTools(
|
|
|
250
291
|
"Use worker_abort only for active work; use worker_close for a ready reusable worker.",
|
|
251
292
|
],
|
|
252
293
|
parameters: workerAbortSchema,
|
|
294
|
+
renderCall(args, theme) {
|
|
295
|
+
const target = "wave_id" in args
|
|
296
|
+
? args.wave_id
|
|
297
|
+
: "worker_ids" in args
|
|
298
|
+
? `${args.worker_ids.length} worker${args.worker_ids.length === 1 ? "" : "s"}`
|
|
299
|
+
: "all workers";
|
|
300
|
+
return renderCompactCall(theme, "worker_abort", target);
|
|
301
|
+
},
|
|
302
|
+
renderResult(result, { isPartial }, theme) {
|
|
303
|
+
return renderSimpleResult(result, isPartial ? "Requesting worker stop…" : "Worker stop requested", theme, "warning");
|
|
304
|
+
},
|
|
253
305
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
254
306
|
const ownerSessionId = requireNonblank(
|
|
255
307
|
"owner session ID",
|
|
@@ -279,6 +331,12 @@ export function registerOrchestrationTools(
|
|
|
279
331
|
"Use worker_close when an owned ready reusable worker is finished.",
|
|
280
332
|
],
|
|
281
333
|
parameters: workerCloseSchema,
|
|
334
|
+
renderCall(args, theme) {
|
|
335
|
+
return renderCompactCall(theme, "worker_close", args.worker_id);
|
|
336
|
+
},
|
|
337
|
+
renderResult(result, { isPartial }, theme) {
|
|
338
|
+
return renderSimpleResult(result, isPartial ? "Closing worker…" : "✓ Worker closed", theme);
|
|
339
|
+
},
|
|
282
340
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
283
341
|
const ownerSessionId = requireNonblank(
|
|
284
342
|
"owner session ID",
|
|
@@ -325,16 +383,10 @@ function orchestrateWithMode(
|
|
|
325
383
|
tasks: readonly OrchestrateTaskInput[],
|
|
326
384
|
mode: "async" | "inline",
|
|
327
385
|
signal: AbortSignal | undefined,
|
|
386
|
+
onSettlement?: (settlement: unknown) => void,
|
|
328
387
|
): Promise<AcceptedWave | CompletedWave> {
|
|
329
388
|
if (mode === "async") return runtime.orchestrate(context, tasks, "async");
|
|
330
|
-
|
|
331
|
-
const orchestrateInline = runtime.orchestrate as unknown as (
|
|
332
|
-
context: OrchestrationContext,
|
|
333
|
-
tasks: readonly OrchestrateTaskInput[],
|
|
334
|
-
mode: "inline",
|
|
335
|
-
signal?: AbortSignal,
|
|
336
|
-
) => Promise<CompletedWave>;
|
|
337
|
-
return orchestrateInline.call(runtime, context, tasks, "inline", signal);
|
|
389
|
+
return runtime.orchestrate(context, tasks, "inline", signal, onSettlement);
|
|
338
390
|
}
|
|
339
391
|
|
|
340
392
|
function sendWithMode(
|
|
@@ -344,17 +396,10 @@ function sendWithMode(
|
|
|
344
396
|
instructions: string,
|
|
345
397
|
mode: "async" | "inline",
|
|
346
398
|
signal: AbortSignal | undefined,
|
|
399
|
+
onSettlement?: (settlement: unknown) => void,
|
|
347
400
|
): Promise<AcceptedWave | CompletedWave> {
|
|
348
401
|
if (mode === "async") return runtime.send(context, workerId, instructions, "async");
|
|
349
|
-
|
|
350
|
-
const sendInline = runtime.send as unknown as (
|
|
351
|
-
context: OrchestrationContext,
|
|
352
|
-
workerId: WorkerId,
|
|
353
|
-
instructions: string,
|
|
354
|
-
mode: "inline",
|
|
355
|
-
signal?: AbortSignal,
|
|
356
|
-
) => Promise<CompletedWave>;
|
|
357
|
-
return sendInline.call(runtime, context, workerId, instructions, "inline", signal);
|
|
402
|
+
return runtime.send(context, workerId, instructions, "inline", signal, onSettlement);
|
|
358
403
|
}
|
|
359
404
|
|
|
360
405
|
function requireNonblank(name: string, value: string): string {
|
|
@@ -557,3 +602,235 @@ function readableDetails(title: string, details: unknown): string {
|
|
|
557
602
|
|
|
558
603
|
return `${truncation.content}\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}. Full structured details remain available.]`;
|
|
559
604
|
}
|
|
605
|
+
|
|
606
|
+
function renderDispatchCall(
|
|
607
|
+
theme: Theme,
|
|
608
|
+
tasks: readonly { worker: string; title: string; instructions: string }[],
|
|
609
|
+
expanded: boolean,
|
|
610
|
+
): Component {
|
|
611
|
+
const container = new Container();
|
|
612
|
+
const count = tasks.length;
|
|
613
|
+
container.addChild(new Text(
|
|
614
|
+
theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", `${count} worker${count === 1 ? "" : "s"}`),
|
|
615
|
+
0, 0,
|
|
616
|
+
));
|
|
617
|
+
if (expanded) {
|
|
618
|
+
for (const task of tasks) {
|
|
619
|
+
container.addChild(new Spacer(1));
|
|
620
|
+
container.addChild(new Text(`${theme.fg("accent", "→")} ${theme.fg("muted", task.worker)} · ${theme.fg("text", theme.bold(task.title))}`, 0, 0));
|
|
621
|
+
container.addChild(new Text(safeTerminalText(task.instructions), 2, 0));
|
|
622
|
+
}
|
|
623
|
+
return new WidthBoundComponent(container);
|
|
624
|
+
}
|
|
625
|
+
container.addChild(new InstructionPreview(tasks, theme));
|
|
626
|
+
container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
|
|
627
|
+
return new WidthBoundComponent(container);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
class InstructionPreview implements Component {
|
|
631
|
+
constructor(
|
|
632
|
+
private readonly tasks: readonly { worker: string; title: string; instructions: string }[],
|
|
633
|
+
private readonly theme: Theme,
|
|
634
|
+
) {}
|
|
635
|
+
render(width: number): string[] {
|
|
636
|
+
const bounded = Math.max(1, width);
|
|
637
|
+
return this.tasks.map((task) => {
|
|
638
|
+
const preview = firstInstructionLine(task.instructions) ?? "";
|
|
639
|
+
const row = `${this.theme.fg("accent", "→")} ${safeTerminalText(task.worker)} · ${safeTerminalText(task.title)}${preview ? ` — ${safeTerminalText(preview)}` : ""}`;
|
|
640
|
+
return truncateToWidth(row, bounded, "…");
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
invalidate(): void {}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function renderWorkerMessageCall(
|
|
647
|
+
theme: Theme,
|
|
648
|
+
tool: string,
|
|
649
|
+
workerId: string,
|
|
650
|
+
instructions: string,
|
|
651
|
+
expanded: boolean,
|
|
652
|
+
): Component {
|
|
653
|
+
const container = new Container();
|
|
654
|
+
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", workerId), 0, 0));
|
|
655
|
+
if (expanded) container.addChild(new Text(safeTerminalText(instructions), 2, 0));
|
|
656
|
+
else {
|
|
657
|
+
container.addChild(new Text(`${theme.fg("accent", "→")} ${truncateInstruction(instructions, 240)}`, 0, 0));
|
|
658
|
+
container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full message")), 0, 0));
|
|
659
|
+
}
|
|
660
|
+
return new WidthBoundComponent(container);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
class WidthBoundComponent implements Component {
|
|
664
|
+
constructor(private readonly child: Component, private readonly maxLines?: number) {}
|
|
665
|
+
render(width: number): string[] {
|
|
666
|
+
const bounded = Math.max(1, Math.floor(width));
|
|
667
|
+
const lines = this.child.render(bounded);
|
|
668
|
+
return (this.maxLines === undefined ? lines : lines.slice(0, this.maxLines))
|
|
669
|
+
.map((line) => truncateToWidth(line, bounded, "…"));
|
|
670
|
+
}
|
|
671
|
+
invalidate(): void { this.child.invalidate(); }
|
|
672
|
+
dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function safeTerminalText(value: string): string {
|
|
676
|
+
return value.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
|
|
677
|
+
const code = character.charCodeAt(0);
|
|
678
|
+
return code === 0x7f ? "␡" : String.fromCodePoint(0x2400 + code);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function firstInstructionLine(instructions: string): string | undefined {
|
|
683
|
+
return instructions.split(/\r\n?|\n/).find((line) => line.trim().length > 0);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function truncateInstruction(instructions: string, limit: number): string {
|
|
687
|
+
const first = firstInstructionLine(instructions) ?? "";
|
|
688
|
+
return first.length > limit ? `${first.slice(0, limit - 1)}…` : first;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function renderCompactCall(theme: Theme, tool: string, target: string): Text {
|
|
692
|
+
return new Text(
|
|
693
|
+
theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", target),
|
|
694
|
+
0,
|
|
695
|
+
0,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function renderOrchestrationResult(
|
|
700
|
+
result: AgentToolResult<unknown>,
|
|
701
|
+
isPartial: boolean,
|
|
702
|
+
expanded: boolean,
|
|
703
|
+
theme: Theme,
|
|
704
|
+
lastComponent: unknown,
|
|
705
|
+
): Component {
|
|
706
|
+
const details = result.details;
|
|
707
|
+
if (isRecord(details) && typeof details.id === "string" && Array.isArray(details.workerIds) && details.workerIds.every((id) => typeof id === "string")) {
|
|
708
|
+
const count = details.workerIds.length;
|
|
709
|
+
return new WidthBoundComponent(new Text(theme.fg("success", `Sent to ${count} worker${count === 1 ? "" : "s"}`) + theme.fg("dim", " · responses arrive as they complete"), 0, 0));
|
|
710
|
+
}
|
|
711
|
+
const settlements = inlineSettlements(details);
|
|
712
|
+
if (settlements.length > 0) {
|
|
713
|
+
const component = lastComponent instanceof InlineResultComponent
|
|
714
|
+
? lastComponent
|
|
715
|
+
: new InlineResultComponent(theme);
|
|
716
|
+
component.update(settlements, isPartial, expanded);
|
|
717
|
+
return component;
|
|
718
|
+
}
|
|
719
|
+
if (isRecord(details) && (Array.isArray(details.settlements) || Array.isArray(details.results) || "workerIds" in details)) {
|
|
720
|
+
return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
|
|
721
|
+
}
|
|
722
|
+
if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
|
|
723
|
+
return new WidthBoundComponent(renderSimpleResult(result, firstResultLine(result) || "Work sent", theme, "warning"));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
interface InlineSettlement {
|
|
727
|
+
worker: string;
|
|
728
|
+
title: string;
|
|
729
|
+
status: "completed" | "ready" | "failed" | "aborted";
|
|
730
|
+
response: string;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
class InlineResultComponent implements Component {
|
|
734
|
+
private settlements: readonly InlineSettlement[] = [];
|
|
735
|
+
private partial = false;
|
|
736
|
+
private expanded = false;
|
|
737
|
+
private child: Component = new Container();
|
|
738
|
+
constructor(private readonly theme: Theme) {}
|
|
739
|
+
update(settlements: readonly InlineSettlement[], partial: boolean, expanded: boolean): void {
|
|
740
|
+
this.settlements = settlements;
|
|
741
|
+
this.partial = partial;
|
|
742
|
+
this.expanded = expanded;
|
|
743
|
+
this.rebuild();
|
|
744
|
+
}
|
|
745
|
+
render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
|
|
746
|
+
invalidate(): void { this.rebuild(); }
|
|
747
|
+
dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
|
|
748
|
+
private rebuild(): void {
|
|
749
|
+
(this.child as Component & { dispose?: () => void }).dispose?.();
|
|
750
|
+
const container = new Container();
|
|
751
|
+
for (const settlement of this.settlements) {
|
|
752
|
+
const failed = settlement.status === "failed";
|
|
753
|
+
const aborted = settlement.status === "aborted";
|
|
754
|
+
const color = failed ? "error" : aborted ? "warning" : "success";
|
|
755
|
+
const icon = failed ? "✗" : aborted ? "■" : "✓";
|
|
756
|
+
container.addChild(new WidthBoundComponent(new Text(this.theme.fg(color, this.theme.bold(`${icon} ${settlement.worker} · ${settlement.title} · ${settlement.status}`)), 0, 0), 1));
|
|
757
|
+
if (settlement.response) {
|
|
758
|
+
const markdown = new Markdown(settlement.response, this.expanded ? 2 : 0, 0, getMarkdownTheme());
|
|
759
|
+
container.addChild(new WidthBoundComponent(markdown, this.expanded ? undefined : 2));
|
|
760
|
+
}
|
|
761
|
+
container.addChild(new Spacer(1));
|
|
762
|
+
}
|
|
763
|
+
if (this.partial) container.addChild(new Text(this.theme.fg("warning", "Waiting for remaining workers…"), 0, 0));
|
|
764
|
+
else if (!this.expanded) container.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to inspect full responses")), 0, 0));
|
|
765
|
+
this.child = container;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function inlineSettlements(details: unknown): InlineSettlement[] {
|
|
770
|
+
if (!isRecord(details)) return [];
|
|
771
|
+
const values = Array.isArray(details.settlements) ? details.settlements : Array.isArray(details.results) ? details.results : [];
|
|
772
|
+
const parsed: InlineSettlement[] = [];
|
|
773
|
+
for (const value of values) {
|
|
774
|
+
const settlement = readInlineSettlement(value);
|
|
775
|
+
if (settlement) parsed.push(settlement);
|
|
776
|
+
}
|
|
777
|
+
return parsed;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function readInlineSettlement(value: unknown): InlineSettlement | undefined {
|
|
781
|
+
if (!isRecord(value) || typeof value.worker !== "string" || typeof value.title !== "string" || !isRecord(value.outcome)) return undefined;
|
|
782
|
+
const outcome = value.outcome;
|
|
783
|
+
const statuses = ["completed", "ready", "failed", "aborted"] as const;
|
|
784
|
+
const status = statuses.find((item) => item === value.status);
|
|
785
|
+
const outcomeStatus = statuses.find((item) => item === outcome.status);
|
|
786
|
+
if (!status || outcomeStatus !== status) return undefined;
|
|
787
|
+
const message = outcome.message;
|
|
788
|
+
const camelAssistant = outcome.assistantText;
|
|
789
|
+
const snakeAssistant = outcome.assistant_text;
|
|
790
|
+
if (message !== undefined && typeof message !== "string") return undefined;
|
|
791
|
+
if (camelAssistant !== undefined && typeof camelAssistant !== "string") return undefined;
|
|
792
|
+
if (snakeAssistant !== undefined && typeof snakeAssistant !== "string") return undefined;
|
|
793
|
+
const assistantText = typeof camelAssistant === "string" ? camelAssistant : snakeAssistant;
|
|
794
|
+
if ((status === "completed" || status === "ready") && typeof assistantText !== "string") return undefined;
|
|
795
|
+
if (status === "failed" && typeof message !== "string") return undefined;
|
|
796
|
+
return {
|
|
797
|
+
worker: value.worker,
|
|
798
|
+
title: value.title,
|
|
799
|
+
status,
|
|
800
|
+
response: [message, assistantText].filter((item): item is string => typeof item === "string" && item.length > 0).join("\n\n"),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
|
|
805
|
+
if (isPartial) return new Text(theme.fg("muted", "Reading orchestration diagnostics…"), 0, 0);
|
|
806
|
+
const details = result.details;
|
|
807
|
+
if (isRecord(details) && isRecord(details.snapshot) && Array.isArray(details.snapshot.workers)) {
|
|
808
|
+
const workers = details.snapshot.workers.filter(isRecord);
|
|
809
|
+
const active = workers.filter((worker) => ["starting", "running", "stopping"].includes(String(worker.status))).length;
|
|
810
|
+
const ready = workers.filter((worker) => worker.status === "ready").length;
|
|
811
|
+
const diagnostics = isRecord(details.catalog) && Array.isArray(details.catalog.diagnostics) ? details.catalog.diagnostics.length : 0;
|
|
812
|
+
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);
|
|
813
|
+
return new Text(theme.fg("muted", facts.join(" · ")), 0, 0);
|
|
814
|
+
}
|
|
815
|
+
return new Text(theme.fg("muted", firstResultLine(result) || "Diagnostics unavailable"), 0, 0);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function renderSimpleResult(
|
|
819
|
+
result: AgentToolResult<unknown>,
|
|
820
|
+
message: string,
|
|
821
|
+
theme: Theme,
|
|
822
|
+
normalColor: "success" | "warning" = "success",
|
|
823
|
+
): Text {
|
|
824
|
+
const failed = "isError" in result && result.isError === true;
|
|
825
|
+
return new Text(theme.fg(failed ? "error" : normalColor, failed ? firstResultLine(result) || message : message), 0, 0);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function firstResultLine(result: AgentToolResult<unknown>): string | undefined {
|
|
829
|
+
const first = result.content[0];
|
|
830
|
+
if (first?.type !== "text") return undefined;
|
|
831
|
+
return first.text.split("\n").find((line) => line.trim())?.trim();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
835
|
+
return typeof value === "object" && value !== null;
|
|
836
|
+
}
|
|
@@ -421,17 +421,22 @@ async function prepareChildModelRuntime(
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
const resolvedAuth = await options.modelRegistry.getApiKeyAndHeaders(initiallyResolvedModel);
|
|
424
|
+
const parentUsesOAuth = options.modelRegistry.isUsingOAuth(initiallyResolvedModel);
|
|
424
425
|
if (resolvedAuth.ok) {
|
|
425
426
|
if (resolvedAuth.headers) {
|
|
426
427
|
modelRuntime.registerProvider(selected.provider, {
|
|
427
428
|
headers: { ...resolvedAuth.headers },
|
|
428
429
|
});
|
|
429
430
|
}
|
|
430
|
-
|
|
431
|
+
// OAuth resolution returns an access token through the compatibility API, but
|
|
432
|
+
// installing that token as a runtime API key masks the complete OAuth credential
|
|
433
|
+
// that the child already reads from the shared auth.json file.
|
|
434
|
+
if (resolvedAuth.apiKey && !parentUsesOAuth) {
|
|
431
435
|
await modelRuntime.setRuntimeApiKey(selected.provider, resolvedAuth.apiKey);
|
|
432
436
|
}
|
|
433
437
|
// Pi 0.80.10 exposes resolved provider env here but has no public ModelRuntime
|
|
434
|
-
// runtime-env setter. Provider registrations, resolved headers, and
|
|
438
|
+
// runtime-env setter. Provider registrations, resolved headers, and non-OAuth
|
|
439
|
+
// API keys are copied.
|
|
435
440
|
}
|
|
436
441
|
|
|
437
442
|
await refreshChildModelRuntime(modelRuntime, options.definition);
|