@zachwill/pi-orchestrate 0.2.1 → 0.4.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 +77 -48
- package/examples/workers/investigator.md +5 -31
- package/examples/workers/scout.md +5 -26
- package/examples/workers/web.md +84 -0
- package/examples/workers/worker.md +9 -35
- package/extension/catalog.ts +40 -22
- package/extension/contract.ts +4 -4
- package/extension/delivery.ts +72 -16
- package/extension/domain.ts +21 -55
- package/extension/host.ts +1 -37
- package/extension/index.ts +39 -11
- package/extension/presentation.ts +62 -158
- package/extension/runtime.ts +239 -331
- package/extension/tools.ts +206 -214
- package/extension/worker-session.ts +213 -50
- package/extension/worker-settlement.ts +106 -0
- package/package.json +1 -1
package/extension/tools.ts
CHANGED
|
@@ -23,9 +23,7 @@ import {
|
|
|
23
23
|
import { Type } from "typebox";
|
|
24
24
|
import {
|
|
25
25
|
type CatalogDiagnostic,
|
|
26
|
-
type
|
|
27
|
-
type WaveId,
|
|
28
|
-
type WaveRecord,
|
|
26
|
+
type RunRecord,
|
|
29
27
|
type WorkerCatalog,
|
|
30
28
|
type WorkerDefinition,
|
|
31
29
|
type WorkerId,
|
|
@@ -35,16 +33,17 @@ import {
|
|
|
35
33
|
} from "./domain.js";
|
|
36
34
|
import type {
|
|
37
35
|
AbortTarget,
|
|
38
|
-
|
|
36
|
+
AcceptedRun,
|
|
39
37
|
CompletedResult,
|
|
40
|
-
|
|
38
|
+
CompletedRun,
|
|
41
39
|
OrchestrationContext,
|
|
42
40
|
OrchestratorRuntime,
|
|
43
41
|
RuntimeSnapshot,
|
|
42
|
+
SettlementListener,
|
|
43
|
+
WorkerSettlement,
|
|
44
44
|
} from "./runtime.js";
|
|
45
45
|
|
|
46
46
|
const STRICT_OBJECT = { additionalProperties: false } as const;
|
|
47
|
-
const MAX_TASKS_PER_WAVE = 12;
|
|
48
47
|
const MAX_INSTRUCTION_PREVIEW_LINES = 2;
|
|
49
48
|
|
|
50
49
|
const taskSchema = Type.Object(
|
|
@@ -56,15 +55,7 @@ const taskSchema = Type.Object(
|
|
|
56
55
|
STRICT_OBJECT,
|
|
57
56
|
);
|
|
58
57
|
|
|
59
|
-
const orchestrateSchema =
|
|
60
|
-
{
|
|
61
|
-
tasks: Type.Array(taskSchema, {
|
|
62
|
-
minItems: 1,
|
|
63
|
-
maxItems: MAX_TASKS_PER_WAVE,
|
|
64
|
-
}),
|
|
65
|
-
},
|
|
66
|
-
STRICT_OBJECT,
|
|
67
|
-
);
|
|
58
|
+
const orchestrateSchema = taskSchema;
|
|
68
59
|
|
|
69
60
|
const statusSchema = Type.Object({}, STRICT_OBJECT);
|
|
70
61
|
|
|
@@ -83,12 +74,6 @@ const workerAbortSchema = Type.Union([
|
|
|
83
74
|
},
|
|
84
75
|
STRICT_OBJECT,
|
|
85
76
|
),
|
|
86
|
-
Type.Object(
|
|
87
|
-
{
|
|
88
|
-
wave_id: Type.String({ minLength: 1 }),
|
|
89
|
-
},
|
|
90
|
-
STRICT_OBJECT,
|
|
91
|
-
),
|
|
92
77
|
Type.Object(
|
|
93
78
|
{
|
|
94
79
|
all: Type.Literal(true),
|
|
@@ -104,10 +89,18 @@ const workerCloseSchema = Type.Object(
|
|
|
104
89
|
STRICT_OBJECT,
|
|
105
90
|
);
|
|
106
91
|
|
|
92
|
+
export interface DispatchDecision {
|
|
93
|
+
readonly mode: "async" | "inline";
|
|
94
|
+
readonly synthesisGroup?: {
|
|
95
|
+
readonly id: string;
|
|
96
|
+
readonly size: number;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
107
100
|
export interface OrchestrationToolDependencies {
|
|
108
101
|
readonly runtime: OrchestratorRuntime;
|
|
109
102
|
getCatalog(ctx: ExtensionContext): WorkerCatalog | Promise<WorkerCatalog>;
|
|
110
|
-
|
|
103
|
+
getDispatchDecision(toolCallId: string): DispatchDecision;
|
|
111
104
|
}
|
|
112
105
|
|
|
113
106
|
export function registerOrchestrationTools(
|
|
@@ -118,66 +111,62 @@ export function registerOrchestrationTools(
|
|
|
118
111
|
name: "orchestrate",
|
|
119
112
|
label: "Orchestrate",
|
|
120
113
|
description:
|
|
121
|
-
"Dispatch
|
|
122
|
-
promptSnippet: "Dispatch one
|
|
114
|
+
"Dispatch one fully briefed task. One or more sibling orchestrate calls run concurrently and asynchronously. Mixing orchestrate with another tool makes it inline and blocking.",
|
|
115
|
+
promptSnippet: "Dispatch one fully briefed worker task",
|
|
123
116
|
promptGuidelines: [
|
|
124
|
-
"Use orchestrate for
|
|
117
|
+
"Use sibling orchestrate calls for independent tasks, with one complete brief per call.",
|
|
125
118
|
],
|
|
119
|
+
executionMode: "parallel",
|
|
126
120
|
parameters: orchestrateSchema,
|
|
127
121
|
renderCall(args, theme, { expanded }) {
|
|
128
|
-
return renderDispatchCall(theme, args
|
|
122
|
+
return renderDispatchCall(theme, args, expanded);
|
|
129
123
|
},
|
|
130
124
|
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
131
125
|
return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
|
|
132
126
|
},
|
|
133
127
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const onSettlement = mode === "inline" ? (settlement: unknown) => {
|
|
138
|
-
settlements.push(settlement);
|
|
139
|
-
onUpdate?.({
|
|
140
|
-
content: [{ type: "text", text: `${settlements.length} worker response(s) received.` }],
|
|
141
|
-
details: { mode: "inline", settlements: [...settlements] },
|
|
142
|
-
});
|
|
143
|
-
} : undefined;
|
|
144
|
-
const wave = await orchestrateWithMode(
|
|
145
|
-
deps.runtime,
|
|
146
|
-
runtimeContext,
|
|
147
|
-
params.tasks,
|
|
148
|
-
mode,
|
|
149
|
-
signal,
|
|
150
|
-
onSettlement,
|
|
151
|
-
);
|
|
152
|
-
|
|
128
|
+
const decision = deps.getDispatchDecision(toolCallId);
|
|
129
|
+
const mode = decision.mode;
|
|
130
|
+
const runtimeContext = await buildRuntimeContext(ctx, deps, decision.synthesisGroup);
|
|
153
131
|
if (mode === "async") {
|
|
154
|
-
const
|
|
155
|
-
|
|
132
|
+
const acceptedRun = await deps.runtime.orchestrate(
|
|
133
|
+
runtimeContext,
|
|
134
|
+
params,
|
|
135
|
+
"async",
|
|
136
|
+
signal,
|
|
137
|
+
);
|
|
138
|
+
const readable = acceptedRunDetails(acceptedRun);
|
|
156
139
|
return {
|
|
157
140
|
content: [
|
|
158
141
|
{
|
|
159
142
|
type: "text",
|
|
160
|
-
text: readableDetails(`Accepted async
|
|
143
|
+
text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
|
|
161
144
|
},
|
|
162
145
|
],
|
|
163
|
-
details:
|
|
146
|
+
details: readable,
|
|
164
147
|
terminate: true,
|
|
165
148
|
};
|
|
166
149
|
}
|
|
167
150
|
|
|
168
|
-
const
|
|
169
|
-
|
|
151
|
+
const completedRun = await deps.runtime.orchestrate(
|
|
152
|
+
runtimeContext,
|
|
153
|
+
params,
|
|
154
|
+
"inline",
|
|
155
|
+
signal,
|
|
156
|
+
createInlineSettlementListener(onUpdate),
|
|
157
|
+
);
|
|
158
|
+
const readable = completedRunDetails(completedRun);
|
|
170
159
|
return {
|
|
171
160
|
content: [
|
|
172
161
|
{
|
|
173
162
|
type: "text",
|
|
174
163
|
text: readableDetails(
|
|
175
|
-
`Completed inline
|
|
164
|
+
`Completed inline run ${readable.run_id}.`,
|
|
176
165
|
readable,
|
|
177
166
|
),
|
|
178
167
|
},
|
|
179
168
|
],
|
|
180
|
-
details:
|
|
169
|
+
details: readable,
|
|
181
170
|
};
|
|
182
171
|
},
|
|
183
172
|
});
|
|
@@ -238,54 +227,49 @@ export function registerOrchestrationTools(
|
|
|
238
227
|
},
|
|
239
228
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
240
229
|
const workerId = asWorkerId(params.worker_id);
|
|
241
|
-
const mode = deps.
|
|
230
|
+
const mode = deps.getDispatchDecision(toolCallId).mode;
|
|
242
231
|
const runtimeContext = await buildRuntimeContext(ctx, deps);
|
|
243
|
-
const settlements: unknown[] = [];
|
|
244
|
-
const onSettlement = mode === "inline" ? (settlement: unknown) => {
|
|
245
|
-
settlements.push(settlement);
|
|
246
|
-
onUpdate?.({
|
|
247
|
-
content: [{ type: "text", text: `${settlements.length} worker response(s) received.` }],
|
|
248
|
-
details: { mode: "inline", settlements: [...settlements] },
|
|
249
|
-
});
|
|
250
|
-
} : undefined;
|
|
251
|
-
const wave = await sendWithMode(
|
|
252
|
-
deps.runtime,
|
|
253
|
-
runtimeContext,
|
|
254
|
-
workerId,
|
|
255
|
-
params.instructions,
|
|
256
|
-
mode,
|
|
257
|
-
signal,
|
|
258
|
-
onSettlement,
|
|
259
|
-
);
|
|
260
|
-
|
|
261
232
|
if (mode === "async") {
|
|
262
|
-
const
|
|
263
|
-
|
|
233
|
+
const acceptedRun = await deps.runtime.send(
|
|
234
|
+
runtimeContext,
|
|
235
|
+
workerId,
|
|
236
|
+
params.instructions,
|
|
237
|
+
"async",
|
|
238
|
+
signal,
|
|
239
|
+
);
|
|
240
|
+
const readable = acceptedRunDetails(acceptedRun);
|
|
264
241
|
return {
|
|
265
242
|
content: [
|
|
266
243
|
{
|
|
267
244
|
type: "text",
|
|
268
|
-
text: readableDetails(`Accepted async
|
|
245
|
+
text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
|
|
269
246
|
},
|
|
270
247
|
],
|
|
271
|
-
details:
|
|
248
|
+
details: readable,
|
|
272
249
|
terminate: true,
|
|
273
250
|
};
|
|
274
251
|
}
|
|
275
252
|
|
|
276
|
-
const
|
|
277
|
-
|
|
253
|
+
const completedRun = await deps.runtime.send(
|
|
254
|
+
runtimeContext,
|
|
255
|
+
workerId,
|
|
256
|
+
params.instructions,
|
|
257
|
+
"inline",
|
|
258
|
+
signal,
|
|
259
|
+
createInlineSettlementListener(onUpdate),
|
|
260
|
+
);
|
|
261
|
+
const readable = completedRunDetails(completedRun);
|
|
278
262
|
return {
|
|
279
263
|
content: [
|
|
280
264
|
{
|
|
281
265
|
type: "text",
|
|
282
266
|
text: readableDetails(
|
|
283
|
-
`Completed inline
|
|
267
|
+
`Completed inline run ${readable.run_id}.`,
|
|
284
268
|
readable,
|
|
285
269
|
),
|
|
286
270
|
},
|
|
287
271
|
],
|
|
288
|
-
details:
|
|
272
|
+
details: readable,
|
|
289
273
|
};
|
|
290
274
|
},
|
|
291
275
|
});
|
|
@@ -294,18 +278,16 @@ export function registerOrchestrationTools(
|
|
|
294
278
|
name: "worker_abort",
|
|
295
279
|
label: "Worker Abort",
|
|
296
280
|
description:
|
|
297
|
-
"Abort owned active work by worker IDs
|
|
298
|
-
promptSnippet: "Abort active owned workers by worker IDs
|
|
281
|
+
"Abort owned active work by worker IDs or all active owned workers. Use worker_close for ready reusable workers.",
|
|
282
|
+
promptSnippet: "Abort active owned workers by worker IDs or all",
|
|
299
283
|
promptGuidelines: [
|
|
300
284
|
"Use worker_abort only for active work; use worker_close for a ready reusable worker.",
|
|
301
285
|
],
|
|
302
286
|
parameters: workerAbortSchema,
|
|
303
287
|
renderCall(args, theme) {
|
|
304
|
-
const target = "
|
|
305
|
-
? args.
|
|
306
|
-
: "
|
|
307
|
-
? `${args.worker_ids.length} worker${args.worker_ids.length === 1 ? "" : "s"}`
|
|
308
|
-
: "all workers";
|
|
288
|
+
const target = "worker_ids" in args
|
|
289
|
+
? `${args.worker_ids.length} worker${args.worker_ids.length === 1 ? "" : "s"}`
|
|
290
|
+
: "all workers";
|
|
309
291
|
return renderCompactCall(theme, "worker_abort", target);
|
|
310
292
|
},
|
|
311
293
|
renderResult(result, { isPartial }, theme) {
|
|
@@ -326,7 +308,7 @@ export function registerOrchestrationTools(
|
|
|
326
308
|
text: readableDetails("Abort request completed.", readable),
|
|
327
309
|
},
|
|
328
310
|
],
|
|
329
|
-
details: { target: target.
|
|
311
|
+
details: { target: target.external },
|
|
330
312
|
};
|
|
331
313
|
},
|
|
332
314
|
});
|
|
@@ -361,7 +343,7 @@ export function registerOrchestrationTools(
|
|
|
361
343
|
text: readableDetails(`Closed worker ${workerId}.`, readable),
|
|
362
344
|
},
|
|
363
345
|
],
|
|
364
|
-
details:
|
|
346
|
+
details: readable,
|
|
365
347
|
};
|
|
366
348
|
},
|
|
367
349
|
});
|
|
@@ -370,6 +352,7 @@ export function registerOrchestrationTools(
|
|
|
370
352
|
async function buildRuntimeContext(
|
|
371
353
|
ctx: ExtensionContext,
|
|
372
354
|
deps: OrchestrationToolDependencies,
|
|
355
|
+
synthesisGroup?: DispatchDecision["synthesisGroup"],
|
|
373
356
|
): Promise<OrchestrationContext> {
|
|
374
357
|
return {
|
|
375
358
|
ownerSessionId: requireNonblank(
|
|
@@ -383,32 +366,22 @@ async function buildRuntimeContext(
|
|
|
383
366
|
catalog: await deps.getCatalog(ctx),
|
|
384
367
|
parentModel: ctx.model,
|
|
385
368
|
modelRegistry: ctx.modelRegistry,
|
|
369
|
+
...(synthesisGroup ? { synthesisGroup } : {}),
|
|
386
370
|
};
|
|
387
371
|
}
|
|
388
372
|
|
|
389
|
-
function
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function sendWithMode(
|
|
402
|
-
runtime: OrchestratorRuntime,
|
|
403
|
-
context: OrchestrationContext,
|
|
404
|
-
workerId: WorkerId,
|
|
405
|
-
instructions: string,
|
|
406
|
-
mode: "async" | "inline",
|
|
407
|
-
signal: AbortSignal | undefined,
|
|
408
|
-
onSettlement?: (settlement: unknown) => void,
|
|
409
|
-
): Promise<AcceptedWave | CompletedWave> {
|
|
410
|
-
if (mode === "async") return runtime.send(context, workerId, instructions, "async");
|
|
411
|
-
return runtime.send(context, workerId, instructions, "inline", signal, onSettlement);
|
|
373
|
+
function createInlineSettlementListener(
|
|
374
|
+
onUpdate: ((result: AgentToolResult<unknown>) => void) | undefined,
|
|
375
|
+
): SettlementListener {
|
|
376
|
+
return (settlement) => {
|
|
377
|
+
onUpdate?.({
|
|
378
|
+
content: [{ type: "text", text: "Worker response received." }],
|
|
379
|
+
details: {
|
|
380
|
+
mode: "inline",
|
|
381
|
+
result: inlineResultDetails(settlement),
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
};
|
|
412
385
|
}
|
|
413
386
|
|
|
414
387
|
function requireNonblank(name: string, value: string): string {
|
|
@@ -422,24 +395,15 @@ function asWorkerId(value: string): WorkerId {
|
|
|
422
395
|
return requireNonblank("worker_id", value) as WorkerId;
|
|
423
396
|
}
|
|
424
397
|
|
|
425
|
-
function asWaveId(value: string): WaveId {
|
|
426
|
-
return requireNonblank("wave_id", value) as WaveId;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
398
|
function abortTarget(params: {
|
|
430
399
|
worker_ids?: string[];
|
|
431
|
-
wave_id?: string;
|
|
432
400
|
all?: true;
|
|
433
401
|
}): {
|
|
434
402
|
runtime: AbortTarget;
|
|
435
|
-
external:
|
|
436
|
-
| { worker_ids: readonly WorkerId[] }
|
|
437
|
-
| { wave_id: WaveId }
|
|
438
|
-
| { all: true };
|
|
403
|
+
external: { worker_ids: readonly WorkerId[] } | { all: true };
|
|
439
404
|
} {
|
|
440
405
|
const selectedTargetCount = [
|
|
441
406
|
params.worker_ids !== undefined,
|
|
442
|
-
params.wave_id !== undefined,
|
|
443
407
|
params.all !== undefined,
|
|
444
408
|
].filter(Boolean).length;
|
|
445
409
|
if (selectedTargetCount !== 1 || (params.all !== undefined && params.all !== true)) {
|
|
@@ -456,33 +420,26 @@ function abortTarget(params: {
|
|
|
456
420
|
external: { worker_ids: workerIds },
|
|
457
421
|
};
|
|
458
422
|
}
|
|
459
|
-
if (params.wave_id !== undefined) {
|
|
460
|
-
const waveId = asWaveId(params.wave_id);
|
|
461
|
-
return {
|
|
462
|
-
runtime: { waveId },
|
|
463
|
-
external: { wave_id: waveId },
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
423
|
return {
|
|
467
424
|
runtime: { all: true },
|
|
468
425
|
external: { all: true },
|
|
469
426
|
};
|
|
470
427
|
}
|
|
471
428
|
|
|
472
|
-
function
|
|
429
|
+
function acceptedRunDetails(run: AcceptedRun) {
|
|
473
430
|
return {
|
|
474
431
|
mode: "async" as const,
|
|
475
|
-
|
|
476
|
-
|
|
432
|
+
run_id: run.id,
|
|
433
|
+
worker_id: run.workerId,
|
|
477
434
|
};
|
|
478
435
|
}
|
|
479
436
|
|
|
480
|
-
function
|
|
437
|
+
function completedRunDetails(run: CompletedRun) {
|
|
481
438
|
return {
|
|
482
|
-
mode:
|
|
483
|
-
|
|
484
|
-
owner_session_id:
|
|
485
|
-
|
|
439
|
+
mode: run.mode,
|
|
440
|
+
run_id: run.id,
|
|
441
|
+
owner_session_id: run.ownerSessionId,
|
|
442
|
+
result: completedResultDetails(run.result),
|
|
486
443
|
};
|
|
487
444
|
}
|
|
488
445
|
|
|
@@ -494,18 +451,34 @@ function completedResultDetails(result: CompletedResult) {
|
|
|
494
451
|
status: result.status,
|
|
495
452
|
outcome: outcomeDetails(result.outcome),
|
|
496
453
|
usage: usageDetails(result.usage),
|
|
454
|
+
started_at: result.startedAt,
|
|
455
|
+
settled_at: result.settledAt,
|
|
497
456
|
session_file: result.sessionFile,
|
|
498
457
|
};
|
|
499
458
|
}
|
|
500
459
|
|
|
460
|
+
function inlineResultDetails(settlement: WorkerSettlement) {
|
|
461
|
+
return {
|
|
462
|
+
worker_id: settlement.workerId,
|
|
463
|
+
worker: settlement.worker,
|
|
464
|
+
title: settlement.title,
|
|
465
|
+
status: settlement.status,
|
|
466
|
+
outcome: outcomeDetails(settlement.outcome),
|
|
467
|
+
usage: usageDetails(settlement.usage),
|
|
468
|
+
started_at: settlement.startedAt,
|
|
469
|
+
settled_at: settlement.settledAt,
|
|
470
|
+
session_file: settlement.sessionFile,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
501
474
|
function statusDetails(catalog: WorkerCatalog, snapshot: RuntimeSnapshot) {
|
|
502
475
|
return {
|
|
503
476
|
catalog: {
|
|
504
477
|
workers: catalog.workers.map(catalogWorkerDetails),
|
|
505
478
|
diagnostics: catalog.diagnostics.map(diagnosticDetails),
|
|
506
479
|
},
|
|
507
|
-
|
|
508
|
-
|
|
480
|
+
state: {
|
|
481
|
+
runs: snapshot.runs.map(runDetails),
|
|
509
482
|
workers: snapshot.workers.map(workerDetails),
|
|
510
483
|
},
|
|
511
484
|
};
|
|
@@ -545,14 +518,14 @@ function diagnosticDetails(diagnostic: CatalogDiagnostic) {
|
|
|
545
518
|
};
|
|
546
519
|
}
|
|
547
520
|
|
|
548
|
-
function
|
|
521
|
+
function runDetails(run: RunRecord) {
|
|
549
522
|
return {
|
|
550
|
-
|
|
551
|
-
owner_session_id:
|
|
552
|
-
|
|
553
|
-
mode:
|
|
554
|
-
state:
|
|
555
|
-
created_at:
|
|
523
|
+
run_id: run.id,
|
|
524
|
+
owner_session_id: run.ownerSessionId,
|
|
525
|
+
worker_id: run.workerId,
|
|
526
|
+
mode: run.mode,
|
|
527
|
+
state: run.state,
|
|
528
|
+
created_at: run.createdAt,
|
|
556
529
|
};
|
|
557
530
|
}
|
|
558
531
|
|
|
@@ -561,7 +534,7 @@ function workerDetails(worker: WorkerRecord) {
|
|
|
561
534
|
worker_id: worker.id,
|
|
562
535
|
worker: worker.worker,
|
|
563
536
|
owner_session_id: worker.ownerSessionId,
|
|
564
|
-
|
|
537
|
+
run_id: worker.runId,
|
|
565
538
|
title: worker.title,
|
|
566
539
|
lifecycle: worker.lifecycle,
|
|
567
540
|
status: worker.status,
|
|
@@ -620,54 +593,48 @@ interface RenderableTask {
|
|
|
620
593
|
|
|
621
594
|
function renderDispatchCall(
|
|
622
595
|
theme: Theme,
|
|
623
|
-
|
|
596
|
+
task: RenderableTask,
|
|
624
597
|
expanded: boolean,
|
|
625
598
|
): Component {
|
|
626
599
|
const container = new Container();
|
|
627
|
-
const renderableTasks = Array.isArray(tasks) ? tasks : [];
|
|
628
|
-
const count = renderableTasks.length;
|
|
629
600
|
container.addChild(new Text(
|
|
630
|
-
theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted",
|
|
601
|
+
theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", safeTerminalText(task.worker)),
|
|
602
|
+
0, 0,
|
|
603
|
+
));
|
|
604
|
+
container.addChild(new Text(
|
|
605
|
+
`${theme.fg("accent", "→")} ${theme.fg("text", theme.bold(safeTerminalText(task.title)))}`,
|
|
631
606
|
0, 0,
|
|
632
607
|
));
|
|
633
608
|
if (expanded) {
|
|
634
|
-
|
|
635
|
-
container.addChild(new Spacer(1));
|
|
636
|
-
container.addChild(new Text(`${theme.fg("accent", "→")} ${theme.fg("muted", safeTerminalText(task.worker))} · ${theme.fg("text", theme.bold(safeTerminalText(task.title)))}`, 0, 0));
|
|
637
|
-
container.addChild(new Text(safeTerminalText(task.instructions), 2, 0));
|
|
638
|
-
}
|
|
609
|
+
container.addChild(new Text(safeTerminalText(task.instructions), 2, 0));
|
|
639
610
|
return new WidthBoundComponent(container);
|
|
640
611
|
}
|
|
641
|
-
container.addChild(new InstructionPreview(
|
|
612
|
+
container.addChild(new InstructionPreview(task.instructions, theme));
|
|
642
613
|
container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
|
|
643
614
|
return new WidthBoundComponent(container);
|
|
644
615
|
}
|
|
645
616
|
|
|
646
617
|
class InstructionPreview implements Component {
|
|
647
618
|
constructor(
|
|
648
|
-
private readonly
|
|
619
|
+
private readonly instructions: unknown,
|
|
649
620
|
private readonly theme: Theme,
|
|
650
621
|
) {}
|
|
651
622
|
render(width: number): string[] {
|
|
652
623
|
const bounded = Math.max(1, width);
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const previewLines = wrapped.slice(0, MAX_INSTRUCTION_PREVIEW_LINES);
|
|
664
|
-
if (preview.truncated || wrapped.length > MAX_INSTRUCTION_PREVIEW_LINES) {
|
|
665
|
-
const lastIndex = previewLines.length - 1;
|
|
666
|
-
previewLines[lastIndex] = truncateToWidth(`${previewLines[lastIndex] ?? ""}…`, contentWidth, "…");
|
|
667
|
-
}
|
|
668
|
-
for (const line of previewLines) lines.push(this.theme.fg("dim", ` ${line}`));
|
|
624
|
+
const contentWidth = Math.max(1, bounded - 2);
|
|
625
|
+
const characterLimit = Math.max(256, Math.min(4096, contentWidth * 3));
|
|
626
|
+
const preview = compactInstructionPreview(this.instructions, characterLimit);
|
|
627
|
+
if (!preview.text) return [];
|
|
628
|
+
|
|
629
|
+
const wrapped = wrapTextWithAnsi(preview.text, contentWidth);
|
|
630
|
+
const previewLines = wrapped.slice(0, MAX_INSTRUCTION_PREVIEW_LINES);
|
|
631
|
+
if (preview.truncated || wrapped.length > MAX_INSTRUCTION_PREVIEW_LINES) {
|
|
632
|
+
const lastIndex = previewLines.length - 1;
|
|
633
|
+
previewLines[lastIndex] = truncateToWidth(`${previewLines[lastIndex] ?? ""}…`, contentWidth, "…");
|
|
669
634
|
}
|
|
670
|
-
return
|
|
635
|
+
return previewLines.map((line) =>
|
|
636
|
+
truncateToWidth(this.theme.fg("dim", ` ${line}`), bounded, "…")
|
|
637
|
+
);
|
|
671
638
|
}
|
|
672
639
|
invalidate(): void {}
|
|
673
640
|
}
|
|
@@ -744,19 +711,18 @@ function renderOrchestrationResult(
|
|
|
744
711
|
lastComponent: unknown,
|
|
745
712
|
): Component {
|
|
746
713
|
const details = result.details;
|
|
747
|
-
if (isRecord(details) && typeof details.
|
|
748
|
-
|
|
749
|
-
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));
|
|
714
|
+
if (isRecord(details) && typeof details.run_id === "string" && typeof details.worker_id === "string" && details.mode === "async") {
|
|
715
|
+
return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
|
|
750
716
|
}
|
|
751
|
-
const
|
|
752
|
-
if (
|
|
717
|
+
const inlineResult = readInlineResult(details);
|
|
718
|
+
if (inlineResult) {
|
|
753
719
|
const component = lastComponent instanceof InlineResultComponent
|
|
754
720
|
? lastComponent
|
|
755
721
|
: new InlineResultComponent(theme);
|
|
756
|
-
component.update(
|
|
722
|
+
component.update(inlineResult, isPartial, expanded);
|
|
757
723
|
return component;
|
|
758
724
|
}
|
|
759
|
-
if (isRecord(details) && (
|
|
725
|
+
if (isRecord(details) && ("result" in details || "worker_id" in details)) {
|
|
760
726
|
return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
|
|
761
727
|
}
|
|
762
728
|
if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
|
|
@@ -768,16 +734,17 @@ interface InlineSettlement {
|
|
|
768
734
|
title: string;
|
|
769
735
|
status: "completed" | "ready" | "failed" | "aborted";
|
|
770
736
|
response: string;
|
|
737
|
+
elapsed?: string;
|
|
771
738
|
}
|
|
772
739
|
|
|
773
740
|
class InlineResultComponent implements Component {
|
|
774
|
-
private
|
|
741
|
+
private result: InlineSettlement | undefined;
|
|
775
742
|
private partial = false;
|
|
776
743
|
private expanded = false;
|
|
777
744
|
private child: Component = new Container();
|
|
778
745
|
constructor(private readonly theme: Theme) {}
|
|
779
|
-
update(
|
|
780
|
-
this.
|
|
746
|
+
update(result: InlineSettlement, partial: boolean, expanded: boolean): void {
|
|
747
|
+
this.result = result;
|
|
781
748
|
this.partial = partial;
|
|
782
749
|
this.expanded = expanded;
|
|
783
750
|
this.rebuild();
|
|
@@ -788,33 +755,35 @@ class InlineResultComponent implements Component {
|
|
|
788
755
|
private rebuild(): void {
|
|
789
756
|
(this.child as Component & { dispose?: () => void }).dispose?.();
|
|
790
757
|
const container = new Container();
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
758
|
+
const result = this.result;
|
|
759
|
+
if (!result) {
|
|
760
|
+
this.child = container;
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const appearance = inlineResultAppearance(result.status);
|
|
764
|
+
const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
|
|
765
|
+
const title = this.theme.bold(result.title);
|
|
766
|
+
const workerType = this.theme.fg("muted", this.theme.italic(result.worker));
|
|
767
|
+
const header = [
|
|
768
|
+
this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
|
|
769
|
+
workerType,
|
|
770
|
+
...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
|
|
771
|
+
].join(" · ");
|
|
772
|
+
container.addChild(new WidthBoundComponent(new Text(header, 0, 0), 1));
|
|
773
|
+
if (result.response) {
|
|
774
|
+
const markdown = new Markdown(result.response, this.expanded ? 2 : 0, 0, getMarkdownTheme());
|
|
775
|
+
container.addChild(new WidthBoundComponent(markdown, this.expanded ? undefined : 2));
|
|
802
776
|
}
|
|
803
|
-
|
|
804
|
-
|
|
777
|
+
container.addChild(new Spacer(1));
|
|
778
|
+
if (this.partial) container.addChild(new Text(this.theme.fg("warning", "Receiving worker response…"), 0, 0));
|
|
779
|
+
else if (!this.expanded) container.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to inspect full response")), 0, 0));
|
|
805
780
|
this.child = container;
|
|
806
781
|
}
|
|
807
782
|
}
|
|
808
783
|
|
|
809
|
-
function
|
|
810
|
-
if (!isRecord(details)) return
|
|
811
|
-
|
|
812
|
-
const parsed: InlineSettlement[] = [];
|
|
813
|
-
for (const value of values) {
|
|
814
|
-
const settlement = readInlineSettlement(value);
|
|
815
|
-
if (settlement) parsed.push(settlement);
|
|
816
|
-
}
|
|
817
|
-
return parsed;
|
|
784
|
+
function readInlineResult(details: unknown): InlineSettlement | undefined {
|
|
785
|
+
if (!isRecord(details)) return undefined;
|
|
786
|
+
return readInlineSettlement(details.result);
|
|
818
787
|
}
|
|
819
788
|
|
|
820
789
|
function readInlineSettlement(value: unknown): InlineSettlement | undefined {
|
|
@@ -825,27 +794,50 @@ function readInlineSettlement(value: unknown): InlineSettlement | undefined {
|
|
|
825
794
|
const outcomeStatus = statuses.find((item) => item === outcome.status);
|
|
826
795
|
if (!status || outcomeStatus !== status) return undefined;
|
|
827
796
|
const message = outcome.message;
|
|
828
|
-
const
|
|
829
|
-
const snakeAssistant = outcome.assistant_text;
|
|
797
|
+
const assistantText = outcome.assistant_text;
|
|
830
798
|
if (message !== undefined && typeof message !== "string") return undefined;
|
|
831
|
-
if (
|
|
832
|
-
if (snakeAssistant !== undefined && typeof snakeAssistant !== "string") return undefined;
|
|
833
|
-
const assistantText = typeof camelAssistant === "string" ? camelAssistant : snakeAssistant;
|
|
799
|
+
if (assistantText !== undefined && typeof assistantText !== "string") return undefined;
|
|
834
800
|
if ((status === "completed" || status === "ready") && typeof assistantText !== "string") return undefined;
|
|
835
801
|
if (status === "failed" && typeof message !== "string") return undefined;
|
|
802
|
+
const startedAt = value.started_at;
|
|
803
|
+
const settledAt = value.settled_at;
|
|
804
|
+
const elapsed = typeof startedAt === "number" && typeof settledAt === "number" && settledAt >= startedAt
|
|
805
|
+
? formatElapsed(settledAt - startedAt)
|
|
806
|
+
: undefined;
|
|
836
807
|
return {
|
|
837
808
|
worker: value.worker,
|
|
838
809
|
title: value.title,
|
|
839
810
|
status,
|
|
840
811
|
response: [message, assistantText].filter((item): item is string => typeof item === "string" && item.length > 0).join("\n\n"),
|
|
812
|
+
...(elapsed ? { elapsed } : {}),
|
|
841
813
|
};
|
|
842
814
|
}
|
|
843
815
|
|
|
816
|
+
function inlineResultAppearance(status: InlineSettlement["status"]): {
|
|
817
|
+
readonly color: "success" | "error" | "warning";
|
|
818
|
+
readonly icon: "✓" | "✗" | "■";
|
|
819
|
+
readonly qualifier?: string;
|
|
820
|
+
} {
|
|
821
|
+
if (status === "failed") return { color: "error", icon: "✗", qualifier: "failed" };
|
|
822
|
+
if (status === "aborted") return { color: "warning", icon: "■", qualifier: "aborted" };
|
|
823
|
+
if (status === "ready") {
|
|
824
|
+
return { color: "success", icon: "✓", qualifier: "ready for follow-up" };
|
|
825
|
+
}
|
|
826
|
+
return { color: "success", icon: "✓" };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function formatElapsed(milliseconds: number): string {
|
|
830
|
+
const seconds = Math.floor(milliseconds / 1000);
|
|
831
|
+
if (seconds < 60) return `${seconds}s`;
|
|
832
|
+
const minutes = Math.floor(seconds / 60);
|
|
833
|
+
return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
|
|
834
|
+
}
|
|
835
|
+
|
|
844
836
|
function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
|
|
845
837
|
if (isPartial) return new Text(theme.fg("muted", "Reading orchestration diagnostics…"), 0, 0);
|
|
846
838
|
const details = result.details;
|
|
847
|
-
if (isRecord(details) && isRecord(details.
|
|
848
|
-
const workers = details.
|
|
839
|
+
if (isRecord(details) && isRecord(details.state) && Array.isArray(details.state.workers)) {
|
|
840
|
+
const workers = details.state.workers.filter(isRecord);
|
|
849
841
|
const active = workers.filter((worker) => ["starting", "running", "stopping"].includes(String(worker.status))).length;
|
|
850
842
|
const ready = workers.filter((worker) => worker.status === "ready").length;
|
|
851
843
|
const diagnostics = isRecord(details.catalog) && Array.isArray(details.catalog.diagnostics) ? details.catalog.diagnostics.length : 0;
|