@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.
- package/extension/catalog/definition.ts +89 -0
- package/extension/{catalog.ts → catalog/discovery.ts} +4 -4
- package/extension/index.ts +22 -51
- package/extension/orchestration/admission.ts +297 -0
- package/extension/{domain.ts → orchestration/model.ts} +27 -89
- package/extension/{runtime.ts → orchestration/service.ts} +222 -523
- package/extension/{worker-settlement.ts → orchestration/settlement.ts} +67 -21
- package/extension/package-root.ts +3 -0
- package/extension/{contract.ts → parent/contract.ts} +75 -9
- package/extension/{delivery.ts → parent/delivery.ts} +19 -16
- package/extension/parent/dispatch-policy.ts +49 -0
- package/extension/{host.ts → parent/process-host.ts} +30 -31
- package/extension/{presentation.ts → pi/presentation.ts} +41 -38
- 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} +23 -287
- package/package.json +2 -1
- package/extension/tools.ts +0 -771
- /package/extension/{tui.ts → pi/tui.ts} +0 -0
|
@@ -15,9 +15,12 @@ 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.js";
|
|
23
|
+
import type { OwnerSnapshot } from "../orchestration/service.js";
|
|
21
24
|
import {
|
|
22
25
|
disposeComponent,
|
|
23
26
|
formatElapsed,
|
|
@@ -25,9 +28,9 @@ import {
|
|
|
25
28
|
WidthBoundComponent,
|
|
26
29
|
} from "./tui.js";
|
|
27
30
|
import {
|
|
28
|
-
|
|
29
|
-
type
|
|
30
|
-
} from "
|
|
31
|
+
decodePersistedWorkerSettlement,
|
|
32
|
+
type WorkerSettlement,
|
|
33
|
+
} from "../orchestration/settlement.js";
|
|
31
34
|
|
|
32
35
|
export const ORCHESTRATION_PRESENTATION_KEY = "pi-orchestrate";
|
|
33
36
|
export const MAX_RESULT_PREVIEW_LINES = 6;
|
|
@@ -51,10 +54,11 @@ const ANIMATION_CYCLE_TICKS = 40;
|
|
|
51
54
|
const SPINNER_INTERVAL_MS = 140;
|
|
52
55
|
const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running", "stopping"]);
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
/** Owner-scoped worker state feed consumed by the parent's status presentation. */
|
|
58
|
+
export interface WorkerStateSource {
|
|
55
59
|
subscribeState(
|
|
56
60
|
ownerSessionId: string,
|
|
57
|
-
listener: (snapshot:
|
|
61
|
+
listener: (snapshot: OwnerSnapshot) => void,
|
|
58
62
|
): () => void;
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -66,14 +70,14 @@ interface StatusBinding {
|
|
|
66
70
|
interface RenderRequester { requestRender(): void }
|
|
67
71
|
|
|
68
72
|
export function registerOrchestrationPresentation(pi: ExtensionAPI): void {
|
|
69
|
-
pi.registerMessageRenderer<
|
|
73
|
+
pi.registerMessageRenderer<WorkerSettlement>(
|
|
70
74
|
"pi-orchestrate-worker-result",
|
|
71
75
|
(message, { expanded }, theme) =>
|
|
72
76
|
new WorkerResultComponent(messageText(message.content), message.details, expanded, theme),
|
|
73
77
|
);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
export function formatFooterStatus(snapshot:
|
|
80
|
+
export function formatFooterStatus(snapshot: OwnerSnapshot): string | undefined {
|
|
77
81
|
const ready = snapshot.workers.filter((worker) => worker.status === "ready").length;
|
|
78
82
|
return ready > 0 ? `${ready} interactive ready` : undefined;
|
|
79
83
|
}
|
|
@@ -83,17 +87,15 @@ export class StatusController {
|
|
|
83
87
|
private disposed = false;
|
|
84
88
|
private unsubscribeState: (() => void) | undefined;
|
|
85
89
|
private widget: WorkerStatusComponent | undefined;
|
|
86
|
-
private widgetInstalled = false;
|
|
87
|
-
private pendingSnapshot: RuntimeSnapshot | undefined;
|
|
88
90
|
|
|
89
|
-
constructor(private readonly
|
|
91
|
+
constructor(private readonly workerState: WorkerStateSource) {}
|
|
90
92
|
|
|
91
93
|
bind(ownerSessionId: string, ctx: ExtensionContext): void {
|
|
92
94
|
if (this.disposed) return;
|
|
93
95
|
this.clearBinding();
|
|
94
96
|
const binding = { ownerSessionId, ctx };
|
|
95
97
|
this.binding = binding;
|
|
96
|
-
this.unsubscribeState = this.
|
|
98
|
+
this.unsubscribeState = this.workerState.subscribeState(ownerSessionId, (snapshot) => {
|
|
97
99
|
if (this.binding !== binding) return;
|
|
98
100
|
this.present(binding.ctx, snapshot);
|
|
99
101
|
});
|
|
@@ -110,59 +112,60 @@ export class StatusController {
|
|
|
110
112
|
this.clearBinding();
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
private present(ctx: ExtensionContext, snapshot:
|
|
115
|
+
private present(ctx: ExtensionContext, snapshot: OwnerSnapshot): void {
|
|
114
116
|
ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, formatFooterStatus(snapshot));
|
|
115
117
|
if (ctx.mode !== "tui") return;
|
|
116
118
|
const active = activeWorkers(snapshot);
|
|
117
|
-
this.pendingSnapshot = snapshot;
|
|
118
119
|
if (active.length === 0) {
|
|
119
|
-
if (this.
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
if (this.widget !== undefined) {
|
|
121
|
+
ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
122
|
+
this.widget = undefined;
|
|
123
|
+
}
|
|
122
124
|
return;
|
|
123
125
|
}
|
|
124
|
-
if (this.widget) {
|
|
126
|
+
if (this.widget !== undefined) {
|
|
125
127
|
this.widget.update(snapshot);
|
|
126
128
|
return;
|
|
127
129
|
}
|
|
128
|
-
if (this.widgetInstalled) return;
|
|
129
130
|
ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, (tui, theme) => {
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
const widget = new WorkerStatusComponent(snapshot, theme, tui);
|
|
132
|
+
this.widget = widget;
|
|
133
|
+
return widget;
|
|
132
134
|
}, { placement: "aboveEditor" });
|
|
133
|
-
this.widgetInstalled = true;
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
private clearBinding(): void {
|
|
137
138
|
const unsubscribeState = this.unsubscribeState;
|
|
138
139
|
this.unsubscribeState = undefined;
|
|
139
140
|
unsubscribeState?.();
|
|
140
|
-
this.widget = undefined;
|
|
141
|
-
this.pendingSnapshot = undefined;
|
|
142
141
|
const current = this.binding;
|
|
143
142
|
if (!current) return;
|
|
144
143
|
current.ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
145
|
-
if (current.ctx.mode === "tui" && this.
|
|
146
|
-
|
|
144
|
+
if (current.ctx.mode === "tui" && this.widget !== undefined) {
|
|
145
|
+
current.ctx.ui.setWidget(ORCHESTRATION_PRESENTATION_KEY, undefined);
|
|
146
|
+
this.widget = undefined;
|
|
147
|
+
}
|
|
147
148
|
this.binding = undefined;
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
export function createStatusController(
|
|
152
|
-
|
|
152
|
+
export function createStatusController(
|
|
153
|
+
workerState: WorkerStateSource,
|
|
154
|
+
): StatusController {
|
|
155
|
+
return new StatusController(workerState);
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
export class WorkerStatusComponent implements Component {
|
|
156
159
|
private frameIndex = 0;
|
|
157
|
-
private snapshot:
|
|
160
|
+
private snapshot: OwnerSnapshot;
|
|
158
161
|
private timer: ReturnType<typeof setInterval> | undefined;
|
|
159
162
|
|
|
160
|
-
constructor(snapshot:
|
|
163
|
+
constructor(snapshot: OwnerSnapshot, private readonly theme: Theme, private readonly tui?: RenderRequester) {
|
|
161
164
|
this.snapshot = snapshot;
|
|
162
165
|
this.startTimer();
|
|
163
166
|
}
|
|
164
167
|
|
|
165
|
-
update(snapshot:
|
|
168
|
+
update(snapshot: OwnerSnapshot): void {
|
|
166
169
|
this.snapshot = snapshot;
|
|
167
170
|
if (activeWorkers(snapshot).length > 0) this.startTimer();
|
|
168
171
|
else this.stopTimer();
|
|
@@ -292,8 +295,8 @@ export class WorkerResultComponent implements Component {
|
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
|
|
295
|
-
function readSettlement(value: unknown):
|
|
296
|
-
const decoded =
|
|
298
|
+
function readSettlement(value: unknown): WorkerSettlement | undefined {
|
|
299
|
+
const decoded = decodePersistedWorkerSettlement(value);
|
|
297
300
|
return Result.isSuccess(decoded) ? decoded.success : undefined;
|
|
298
301
|
}
|
|
299
302
|
|
|
@@ -310,7 +313,7 @@ function outcomeText(outcome: WorkerOutcome): string {
|
|
|
310
313
|
return "Worker session closed.";
|
|
311
314
|
}
|
|
312
315
|
|
|
313
|
-
function presentedOutcome(result:
|
|
316
|
+
function presentedOutcome(result: WorkerSettlement): string {
|
|
314
317
|
const body = outcomeText(result.outcome);
|
|
315
318
|
if (result.status !== "completed" && result.status !== "ready") return body;
|
|
316
319
|
|
|
@@ -325,7 +328,7 @@ function presentedOutcome(result: WorkerSettlementDetails): string {
|
|
|
325
328
|
return lines.join("\n").trimEnd();
|
|
326
329
|
}
|
|
327
330
|
|
|
328
|
-
function settlementMetadata(result:
|
|
331
|
+
function settlementMetadata(result: WorkerSettlement): string[] {
|
|
329
332
|
return [
|
|
330
333
|
`worker ID ${result.workerId} · run ID ${result.runId}`,
|
|
331
334
|
`status ${result.status} · generation ${result.generation}`,
|
|
@@ -341,7 +344,7 @@ function messageText(content: unknown): string {
|
|
|
341
344
|
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
|
|
342
345
|
}
|
|
343
346
|
|
|
344
|
-
function activeWorkers(snapshot:
|
|
347
|
+
function activeWorkers(snapshot: OwnerSnapshot): WorkerRecord[] {
|
|
345
348
|
return snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status));
|
|
346
349
|
}
|
|
347
350
|
|
|
@@ -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.js";
|
|
23
|
+
import {
|
|
24
|
+
decodeInlineWorkerToolDetails,
|
|
25
|
+
type InlineWorkerSettlementDetails,
|
|
26
|
+
WorkerSettlement,
|
|
27
|
+
} from "../orchestration/settlement.js";
|
|
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
|
+
}
|