@zachwill/pi-orchestrate 0.9.2 → 0.11.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.
- package/README.md +47 -108
- package/extension/catalog/definition.ts +89 -0
- package/extension/{catalog.ts → catalog/discovery.ts} +4 -4
- package/extension/index.ts +19 -36
- package/extension/orchestration/admission.ts +297 -0
- package/extension/{domain.ts → orchestration/model.ts} +27 -89
- package/extension/{runtime.ts → orchestration/service.ts} +179 -473
- package/extension/{worker-settlement.ts → orchestration/settlement.ts} +67 -21
- package/extension/package-root.ts +3 -0
- package/extension/parent/contract.ts +147 -0
- package/extension/{delivery.ts → parent/delivery.ts} +5 -8
- package/extension/parent/dispatch-policy.ts +49 -0
- package/extension/{host.ts → parent/process-host.ts} +33 -26
- package/extension/{presentation.ts → pi/presentation.ts} +39 -27
- package/extension/pi/tool-renderer.ts +422 -0
- package/extension/pi/tools.ts +470 -0
- package/extension/worker/child-sessions.ts +291 -0
- package/extension/{worker-session.ts → worker/session.ts} +33 -291
- package/package.json +2 -1
- package/extension/contract.ts +0 -144
- package/extension/tools.ts +0 -837
- /package/extension/{tui.ts → pi/tui.ts} +0 -0
|
@@ -15,19 +15,22 @@ import {
|
|
|
15
15
|
type Component,
|
|
16
16
|
} from "@earendil-works/pi-tui";
|
|
17
17
|
import { Result } from "effect";
|
|
18
|
-
import type {
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
import type {
|
|
19
|
+
WorkerOutcome,
|
|
20
|
+
WorkerRecord,
|
|
21
|
+
WorkerStatus,
|
|
22
|
+
} from "../orchestration/model.ts";
|
|
23
|
+
import type { OwnerSnapshot } from "../orchestration/service.ts";
|
|
21
24
|
import {
|
|
22
25
|
disposeComponent,
|
|
23
26
|
formatElapsed,
|
|
24
27
|
resultAppearance,
|
|
25
28
|
WidthBoundComponent,
|
|
26
|
-
} from "./tui.
|
|
29
|
+
} from "./tui.ts";
|
|
27
30
|
import {
|
|
28
|
-
|
|
29
|
-
type
|
|
30
|
-
} from "
|
|
31
|
+
decodePersistedWorkerSettlement,
|
|
32
|
+
type WorkerSettlement,
|
|
33
|
+
} from "../orchestration/settlement.ts";
|
|
31
34
|
|
|
32
35
|
export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
|
|
33
36
|
export const MAX_RESULT_PREVIEW_LINES = 6;
|
|
@@ -49,12 +52,17 @@ const WORKER_ANIMATIONS = {
|
|
|
49
52
|
} as const;
|
|
50
53
|
const ANIMATION_CYCLE_TICKS = 40;
|
|
51
54
|
const SPINNER_INTERVAL_MS = 140;
|
|
55
|
+
const TURN_USAGE_MIN_WIDTH = 12;
|
|
56
|
+
const CONTEXT_USAGE_MIN_WIDTH = 28;
|
|
57
|
+
const WIDE_WORKER_ROW_MIN_WIDTH = 72;
|
|
58
|
+
const WORKER_NAME_SLACK_COLUMNS = 10;
|
|
52
59
|
const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running", "stopping"]);
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
/** Owner-scoped worker state feed consumed by the parent's status presentation. */
|
|
62
|
+
export interface WorkerStateSource {
|
|
55
63
|
subscribeState(
|
|
56
64
|
ownerSessionId: string,
|
|
57
|
-
listener: (snapshot:
|
|
65
|
+
listener: (snapshot: OwnerSnapshot) => void,
|
|
58
66
|
): () => void;
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -66,14 +74,14 @@ interface StatusBinding {
|
|
|
66
74
|
interface RenderRequester { requestRender(): void }
|
|
67
75
|
|
|
68
76
|
export function registerOrchestrationPresentation(pi: ExtensionAPI): void {
|
|
69
|
-
pi.registerMessageRenderer<
|
|
77
|
+
pi.registerMessageRenderer<WorkerSettlement>(
|
|
70
78
|
"pi-orchestrate-worker-result",
|
|
71
79
|
(message, { expanded }, theme) =>
|
|
72
80
|
new WorkerResultComponent(messageText(message.content), message.details, expanded, theme),
|
|
73
81
|
);
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
export function formatFooterStatus(snapshot:
|
|
84
|
+
export function formatFooterStatus(snapshot: OwnerSnapshot): string | undefined {
|
|
77
85
|
const ready = snapshot.workers.filter((worker) => worker.status === "ready").length;
|
|
78
86
|
return ready > 0 ? `${ready} interactive ready` : undefined;
|
|
79
87
|
}
|
|
@@ -84,14 +92,14 @@ export class StatusController {
|
|
|
84
92
|
private unsubscribeState: (() => void) | undefined;
|
|
85
93
|
private widget: WorkerStatusComponent | undefined;
|
|
86
94
|
|
|
87
|
-
constructor(private readonly
|
|
95
|
+
constructor(private readonly workerState: WorkerStateSource) {}
|
|
88
96
|
|
|
89
97
|
bind(ownerSessionId: string, ctx: ExtensionContext): void {
|
|
90
98
|
if (this.disposed) return;
|
|
91
99
|
this.clearBinding();
|
|
92
100
|
const binding = { ownerSessionId, ctx };
|
|
93
101
|
this.binding = binding;
|
|
94
|
-
this.unsubscribeState = this.
|
|
102
|
+
this.unsubscribeState = this.workerState.subscribeState(ownerSessionId, (snapshot) => {
|
|
95
103
|
if (this.binding !== binding) return;
|
|
96
104
|
this.present(binding.ctx, snapshot);
|
|
97
105
|
});
|
|
@@ -108,7 +116,7 @@ export class StatusController {
|
|
|
108
116
|
this.clearBinding();
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
private present(ctx: ExtensionContext, snapshot:
|
|
119
|
+
private present(ctx: ExtensionContext, snapshot: OwnerSnapshot): void {
|
|
112
120
|
ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, formatFooterStatus(snapshot));
|
|
113
121
|
if (ctx.mode !== "tui") return;
|
|
114
122
|
const active = activeWorkers(snapshot);
|
|
@@ -145,21 +153,23 @@ export class StatusController {
|
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
|
|
148
|
-
export function createStatusController(
|
|
149
|
-
|
|
156
|
+
export function createStatusController(
|
|
157
|
+
workerState: WorkerStateSource,
|
|
158
|
+
): StatusController {
|
|
159
|
+
return new StatusController(workerState);
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
export class WorkerStatusComponent implements Component {
|
|
153
163
|
private frameIndex = 0;
|
|
154
|
-
private snapshot:
|
|
164
|
+
private snapshot: OwnerSnapshot;
|
|
155
165
|
private timer: ReturnType<typeof setInterval> | undefined;
|
|
156
166
|
|
|
157
|
-
constructor(snapshot:
|
|
167
|
+
constructor(snapshot: OwnerSnapshot, private readonly theme: Theme, private readonly tui?: RenderRequester) {
|
|
158
168
|
this.snapshot = snapshot;
|
|
159
169
|
this.startTimer();
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
update(snapshot:
|
|
172
|
+
update(snapshot: OwnerSnapshot): void {
|
|
163
173
|
this.snapshot = snapshot;
|
|
164
174
|
if (activeWorkers(snapshot).length > 0) this.startTimer();
|
|
165
175
|
else this.stopTimer();
|
|
@@ -205,12 +215,14 @@ export class WorkerStatusComponent implements Component {
|
|
|
205
215
|
);
|
|
206
216
|
const turns = formatTurnMarker(worker);
|
|
207
217
|
const context = `${formatContextTokens(numberOrZero(worker.usage?.contextTokens))} ctx`;
|
|
208
|
-
const usageFields = width >=
|
|
218
|
+
const usageFields = width >= CONTEXT_USAGE_MIN_WIDTH
|
|
219
|
+
? [turns, context]
|
|
220
|
+
: width >= TURN_USAGE_MIN_WIDTH ? [turns] : [];
|
|
209
221
|
const workerName = this.theme.fg("muted", this.theme.italic(worker.worker));
|
|
210
222
|
const workerNameFits = visibleWidth(
|
|
211
223
|
`⠋ · ${worker.worker} · ${usageFields.join(" · ")}`,
|
|
212
|
-
) +
|
|
213
|
-
const suffixFields = width >=
|
|
224
|
+
) + WORKER_NAME_SLACK_COLUMNS <= width;
|
|
225
|
+
const suffixFields = width >= WIDE_WORKER_ROW_MIN_WIDTH && workerNameFits
|
|
214
226
|
? [workerName, ...usageFields]
|
|
215
227
|
: usageFields;
|
|
216
228
|
const prefix = `${glyph} `;
|
|
@@ -289,8 +301,8 @@ export class WorkerResultComponent implements Component {
|
|
|
289
301
|
}
|
|
290
302
|
}
|
|
291
303
|
|
|
292
|
-
function readSettlement(value: unknown):
|
|
293
|
-
const decoded =
|
|
304
|
+
function readSettlement(value: unknown): WorkerSettlement | undefined {
|
|
305
|
+
const decoded = decodePersistedWorkerSettlement(value);
|
|
294
306
|
return Result.isSuccess(decoded) ? decoded.success : undefined;
|
|
295
307
|
}
|
|
296
308
|
|
|
@@ -307,7 +319,7 @@ function outcomeText(outcome: WorkerOutcome): string {
|
|
|
307
319
|
return "Worker session closed.";
|
|
308
320
|
}
|
|
309
321
|
|
|
310
|
-
function presentedOutcome(result:
|
|
322
|
+
function presentedOutcome(result: WorkerSettlement): string {
|
|
311
323
|
const body = outcomeText(result.outcome);
|
|
312
324
|
if (result.status !== "completed" && result.status !== "ready") return body;
|
|
313
325
|
|
|
@@ -322,7 +334,7 @@ function presentedOutcome(result: WorkerSettlementDetails): string {
|
|
|
322
334
|
return lines.join("\n").trimEnd();
|
|
323
335
|
}
|
|
324
336
|
|
|
325
|
-
function settlementMetadata(result:
|
|
337
|
+
function settlementMetadata(result: WorkerSettlement): string[] {
|
|
326
338
|
return [
|
|
327
339
|
`worker ID ${result.workerId} · run ID ${result.runId}`,
|
|
328
340
|
`status ${result.status} · generation ${result.generation}`,
|
|
@@ -338,7 +350,7 @@ function messageText(content: unknown): string {
|
|
|
338
350
|
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
|
|
339
351
|
}
|
|
340
352
|
|
|
341
|
-
function activeWorkers(snapshot:
|
|
353
|
+
function activeWorkers(snapshot: OwnerSnapshot): WorkerRecord[] {
|
|
342
354
|
return snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status));
|
|
343
355
|
}
|
|
344
356
|
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
getMarkdownTheme,
|
|
5
|
+
keyHint,
|
|
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 { Result, Schema } from "effect";
|
|
17
|
+
import {
|
|
18
|
+
disposeComponent,
|
|
19
|
+
formatElapsed,
|
|
20
|
+
resultAppearance,
|
|
21
|
+
WidthBoundComponent,
|
|
22
|
+
} from "./tui.ts";
|
|
23
|
+
import {
|
|
24
|
+
decodeInlineWorkerToolDetails,
|
|
25
|
+
type InlineWorkerSettlementDetails,
|
|
26
|
+
WorkerSettlement,
|
|
27
|
+
} from "../orchestration/settlement.ts";
|
|
28
|
+
|
|
29
|
+
const MAX_INSTRUCTION_PREVIEW_LINES = 2;
|
|
30
|
+
|
|
31
|
+
const AcceptedRunRenderDetails = Schema.Struct({
|
|
32
|
+
mode: Schema.Literal("async"),
|
|
33
|
+
run_id: WorkerSettlement.fields.runId,
|
|
34
|
+
worker_id: WorkerSettlement.fields.workerId,
|
|
35
|
+
});
|
|
36
|
+
const UnavailableWorkerRenderDetails = Schema.Union([
|
|
37
|
+
Schema.Struct({ result: Schema.Unknown }),
|
|
38
|
+
Schema.Struct({ worker_id: Schema.Unknown }),
|
|
39
|
+
]);
|
|
40
|
+
const decodeAcceptedRunRenderDetails = Schema.decodeUnknownResult(
|
|
41
|
+
AcceptedRunRenderDetails,
|
|
42
|
+
);
|
|
43
|
+
const decodeUnavailableWorkerRenderDetails = Schema.decodeUnknownResult(
|
|
44
|
+
UnavailableWorkerRenderDetails,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
interface RenderOptions {
|
|
48
|
+
readonly isPartial: boolean;
|
|
49
|
+
readonly expanded: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface RenderContext {
|
|
53
|
+
readonly isError: boolean;
|
|
54
|
+
readonly lastComponent?: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const orchestrateToolRenderer = {
|
|
58
|
+
renderCall(args: unknown, theme: Theme, { expanded }: RenderOptions) {
|
|
59
|
+
return renderDispatchCall(theme, args, expanded);
|
|
60
|
+
},
|
|
61
|
+
renderResult(
|
|
62
|
+
result: AgentToolResult<unknown>,
|
|
63
|
+
{ isPartial, expanded }: RenderOptions,
|
|
64
|
+
theme: Theme,
|
|
65
|
+
context: RenderContext,
|
|
66
|
+
) {
|
|
67
|
+
return renderOrchestrationResult(
|
|
68
|
+
result,
|
|
69
|
+
isPartial,
|
|
70
|
+
expanded,
|
|
71
|
+
theme,
|
|
72
|
+
context.isError,
|
|
73
|
+
context.lastComponent,
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const workerStatusToolRenderer = {
|
|
79
|
+
renderCall(_args: unknown, theme: Theme) {
|
|
80
|
+
return new Text(theme.fg("toolTitle", theme.bold("worker_status")), 0, 0);
|
|
81
|
+
},
|
|
82
|
+
renderResult(
|
|
83
|
+
result: AgentToolResult<unknown>,
|
|
84
|
+
{ isPartial }: RenderOptions,
|
|
85
|
+
theme: Theme,
|
|
86
|
+
context: RenderContext,
|
|
87
|
+
) {
|
|
88
|
+
return renderDiagnosticsResult(result, isPartial, context.isError, theme);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const interactiveSendToolRenderer = {
|
|
93
|
+
renderCall(args: unknown, theme: Theme, { expanded }: RenderOptions) {
|
|
94
|
+
const fields = objectFields(args);
|
|
95
|
+
return renderInteractiveMessageCall(
|
|
96
|
+
theme,
|
|
97
|
+
"interactive_send",
|
|
98
|
+
fields.worker_id,
|
|
99
|
+
fields.instructions,
|
|
100
|
+
expanded,
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
renderResult: orchestrateToolRenderer.renderResult,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const workerAbortToolRenderer = {
|
|
107
|
+
renderCall(args: unknown, theme: Theme) {
|
|
108
|
+
const fields = objectFields(args);
|
|
109
|
+
const workerIds = Array.isArray(fields.worker_ids)
|
|
110
|
+
? fields.worker_ids
|
|
111
|
+
: undefined;
|
|
112
|
+
const target = workerIds
|
|
113
|
+
? `${workerIds.length} worker${workerIds.length === 1 ? "" : "s"}`
|
|
114
|
+
: fields.all === true ? "all workers" : "";
|
|
115
|
+
return renderCompactCall(theme, "worker_abort", target);
|
|
116
|
+
},
|
|
117
|
+
renderResult(
|
|
118
|
+
result: AgentToolResult<unknown>,
|
|
119
|
+
{ isPartial }: RenderOptions,
|
|
120
|
+
theme: Theme,
|
|
121
|
+
context: RenderContext,
|
|
122
|
+
) {
|
|
123
|
+
return renderSimpleResult(
|
|
124
|
+
result,
|
|
125
|
+
context.isError,
|
|
126
|
+
isPartial ? "Requesting worker stop…" : "Worker stop requested",
|
|
127
|
+
theme,
|
|
128
|
+
"warning",
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const interactiveCloseToolRenderer = {
|
|
134
|
+
renderCall(args: unknown, theme: Theme) {
|
|
135
|
+
return renderCompactCall(
|
|
136
|
+
theme,
|
|
137
|
+
"interactive_close",
|
|
138
|
+
objectFields(args).worker_id,
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
renderResult(
|
|
142
|
+
result: AgentToolResult<unknown>,
|
|
143
|
+
{ isPartial }: RenderOptions,
|
|
144
|
+
theme: Theme,
|
|
145
|
+
context: RenderContext,
|
|
146
|
+
) {
|
|
147
|
+
return renderSimpleResult(
|
|
148
|
+
result,
|
|
149
|
+
context.isError,
|
|
150
|
+
isPartial ? "Closing worker…" : "✓ Worker closed",
|
|
151
|
+
theme,
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function renderDispatchCall(
|
|
157
|
+
theme: Theme,
|
|
158
|
+
task: unknown,
|
|
159
|
+
expanded: boolean,
|
|
160
|
+
): Component {
|
|
161
|
+
const fields = isRecord(task) ? task : {};
|
|
162
|
+
const container = new Container();
|
|
163
|
+
container.addChild(new Text(
|
|
164
|
+
theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", safeTerminalText(fields.worker)),
|
|
165
|
+
0, 0,
|
|
166
|
+
));
|
|
167
|
+
container.addChild(new Text(
|
|
168
|
+
`${theme.fg("accent", "→")} ${theme.fg("text", theme.bold(safeTerminalText(fields.title)))}`,
|
|
169
|
+
0, 0,
|
|
170
|
+
));
|
|
171
|
+
if (expanded) {
|
|
172
|
+
container.addChild(new Text(safeTerminalText(fields.instructions), 2, 0));
|
|
173
|
+
return new WidthBoundComponent(container);
|
|
174
|
+
}
|
|
175
|
+
container.addChild(new InstructionPreview(fields.instructions, theme));
|
|
176
|
+
container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
|
|
177
|
+
return new WidthBoundComponent(container);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
class InstructionPreview implements Component {
|
|
181
|
+
constructor(
|
|
182
|
+
private readonly instructions: unknown,
|
|
183
|
+
private readonly theme: Theme,
|
|
184
|
+
) {}
|
|
185
|
+
render(width: number): string[] {
|
|
186
|
+
const bounded = Math.max(1, width);
|
|
187
|
+
const contentWidth = Math.max(1, bounded - 2);
|
|
188
|
+
const characterLimit = Math.max(256, Math.min(4096, contentWidth * 3));
|
|
189
|
+
const preview = compactInstructionPreview(this.instructions, characterLimit);
|
|
190
|
+
if (!preview.text) return [];
|
|
191
|
+
|
|
192
|
+
const wrapped = wrapTextWithAnsi(preview.text, contentWidth);
|
|
193
|
+
const previewLines = wrapped.slice(0, MAX_INSTRUCTION_PREVIEW_LINES);
|
|
194
|
+
if (preview.truncated || wrapped.length > MAX_INSTRUCTION_PREVIEW_LINES) {
|
|
195
|
+
const lastIndex = previewLines.length - 1;
|
|
196
|
+
previewLines[lastIndex] = truncateToWidth(`${previewLines[lastIndex] ?? ""}…`, contentWidth, "…");
|
|
197
|
+
}
|
|
198
|
+
return previewLines.map((line) =>
|
|
199
|
+
truncateToWidth(this.theme.fg("dim", ` ${line}`), bounded, "…")
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
invalidate(): void {}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderInteractiveMessageCall(
|
|
206
|
+
theme: Theme,
|
|
207
|
+
tool: string,
|
|
208
|
+
workerId: unknown,
|
|
209
|
+
instructions: unknown,
|
|
210
|
+
expanded: boolean,
|
|
211
|
+
): Component {
|
|
212
|
+
const container = new Container();
|
|
213
|
+
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(workerId)), 0, 0));
|
|
214
|
+
if (expanded) container.addChild(new Text(safeTerminalText(instructions), 2, 0));
|
|
215
|
+
else {
|
|
216
|
+
container.addChild(new Text(`${theme.fg("accent", "→")} ${truncateInstruction(instructions, 240)}`, 0, 0));
|
|
217
|
+
container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full message")), 0, 0));
|
|
218
|
+
}
|
|
219
|
+
return new WidthBoundComponent(container);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function safeTerminalText(value: unknown): string {
|
|
223
|
+
const text = typeof value === "string" ? value : value == null ? "" : String(value);
|
|
224
|
+
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
|
|
225
|
+
const code = character.charCodeAt(0);
|
|
226
|
+
return code === 0x7f ? "␡" : String.fromCodePoint(0x2400 + code);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function compactInstructionPreview(instructions: unknown, characterLimit: number): { text: string; truncated: boolean } {
|
|
231
|
+
const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
|
|
232
|
+
const source = text.slice(0, characterLimit);
|
|
233
|
+
return {
|
|
234
|
+
text: safeTerminalText(source).replace(/\s+/g, " ").trim(),
|
|
235
|
+
truncated: source.length < text.length,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function firstInstructionLine(instructions: unknown): string | undefined {
|
|
240
|
+
const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
|
|
241
|
+
return text.split(/\r\n?|\n/).find((line) => line.trim().length > 0);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function truncateInstruction(instructions: unknown, limit: number): string {
|
|
245
|
+
const first = firstInstructionLine(instructions) ?? "";
|
|
246
|
+
return first.length > limit ? `${first.slice(0, limit - 1)}…` : first;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderCompactCall(theme: Theme, tool: string, target: unknown): Text {
|
|
250
|
+
return new Text(
|
|
251
|
+
theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(target)),
|
|
252
|
+
0,
|
|
253
|
+
0,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderOrchestrationResult(
|
|
258
|
+
result: AgentToolResult<unknown>,
|
|
259
|
+
isPartial: boolean,
|
|
260
|
+
expanded: boolean,
|
|
261
|
+
theme: Theme,
|
|
262
|
+
isError: boolean,
|
|
263
|
+
lastComponent: unknown,
|
|
264
|
+
): Component {
|
|
265
|
+
if (isError) {
|
|
266
|
+
return new WidthBoundComponent(renderSimpleResult(
|
|
267
|
+
result,
|
|
268
|
+
true,
|
|
269
|
+
firstResultLine(result) || "Worker operation failed",
|
|
270
|
+
theme,
|
|
271
|
+
"warning",
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
const details = result.details;
|
|
275
|
+
if (Result.isSuccess(decodeAcceptedRunRenderDetails(details))) {
|
|
276
|
+
return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
|
|
277
|
+
}
|
|
278
|
+
const inlineResult = readInlineResult(details);
|
|
279
|
+
if (inlineResult) {
|
|
280
|
+
const component = lastComponent instanceof InlineResultComponent
|
|
281
|
+
? lastComponent
|
|
282
|
+
: new InlineResultComponent(theme);
|
|
283
|
+
component.update(inlineResult, isPartial, expanded);
|
|
284
|
+
return component;
|
|
285
|
+
}
|
|
286
|
+
if (Result.isSuccess(decodeUnavailableWorkerRenderDetails(details))) {
|
|
287
|
+
return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
|
|
288
|
+
}
|
|
289
|
+
if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
|
|
290
|
+
return new WidthBoundComponent(renderSimpleResult(
|
|
291
|
+
result,
|
|
292
|
+
false,
|
|
293
|
+
firstResultLine(result) || "Work sent",
|
|
294
|
+
theme,
|
|
295
|
+
"warning",
|
|
296
|
+
));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface RenderedInlineSettlement {
|
|
300
|
+
worker: string;
|
|
301
|
+
title: string;
|
|
302
|
+
status: InlineWorkerSettlementDetails["status"];
|
|
303
|
+
response: string;
|
|
304
|
+
elapsed?: string;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
class InlineResultComponent implements Component {
|
|
308
|
+
private result: RenderedInlineSettlement | undefined;
|
|
309
|
+
private partial = false;
|
|
310
|
+
private expanded = false;
|
|
311
|
+
private child: Component = new Container();
|
|
312
|
+
constructor(private readonly theme: Theme) {}
|
|
313
|
+
update(result: RenderedInlineSettlement, partial: boolean, expanded: boolean): void {
|
|
314
|
+
this.result = result;
|
|
315
|
+
this.partial = partial;
|
|
316
|
+
this.expanded = expanded;
|
|
317
|
+
this.rebuild();
|
|
318
|
+
}
|
|
319
|
+
render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
|
|
320
|
+
invalidate(): void { this.rebuild(); }
|
|
321
|
+
dispose(): void { disposeComponent(this.child); }
|
|
322
|
+
private rebuild(): void {
|
|
323
|
+
disposeComponent(this.child);
|
|
324
|
+
const container = new Container();
|
|
325
|
+
const result = this.result;
|
|
326
|
+
if (!result) {
|
|
327
|
+
this.child = container;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const appearance = resultAppearance(result.status, "ready for follow-up");
|
|
331
|
+
const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
|
|
332
|
+
const title = this.theme.bold(result.title);
|
|
333
|
+
const workerName = this.theme.fg("muted", this.theme.italic(result.worker));
|
|
334
|
+
const header = [
|
|
335
|
+
this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
|
|
336
|
+
workerName,
|
|
337
|
+
...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
|
|
338
|
+
].join(" · ");
|
|
339
|
+
container.addChild(new WidthBoundComponent(new Text(header, 0, 0), 1));
|
|
340
|
+
if (result.response) {
|
|
341
|
+
const markdown = new Markdown(result.response, this.expanded ? 2 : 0, 0, getMarkdownTheme());
|
|
342
|
+
container.addChild(new WidthBoundComponent(markdown, this.expanded ? undefined : 2));
|
|
343
|
+
}
|
|
344
|
+
container.addChild(new Spacer(1));
|
|
345
|
+
if (this.partial) container.addChild(new Text(this.theme.fg("warning", "Receiving worker response…"), 0, 0));
|
|
346
|
+
else if (!this.expanded) container.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to inspect full response")), 0, 0));
|
|
347
|
+
this.child = container;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readInlineResult(details: unknown): RenderedInlineSettlement | undefined {
|
|
352
|
+
const decoded = decodeInlineWorkerToolDetails(details);
|
|
353
|
+
if (Result.isFailure(decoded)) return undefined;
|
|
354
|
+
const settlement = decoded.success.result;
|
|
355
|
+
const outcome = settlement.outcome;
|
|
356
|
+
const message = outcome.status === "failed" || outcome.status === "aborted"
|
|
357
|
+
? outcome.message
|
|
358
|
+
: undefined;
|
|
359
|
+
const assistantText = outcome.assistantText;
|
|
360
|
+
const response = [message, assistantText]
|
|
361
|
+
.filter((item): item is string => typeof item === "string" && item.length > 0)
|
|
362
|
+
.join("\n\n");
|
|
363
|
+
return {
|
|
364
|
+
worker: settlement.worker,
|
|
365
|
+
title: settlement.title,
|
|
366
|
+
status: settlement.status,
|
|
367
|
+
response,
|
|
368
|
+
elapsed: formatElapsed(settlement.settledAt - settlement.startedAt),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderDiagnosticsResult(
|
|
373
|
+
result: AgentToolResult<unknown>,
|
|
374
|
+
isPartial: boolean,
|
|
375
|
+
isError: boolean,
|
|
376
|
+
theme: Theme,
|
|
377
|
+
): Text {
|
|
378
|
+
if (isError) {
|
|
379
|
+
return renderSimpleResult(
|
|
380
|
+
result,
|
|
381
|
+
true,
|
|
382
|
+
firstResultLine(result) || "Worker diagnostics failed",
|
|
383
|
+
theme,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (isPartial) return new Text(theme.fg("muted", "Reading worker diagnostics…"), 0, 0);
|
|
387
|
+
const details = result.details;
|
|
388
|
+
if (isRecord(details) && isRecord(details.state) && Array.isArray(details.state.workers)) {
|
|
389
|
+
const workers = details.state.workers.filter(isRecord);
|
|
390
|
+
const active = workers.filter((worker) => ["starting", "running", "stopping"].includes(String(worker.status))).length;
|
|
391
|
+
const ready = workers.filter((worker) => worker.status === "ready").length;
|
|
392
|
+
const diagnostics = isRecord(details.catalog) && Array.isArray(details.catalog.diagnostics) ? details.catalog.diagnostics.length : 0;
|
|
393
|
+
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);
|
|
394
|
+
return new Text(theme.fg("muted", facts.join(" · ")), 0, 0);
|
|
395
|
+
}
|
|
396
|
+
return new Text(theme.fg("muted", firstResultLine(result) || "Diagnostics unavailable"), 0, 0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function renderSimpleResult(
|
|
400
|
+
result: AgentToolResult<unknown>,
|
|
401
|
+
isError: boolean,
|
|
402
|
+
message: string,
|
|
403
|
+
theme: Theme,
|
|
404
|
+
normalColor: "success" | "warning" = "success",
|
|
405
|
+
): Text {
|
|
406
|
+
const text = isError ? firstResultLine(result) || message : message;
|
|
407
|
+
return new Text(theme.fg(isError ? "error" : normalColor, text), 0, 0);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function firstResultLine(result: AgentToolResult<unknown>): string | undefined {
|
|
411
|
+
const first = result.content[0];
|
|
412
|
+
if (first?.type !== "text") return undefined;
|
|
413
|
+
return first.text.split("\n").find((line) => line.trim())?.trim();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function objectFields(value: unknown): Record<string, unknown> {
|
|
417
|
+
return isRecord(value) ? value : {};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
421
|
+
return typeof value === "object" && value !== null;
|
|
422
|
+
}
|