@zachwill/pi-orchestrate 0.8.0 → 0.9.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.ts +70 -75
- package/extension/delivery.ts +49 -7
- package/extension/domain.ts +83 -58
- package/extension/host.ts +321 -31
- package/extension/index.ts +33 -26
- package/extension/presentation.ts +36 -83
- package/extension/runtime.ts +1702 -884
- package/extension/tools.ts +143 -243
- package/extension/tui.ts +50 -0
- package/extension/worker-session.ts +514 -188
- package/extension/worker-settlement.ts +105 -44
- package/package.json +1 -1
- package/extension/scheduler.ts +0 -85
package/extension/tools.ts
CHANGED
|
@@ -20,13 +20,15 @@ import {
|
|
|
20
20
|
keyHint,
|
|
21
21
|
truncateHead,
|
|
22
22
|
} from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { Result, Schema } from "effect";
|
|
23
24
|
import { Type } from "typebox";
|
|
24
25
|
import {
|
|
26
|
+
MAX_WORKER_INSTRUCTIONS_LENGTH,
|
|
27
|
+
MAX_WORKER_TITLE_LENGTH,
|
|
25
28
|
type CatalogDiagnostic,
|
|
26
29
|
type RunRecord,
|
|
27
30
|
type WorkerCatalog,
|
|
28
31
|
type WorkerDefinition,
|
|
29
|
-
type WorkerId,
|
|
30
32
|
type WorkerOutcome,
|
|
31
33
|
type WorkerRecord,
|
|
32
34
|
type WorkerUsage,
|
|
@@ -36,33 +38,68 @@ import type {
|
|
|
36
38
|
AcceptedRun,
|
|
37
39
|
CompletedRun,
|
|
38
40
|
OrchestrationContext,
|
|
39
|
-
OrchestratorRuntime,
|
|
40
41
|
RunResult,
|
|
41
42
|
RuntimeSnapshot,
|
|
42
43
|
SettlementListener,
|
|
43
|
-
WorkerSettlement,
|
|
44
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";
|
|
45
59
|
|
|
46
60
|
const STRICT_OBJECT = { additionalProperties: false } as const;
|
|
47
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
|
+
);
|
|
48
87
|
|
|
49
88
|
const taskSchema = Type.Object(
|
|
50
89
|
{
|
|
51
|
-
worker:
|
|
52
|
-
title:
|
|
53
|
-
instructions:
|
|
90
|
+
worker: shortTextSchema,
|
|
91
|
+
title: shortTextSchema,
|
|
92
|
+
instructions: instructionsSchema,
|
|
54
93
|
},
|
|
55
94
|
STRICT_OBJECT,
|
|
56
95
|
);
|
|
57
96
|
|
|
58
|
-
const orchestrateSchema = taskSchema;
|
|
59
|
-
|
|
60
97
|
const statusSchema = Type.Object({}, STRICT_OBJECT);
|
|
61
98
|
|
|
62
99
|
const interactiveSendSchema = Type.Object(
|
|
63
100
|
{
|
|
64
|
-
worker_id:
|
|
65
|
-
instructions:
|
|
101
|
+
worker_id: workerIdSchema,
|
|
102
|
+
instructions: instructionsSchema,
|
|
66
103
|
},
|
|
67
104
|
STRICT_OBJECT,
|
|
68
105
|
);
|
|
@@ -70,7 +107,7 @@ const interactiveSendSchema = Type.Object(
|
|
|
70
107
|
const workerAbortSchema = Type.Union([
|
|
71
108
|
Type.Object(
|
|
72
109
|
{
|
|
73
|
-
worker_ids: Type.Array(
|
|
110
|
+
worker_ids: Type.Array(workerIdSchema, { minItems: 1 }),
|
|
74
111
|
},
|
|
75
112
|
STRICT_OBJECT,
|
|
76
113
|
),
|
|
@@ -84,7 +121,7 @@ const workerAbortSchema = Type.Union([
|
|
|
84
121
|
|
|
85
122
|
const interactiveCloseSchema = Type.Object(
|
|
86
123
|
{
|
|
87
|
-
worker_id:
|
|
124
|
+
worker_id: workerIdSchema,
|
|
88
125
|
},
|
|
89
126
|
STRICT_OBJECT,
|
|
90
127
|
);
|
|
@@ -99,7 +136,7 @@ export interface DispatchDecision {
|
|
|
99
136
|
|
|
100
137
|
export interface OrchestrationToolDependencies {
|
|
101
138
|
readonly runtime: OrchestratorRuntime;
|
|
102
|
-
getCatalog(ctx: ExtensionContext): WorkerCatalog
|
|
139
|
+
getCatalog(ctx: ExtensionContext): WorkerCatalog;
|
|
103
140
|
getDispatchDecision(toolCallId: string): DispatchDecision;
|
|
104
141
|
}
|
|
105
142
|
|
|
@@ -120,7 +157,7 @@ export function registerOrchestrationTools(
|
|
|
120
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.",
|
|
121
158
|
],
|
|
122
159
|
executionMode: "parallel",
|
|
123
|
-
parameters:
|
|
160
|
+
parameters: taskSchema,
|
|
124
161
|
renderCall(args, theme, { expanded }) {
|
|
125
162
|
return renderDispatchCall(theme, args, expanded);
|
|
126
163
|
},
|
|
@@ -130,7 +167,7 @@ export function registerOrchestrationTools(
|
|
|
130
167
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
131
168
|
const decision = deps.getDispatchDecision(toolCallId);
|
|
132
169
|
const mode = decision.mode;
|
|
133
|
-
const runtimeContext =
|
|
170
|
+
const runtimeContext = buildRuntimeContext(ctx, deps, decision.synthesisGroup);
|
|
134
171
|
if (mode === "async") {
|
|
135
172
|
const acceptedRun = await deps.runtime.orchestrate(
|
|
136
173
|
runtimeContext,
|
|
@@ -140,13 +177,7 @@ export function registerOrchestrationTools(
|
|
|
140
177
|
);
|
|
141
178
|
const readable = acceptedRunDetails(acceptedRun);
|
|
142
179
|
return {
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
type: "text",
|
|
146
|
-
text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
|
|
147
|
-
},
|
|
148
|
-
],
|
|
149
|
-
details: readable,
|
|
180
|
+
...readableToolResult(`Accepted async run ${readable.run_id}.`, readable),
|
|
150
181
|
terminate: true,
|
|
151
182
|
};
|
|
152
183
|
}
|
|
@@ -159,18 +190,10 @@ export function registerOrchestrationTools(
|
|
|
159
190
|
createInlineSettlementListener(onUpdate),
|
|
160
191
|
);
|
|
161
192
|
const readable = completedRunDetails(completedRun);
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
text: readableDetails(
|
|
167
|
-
`Completed inline run ${readable.run_id}.`,
|
|
168
|
-
readable,
|
|
169
|
-
),
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
details: readable,
|
|
173
|
-
};
|
|
193
|
+
return readableToolResult(
|
|
194
|
+
`Completed inline run ${readable.run_id}.`,
|
|
195
|
+
readable,
|
|
196
|
+
);
|
|
174
197
|
},
|
|
175
198
|
});
|
|
176
199
|
|
|
@@ -191,24 +214,14 @@ export function registerOrchestrationTools(
|
|
|
191
214
|
return renderDiagnosticsResult(result, isPartial, theme);
|
|
192
215
|
},
|
|
193
216
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
194
|
-
const ownerSessionId =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
);
|
|
198
|
-
const [catalog, snapshot] = await Promise.all([
|
|
199
|
-
deps.getCatalog(ctx),
|
|
200
|
-
deps.runtime.snapshot(ownerSessionId),
|
|
201
|
-
]);
|
|
217
|
+
const ownerSessionId = ctx.sessionManager.getSessionId();
|
|
218
|
+
const catalog = deps.getCatalog(ctx);
|
|
219
|
+
const snapshot = await deps.runtime.snapshot(ownerSessionId);
|
|
202
220
|
const readable = statusDetails(catalog, snapshot);
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
text: readableDetails("Worker diagnostics and recovery snapshot.", readable),
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
details: readable,
|
|
211
|
-
};
|
|
221
|
+
return readableToolResult(
|
|
222
|
+
"Worker diagnostics and recovery snapshot.",
|
|
223
|
+
readable,
|
|
224
|
+
);
|
|
212
225
|
},
|
|
213
226
|
});
|
|
214
227
|
|
|
@@ -229,9 +242,9 @@ export function registerOrchestrationTools(
|
|
|
229
242
|
return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
|
|
230
243
|
},
|
|
231
244
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
232
|
-
const workerId =
|
|
245
|
+
const workerId = params.worker_id;
|
|
233
246
|
const mode = deps.getDispatchDecision(toolCallId).mode;
|
|
234
|
-
const runtimeContext =
|
|
247
|
+
const runtimeContext = buildRuntimeContext(ctx, deps);
|
|
235
248
|
if (mode === "async") {
|
|
236
249
|
const acceptedRun = await deps.runtime.sendInteractive(
|
|
237
250
|
runtimeContext,
|
|
@@ -242,13 +255,7 @@ export function registerOrchestrationTools(
|
|
|
242
255
|
);
|
|
243
256
|
const readable = acceptedRunDetails(acceptedRun);
|
|
244
257
|
return {
|
|
245
|
-
|
|
246
|
-
{
|
|
247
|
-
type: "text",
|
|
248
|
-
text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
details: readable,
|
|
258
|
+
...readableToolResult(`Accepted async run ${readable.run_id}.`, readable),
|
|
252
259
|
terminate: true,
|
|
253
260
|
};
|
|
254
261
|
}
|
|
@@ -262,18 +269,10 @@ export function registerOrchestrationTools(
|
|
|
262
269
|
createInlineSettlementListener(onUpdate),
|
|
263
270
|
);
|
|
264
271
|
const readable = completedRunDetails(completedRun);
|
|
265
|
-
return
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
text: readableDetails(
|
|
270
|
-
`Completed inline run ${readable.run_id}.`,
|
|
271
|
-
readable,
|
|
272
|
-
),
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
details: readable,
|
|
276
|
-
};
|
|
272
|
+
return readableToolResult(
|
|
273
|
+
`Completed inline run ${readable.run_id}.`,
|
|
274
|
+
readable,
|
|
275
|
+
);
|
|
277
276
|
},
|
|
278
277
|
});
|
|
279
278
|
|
|
@@ -297,22 +296,15 @@ export function registerOrchestrationTools(
|
|
|
297
296
|
return renderSimpleResult(result, isPartial ? "Requesting worker stop…" : "Worker stop requested", theme, "warning");
|
|
298
297
|
},
|
|
299
298
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
300
|
-
const ownerSessionId =
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
content: [
|
|
309
|
-
{
|
|
310
|
-
type: "text",
|
|
311
|
-
text: readableDetails("Abort request completed.", readable),
|
|
312
|
-
},
|
|
313
|
-
],
|
|
314
|
-
details: { target: target.external },
|
|
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 },
|
|
315
306
|
};
|
|
307
|
+
return readableToolResult("Abort request completed.", readable);
|
|
316
308
|
},
|
|
317
309
|
});
|
|
318
310
|
|
|
@@ -332,41 +324,27 @@ export function registerOrchestrationTools(
|
|
|
332
324
|
return renderSimpleResult(result, isPartial ? "Closing worker…" : "✓ Worker closed", theme);
|
|
333
325
|
},
|
|
334
326
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
335
|
-
const ownerSessionId =
|
|
336
|
-
|
|
337
|
-
ctx.sessionManager.getSessionId(),
|
|
338
|
-
);
|
|
339
|
-
const workerId = asWorkerId(params.worker_id);
|
|
327
|
+
const ownerSessionId = ctx.sessionManager.getSessionId();
|
|
328
|
+
const workerId = params.worker_id;
|
|
340
329
|
await deps.runtime.closeInteractive(ownerSessionId, workerId);
|
|
341
330
|
const readable = { worker_id: workerId };
|
|
342
|
-
return {
|
|
343
|
-
content: [
|
|
344
|
-
{
|
|
345
|
-
type: "text",
|
|
346
|
-
text: readableDetails(`Closed worker ${workerId}.`, readable),
|
|
347
|
-
},
|
|
348
|
-
],
|
|
349
|
-
details: readable,
|
|
350
|
-
};
|
|
331
|
+
return readableToolResult(`Closed worker ${workerId}.`, readable);
|
|
351
332
|
},
|
|
352
333
|
});
|
|
353
334
|
}
|
|
354
335
|
|
|
355
|
-
|
|
336
|
+
function buildRuntimeContext(
|
|
356
337
|
ctx: ExtensionContext,
|
|
357
338
|
deps: OrchestrationToolDependencies,
|
|
358
339
|
synthesisGroup?: DispatchDecision["synthesisGroup"],
|
|
359
|
-
):
|
|
340
|
+
): OrchestrationContext {
|
|
360
341
|
return {
|
|
361
|
-
ownerSessionId:
|
|
362
|
-
"owner session ID",
|
|
363
|
-
ctx.sessionManager.getSessionId(),
|
|
364
|
-
),
|
|
342
|
+
ownerSessionId: ctx.sessionManager.getSessionId(),
|
|
365
343
|
cwd: ctx.cwd,
|
|
366
344
|
agentDir: getAgentDir(),
|
|
367
345
|
parentSessionFile: ctx.sessionManager.getSessionFile(),
|
|
368
346
|
projectTrusted: ctx.isProjectTrusted(),
|
|
369
|
-
catalog:
|
|
347
|
+
catalog: deps.getCatalog(ctx),
|
|
370
348
|
parentModel: ctx.model,
|
|
371
349
|
modelRegistry: ctx.modelRegistry,
|
|
372
350
|
...(synthesisGroup ? { synthesisGroup } : {}),
|
|
@@ -379,53 +357,21 @@ function createInlineSettlementListener(
|
|
|
379
357
|
return (settlement) => {
|
|
380
358
|
onUpdate?.({
|
|
381
359
|
content: [{ type: "text", text: "Worker response received." }],
|
|
382
|
-
details: {
|
|
360
|
+
details: encodeInlineWorkerToolDetails({
|
|
383
361
|
mode: "inline",
|
|
384
|
-
result:
|
|
385
|
-
},
|
|
362
|
+
result: inlineResultValue(settlement),
|
|
363
|
+
}),
|
|
386
364
|
});
|
|
387
365
|
};
|
|
388
366
|
}
|
|
389
367
|
|
|
390
|
-
function
|
|
391
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
392
|
-
throw new Error(`${name} must not be blank`);
|
|
393
|
-
}
|
|
394
|
-
return value;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function asWorkerId(value: string): WorkerId {
|
|
398
|
-
return requireNonblank("worker_id", value) as WorkerId;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function abortTarget(params: {
|
|
368
|
+
function normalizeAbortTarget(params: {
|
|
402
369
|
worker_ids?: string[];
|
|
403
|
-
all?:
|
|
404
|
-
}): {
|
|
405
|
-
runtime: AbortTarget;
|
|
406
|
-
external: { worker_ids: readonly WorkerId[] } | { all: true };
|
|
407
|
-
} {
|
|
408
|
-
const selectedTargetCount = [
|
|
409
|
-
params.worker_ids !== undefined,
|
|
410
|
-
params.all !== undefined,
|
|
411
|
-
].filter(Boolean).length;
|
|
412
|
-
if (selectedTargetCount !== 1 || (params.all !== undefined && params.all !== true)) {
|
|
413
|
-
throw new Error("Abort target must specify exactly one target");
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (params.worker_ids !== undefined) {
|
|
417
|
-
if (!Array.isArray(params.worker_ids) || params.worker_ids.length === 0) {
|
|
418
|
-
throw new Error("worker_ids must contain at least one worker ID");
|
|
419
|
-
}
|
|
420
|
-
const workerIds = params.worker_ids.map(asWorkerId);
|
|
421
|
-
return {
|
|
422
|
-
runtime: { workerIds },
|
|
423
|
-
external: { worker_ids: workerIds },
|
|
424
|
-
};
|
|
425
|
-
}
|
|
370
|
+
all?: boolean;
|
|
371
|
+
}): AbortTarget {
|
|
426
372
|
return {
|
|
427
|
-
|
|
428
|
-
|
|
373
|
+
...(params.worker_ids !== undefined ? { workerIds: params.worker_ids } : {}),
|
|
374
|
+
...(params.all !== undefined ? { all: params.all } : {}),
|
|
429
375
|
};
|
|
430
376
|
}
|
|
431
377
|
|
|
@@ -438,39 +384,29 @@ function acceptedRunDetails(run: AcceptedRun) {
|
|
|
438
384
|
}
|
|
439
385
|
|
|
440
386
|
function completedRunDetails(run: CompletedRun) {
|
|
441
|
-
return {
|
|
442
|
-
mode:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
result:
|
|
446
|
-
};
|
|
387
|
+
return encodeInlineWorkerToolDetails({
|
|
388
|
+
mode: "inline",
|
|
389
|
+
runId: run.id,
|
|
390
|
+
ownerSessionId: run.ownerSessionId,
|
|
391
|
+
result: inlineResultValue(run.result),
|
|
392
|
+
});
|
|
447
393
|
}
|
|
448
394
|
|
|
449
|
-
function
|
|
395
|
+
function inlineResultValue(
|
|
396
|
+
result: RunResult | WorkerSettlement,
|
|
397
|
+
): InlineWorkerSettlementDetails {
|
|
450
398
|
return {
|
|
451
|
-
|
|
399
|
+
workerId: result.workerId,
|
|
452
400
|
worker: result.worker,
|
|
453
401
|
title: result.title,
|
|
454
402
|
status: result.status,
|
|
455
|
-
outcome:
|
|
456
|
-
usage:
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function inlineResultDetails(settlement: WorkerSettlement) {
|
|
464
|
-
return {
|
|
465
|
-
worker_id: settlement.workerId,
|
|
466
|
-
worker: settlement.worker,
|
|
467
|
-
title: settlement.title,
|
|
468
|
-
status: settlement.status,
|
|
469
|
-
outcome: outcomeDetails(settlement.outcome),
|
|
470
|
-
usage: usageDetails(settlement.usage),
|
|
471
|
-
started_at: settlement.startedAt,
|
|
472
|
-
settled_at: settlement.settledAt,
|
|
473
|
-
session_file: settlement.sessionFile,
|
|
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 }),
|
|
474
410
|
};
|
|
475
411
|
}
|
|
476
412
|
|
|
@@ -580,6 +516,13 @@ function outcomeDetails(outcome: WorkerOutcome) {
|
|
|
580
516
|
}
|
|
581
517
|
}
|
|
582
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
|
+
|
|
583
526
|
function readableDetails(title: string, details: unknown): string {
|
|
584
527
|
const content = `${title}\n\n${JSON.stringify(details, null, 2)}`;
|
|
585
528
|
const truncation = truncateHead(content);
|
|
@@ -659,18 +602,6 @@ function renderInteractiveMessageCall(
|
|
|
659
602
|
return new WidthBoundComponent(container);
|
|
660
603
|
}
|
|
661
604
|
|
|
662
|
-
class WidthBoundComponent implements Component {
|
|
663
|
-
constructor(private readonly child: Component, private readonly maxLines?: number) {}
|
|
664
|
-
render(width: number): string[] {
|
|
665
|
-
const bounded = Math.max(1, Math.floor(width));
|
|
666
|
-
const lines = this.child.render(bounded);
|
|
667
|
-
return (this.maxLines === undefined ? lines : lines.slice(0, this.maxLines))
|
|
668
|
-
.map((line) => truncateToWidth(line, bounded, "…"));
|
|
669
|
-
}
|
|
670
|
-
invalidate(): void { this.child.invalidate(); }
|
|
671
|
-
dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
|
|
672
|
-
}
|
|
673
|
-
|
|
674
605
|
function safeTerminalText(value: unknown): string {
|
|
675
606
|
const text = typeof value === "string" ? value : value == null ? "" : String(value);
|
|
676
607
|
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
|
|
@@ -714,7 +645,7 @@ function renderOrchestrationResult(
|
|
|
714
645
|
lastComponent: unknown,
|
|
715
646
|
): Component {
|
|
716
647
|
const details = result.details;
|
|
717
|
-
if (
|
|
648
|
+
if (Result.isSuccess(decodeAcceptedRunRenderDetails(details))) {
|
|
718
649
|
return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
|
|
719
650
|
}
|
|
720
651
|
const inlineResult = readInlineResult(details);
|
|
@@ -725,28 +656,28 @@ function renderOrchestrationResult(
|
|
|
725
656
|
component.update(inlineResult, isPartial, expanded);
|
|
726
657
|
return component;
|
|
727
658
|
}
|
|
728
|
-
if (
|
|
659
|
+
if (Result.isSuccess(decodeUnavailableWorkerRenderDetails(details))) {
|
|
729
660
|
return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
|
|
730
661
|
}
|
|
731
662
|
if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
|
|
732
663
|
return new WidthBoundComponent(renderSimpleResult(result, firstResultLine(result) || "Work sent", theme, "warning"));
|
|
733
664
|
}
|
|
734
665
|
|
|
735
|
-
interface
|
|
666
|
+
interface RenderedInlineSettlement {
|
|
736
667
|
worker: string;
|
|
737
668
|
title: string;
|
|
738
|
-
status: "
|
|
669
|
+
status: InlineWorkerSettlementDetails["status"];
|
|
739
670
|
response: string;
|
|
740
671
|
elapsed?: string;
|
|
741
672
|
}
|
|
742
673
|
|
|
743
674
|
class InlineResultComponent implements Component {
|
|
744
|
-
private result:
|
|
675
|
+
private result: RenderedInlineSettlement | undefined;
|
|
745
676
|
private partial = false;
|
|
746
677
|
private expanded = false;
|
|
747
678
|
private child: Component = new Container();
|
|
748
679
|
constructor(private readonly theme: Theme) {}
|
|
749
|
-
update(result:
|
|
680
|
+
update(result: RenderedInlineSettlement, partial: boolean, expanded: boolean): void {
|
|
750
681
|
this.result = result;
|
|
751
682
|
this.partial = partial;
|
|
752
683
|
this.expanded = expanded;
|
|
@@ -754,16 +685,16 @@ class InlineResultComponent implements Component {
|
|
|
754
685
|
}
|
|
755
686
|
render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
|
|
756
687
|
invalidate(): void { this.rebuild(); }
|
|
757
|
-
dispose(): void { (this.child
|
|
688
|
+
dispose(): void { disposeComponent(this.child); }
|
|
758
689
|
private rebuild(): void {
|
|
759
|
-
(this.child
|
|
690
|
+
disposeComponent(this.child);
|
|
760
691
|
const container = new Container();
|
|
761
692
|
const result = this.result;
|
|
762
693
|
if (!result) {
|
|
763
694
|
this.child = container;
|
|
764
695
|
return;
|
|
765
696
|
}
|
|
766
|
-
const appearance =
|
|
697
|
+
const appearance = resultAppearance(result.status, "ready for follow-up");
|
|
767
698
|
const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
|
|
768
699
|
const title = this.theme.bold(result.title);
|
|
769
700
|
const workerName = this.theme.fg("muted", this.theme.italic(result.worker));
|
|
@@ -784,58 +715,27 @@ class InlineResultComponent implements Component {
|
|
|
784
715
|
}
|
|
785
716
|
}
|
|
786
717
|
|
|
787
|
-
function readInlineResult(details: unknown):
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const outcome = value.outcome;
|
|
795
|
-
const statuses = ["completed", "ready", "failed", "aborted"] as const;
|
|
796
|
-
const status = statuses.find((item) => item === value.status);
|
|
797
|
-
const outcomeStatus = statuses.find((item) => item === outcome.status);
|
|
798
|
-
if (!status || outcomeStatus !== status) return undefined;
|
|
799
|
-
const message = outcome.message;
|
|
800
|
-
const assistantText = outcome.assistant_text;
|
|
801
|
-
if (message !== undefined && typeof message !== "string") return undefined;
|
|
802
|
-
if (assistantText !== undefined && typeof assistantText !== "string") return undefined;
|
|
803
|
-
if ((status === "completed" || status === "ready") && typeof assistantText !== "string") return undefined;
|
|
804
|
-
if (status === "failed" && typeof message !== "string") return undefined;
|
|
805
|
-
const startedAt = value.started_at;
|
|
806
|
-
const settledAt = value.settled_at;
|
|
807
|
-
const elapsed = typeof startedAt === "number" && typeof settledAt === "number" && settledAt >= startedAt
|
|
808
|
-
? formatElapsed(settledAt - startedAt)
|
|
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
|
|
809
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");
|
|
810
730
|
return {
|
|
811
|
-
worker:
|
|
812
|
-
title:
|
|
813
|
-
status,
|
|
814
|
-
response
|
|
815
|
-
|
|
731
|
+
worker: settlement.worker,
|
|
732
|
+
title: settlement.title,
|
|
733
|
+
status: settlement.status,
|
|
734
|
+
response,
|
|
735
|
+
elapsed: formatElapsed(settlement.settledAt - settlement.startedAt),
|
|
816
736
|
};
|
|
817
737
|
}
|
|
818
738
|
|
|
819
|
-
function inlineResultAppearance(status: InlineSettlement["status"]): {
|
|
820
|
-
readonly color: "success" | "error" | "warning";
|
|
821
|
-
readonly icon: "✓" | "✗" | "■";
|
|
822
|
-
readonly qualifier?: string;
|
|
823
|
-
} {
|
|
824
|
-
if (status === "failed") return { color: "error", icon: "✗", qualifier: "failed" };
|
|
825
|
-
if (status === "aborted") return { color: "warning", icon: "■", qualifier: "aborted" };
|
|
826
|
-
if (status === "ready") {
|
|
827
|
-
return { color: "success", icon: "✓", qualifier: "ready for follow-up" };
|
|
828
|
-
}
|
|
829
|
-
return { color: "success", icon: "✓" };
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function formatElapsed(milliseconds: number): string {
|
|
833
|
-
const seconds = Math.floor(milliseconds / 1000);
|
|
834
|
-
if (seconds < 60) return `${seconds}s`;
|
|
835
|
-
const minutes = Math.floor(seconds / 60);
|
|
836
|
-
return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
739
|
function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
|
|
840
740
|
if (isPartial) return new Text(theme.fg("muted", "Reading worker diagnostics…"), 0, 0);
|
|
841
741
|
const details = result.details;
|
package/extension/tui.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export class WidthBoundComponent implements Component {
|
|
4
|
+
constructor(
|
|
5
|
+
private readonly child: Component,
|
|
6
|
+
private readonly maxLines?: number,
|
|
7
|
+
) {}
|
|
8
|
+
|
|
9
|
+
render(width: number): string[] {
|
|
10
|
+
const bounded = Math.max(1, Math.floor(width));
|
|
11
|
+
const lines = this.child.render(bounded);
|
|
12
|
+
return (this.maxLines === undefined ? lines : lines.slice(0, this.maxLines))
|
|
13
|
+
.map((line) => truncateToWidth(line, bounded, "…"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
invalidate(): void { this.child.invalidate(); }
|
|
17
|
+
dispose(): void { disposeComponent(this.child); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function disposeComponent(component: Component): void {
|
|
21
|
+
(component as Component & { dispose?: () => void }).dispose?.();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resultAppearance(
|
|
25
|
+
status: "completed" | "ready" | "failed" | "aborted",
|
|
26
|
+
readyQualifier: string,
|
|
27
|
+
failedQualifier = "failed",
|
|
28
|
+
): {
|
|
29
|
+
readonly color: "success" | "error" | "warning";
|
|
30
|
+
readonly icon: "✓" | "✗" | "■";
|
|
31
|
+
readonly qualifier?: string;
|
|
32
|
+
} {
|
|
33
|
+
if (status === "failed") {
|
|
34
|
+
return { color: "error", icon: "✗", qualifier: failedQualifier };
|
|
35
|
+
}
|
|
36
|
+
if (status === "aborted") {
|
|
37
|
+
return { color: "warning", icon: "■", qualifier: "aborted" };
|
|
38
|
+
}
|
|
39
|
+
if (status === "ready") {
|
|
40
|
+
return { color: "success", icon: "✓", qualifier: readyQualifier };
|
|
41
|
+
}
|
|
42
|
+
return { color: "success", icon: "✓" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function formatElapsed(milliseconds: number): string {
|
|
46
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
47
|
+
if (seconds < 60) return `${seconds}s`;
|
|
48
|
+
const minutes = Math.floor(seconds / 60);
|
|
49
|
+
return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
|
|
50
|
+
}
|