@zachwill/pi-orchestrate 0.9.2 → 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 +17 -36
- package/extension/orchestration/admission.ts +297 -0
- package/extension/{domain.ts → orchestration/model.ts} +27 -89
- package/extension/{runtime.ts → orchestration/service.ts} +178 -467
- package/extension/{worker-settlement.ts → orchestration/settlement.ts} +67 -21
- package/extension/package-root.ts +3 -0
- package/extension/{contract.ts → parent/contract.ts} +2 -2
- 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} +27 -24
- package/extension/{presentation.ts → pi/presentation.ts} +29 -23
- 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 -837
- /package/extension/{tui.ts → pi/tui.ts} +0 -0
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
WorkerReadyOutcome,
|
|
9
9
|
WorkerResponseOutcome,
|
|
10
10
|
WorkerUsage,
|
|
11
|
-
|
|
11
|
+
type RunRecord,
|
|
12
|
+
type SettledWorkerRecord,
|
|
13
|
+
} from "./model.js";
|
|
12
14
|
|
|
13
15
|
const NonnegativeInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
14
16
|
const PositiveInteger = Schema.Int.check(Schema.isGreaterThan(0));
|
|
@@ -20,8 +22,8 @@ const FailureStage = Schema.Literals([
|
|
|
20
22
|
"cancellation",
|
|
21
23
|
]);
|
|
22
24
|
|
|
23
|
-
/** Canonical schema for
|
|
24
|
-
export const
|
|
25
|
+
/** Canonical schema for settlements written and persisted by orchestration. */
|
|
26
|
+
export const WorkerSettlement = Schema.Struct({
|
|
25
27
|
eventId: Schema.String,
|
|
26
28
|
sequence: PositiveInteger,
|
|
27
29
|
ownerSessionId: Schema.String,
|
|
@@ -77,25 +79,69 @@ export const WorkerSettlementDetails = Schema.Struct({
|
|
|
77
79
|
}),
|
|
78
80
|
);
|
|
79
81
|
|
|
80
|
-
export interface
|
|
81
|
-
extends Schema.Schema.Type<typeof
|
|
82
|
+
export interface WorkerSettlement
|
|
83
|
+
extends Schema.Schema.Type<typeof WorkerSettlement> {}
|
|
82
84
|
|
|
83
85
|
export type SettlementFailureStage = NonNullable<
|
|
84
|
-
|
|
85
|
-
>;
|
|
86
|
-
|
|
87
|
-
export type WorkerSettlement = Schema.Schema.Type<
|
|
88
|
-
typeof WorkerSettlementDetails
|
|
86
|
+
WorkerSettlement["failureStage"]
|
|
89
87
|
>;
|
|
90
88
|
|
|
91
89
|
const decodeCurrentWorkerSettlement = Schema.decodeUnknownResult(
|
|
92
|
-
|
|
90
|
+
WorkerSettlement,
|
|
93
91
|
);
|
|
94
92
|
|
|
95
|
-
export function
|
|
93
|
+
export function decodePersistedWorkerSettlement(value: unknown) {
|
|
96
94
|
return decodeCurrentWorkerSettlement(value);
|
|
97
95
|
}
|
|
98
96
|
|
|
97
|
+
/** Every input a settlement needs; orchestration state entries stay private. */
|
|
98
|
+
export interface WorkerSettlementInput {
|
|
99
|
+
readonly sequence: number;
|
|
100
|
+
readonly generation: number;
|
|
101
|
+
readonly run: RunRecord;
|
|
102
|
+
readonly worker: SettledWorkerRecord;
|
|
103
|
+
readonly settledAt: number;
|
|
104
|
+
readonly failureStage?: SettlementFailureStage;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Builds the canonical settlement published to listeners and persisted by Pi. */
|
|
108
|
+
export function createWorkerSettlement({
|
|
109
|
+
sequence,
|
|
110
|
+
generation,
|
|
111
|
+
run,
|
|
112
|
+
worker,
|
|
113
|
+
settledAt,
|
|
114
|
+
failureStage,
|
|
115
|
+
}: WorkerSettlementInput): WorkerSettlement {
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
eventId: `${sequence}:${run.id}:${worker.id}:${generation}`,
|
|
118
|
+
sequence,
|
|
119
|
+
ownerSessionId: worker.ownerSessionId,
|
|
120
|
+
runId: run.id,
|
|
121
|
+
workerId: worker.id,
|
|
122
|
+
generation,
|
|
123
|
+
mode: run.mode,
|
|
124
|
+
worker: worker.worker,
|
|
125
|
+
title: worker.title,
|
|
126
|
+
lifecycle: worker.lifecycle,
|
|
127
|
+
status: worker.status,
|
|
128
|
+
outcome: Object.freeze({ ...worker.outcome }),
|
|
129
|
+
...(failureStage ? { failureStage } : {}),
|
|
130
|
+
usage: Object.freeze({ ...worker.usage }),
|
|
131
|
+
startedAt: worker.startedAt,
|
|
132
|
+
settledAt,
|
|
133
|
+
...(run.synthesisGroupId && run.synthesisGroupSize
|
|
134
|
+
? {
|
|
135
|
+
synthesisGroupId: run.synthesisGroupId,
|
|
136
|
+
synthesisGroupSize: run.synthesisGroupSize,
|
|
137
|
+
}
|
|
138
|
+
: {}),
|
|
139
|
+
...(worker.sessionFile !== undefined
|
|
140
|
+
? { sessionFile: worker.sessionFile }
|
|
141
|
+
: {}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
99
145
|
const InlineWorkerOutcome = Schema.Union([
|
|
100
146
|
WorkerCompletedOutcome.pipe(
|
|
101
147
|
Schema.encodeKeys({ assistantText: "assistant_text" }),
|
|
@@ -121,15 +167,15 @@ const InlineWorkerUsage = WorkerUsage.pipe(
|
|
|
121
167
|
|
|
122
168
|
/** Tool transport projection derived from the canonical settlement field schemas. */
|
|
123
169
|
export const InlineWorkerSettlementDetails = Schema.Struct({
|
|
124
|
-
workerId:
|
|
125
|
-
worker:
|
|
126
|
-
title:
|
|
127
|
-
status:
|
|
170
|
+
workerId: WorkerSettlement.fields.workerId,
|
|
171
|
+
worker: WorkerSettlement.fields.worker,
|
|
172
|
+
title: WorkerSettlement.fields.title,
|
|
173
|
+
status: WorkerSettlement.fields.status,
|
|
128
174
|
outcome: InlineWorkerOutcome,
|
|
129
175
|
usage: InlineWorkerUsage,
|
|
130
|
-
startedAt:
|
|
131
|
-
settledAt:
|
|
132
|
-
sessionFile:
|
|
176
|
+
startedAt: WorkerSettlement.fields.startedAt,
|
|
177
|
+
settledAt: WorkerSettlement.fields.settledAt,
|
|
178
|
+
sessionFile: WorkerSettlement.fields.sessionFile,
|
|
133
179
|
}).pipe(
|
|
134
180
|
Schema.encodeKeys({
|
|
135
181
|
workerId: "worker_id",
|
|
@@ -153,9 +199,9 @@ export interface InlineWorkerSettlementDetails
|
|
|
153
199
|
|
|
154
200
|
export const InlineWorkerToolDetails = Schema.Struct({
|
|
155
201
|
mode: Schema.Literal("inline"),
|
|
156
|
-
runId: Schema.optionalKey(
|
|
202
|
+
runId: Schema.optionalKey(WorkerSettlement.fields.runId),
|
|
157
203
|
ownerSessionId: Schema.optionalKey(
|
|
158
|
-
|
|
204
|
+
WorkerSettlement.fields.ownerSessionId,
|
|
159
205
|
),
|
|
160
206
|
result: InlineWorkerSettlementDetails,
|
|
161
207
|
}).pipe(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WorkerCatalog } from "
|
|
1
|
+
import type { WorkerCatalog } from "../catalog/definition.js";
|
|
2
2
|
|
|
3
3
|
const CONTRACT_START = "<!-- pi-orchestrate:contract:start -->";
|
|
4
4
|
const CONTRACT_END = "<!-- pi-orchestrate:contract:end -->";
|
|
@@ -124,7 +124,7 @@ ${formatCatalog(catalog)}
|
|
|
124
124
|
${CONTRACT_END}`;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
export function
|
|
127
|
+
export function applyOrchestratorContract(
|
|
128
128
|
systemPrompt: string,
|
|
129
129
|
catalog: WorkerCatalog,
|
|
130
130
|
): string {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
import { Orchestration } from "
|
|
3
|
-
import type { WorkerSettlement } from "
|
|
2
|
+
import { Orchestration } from "../orchestration/service.js";
|
|
3
|
+
import type { WorkerSettlement } from "../orchestration/settlement.js";
|
|
4
4
|
|
|
5
5
|
export const MAX_DELIVERY_MARKDOWN_BYTES = 50 * 1024;
|
|
6
6
|
export const MAX_WORKER_DELIVERY_MARKDOWN_BYTES = 16 * 1024;
|
|
@@ -11,14 +11,11 @@ export const DELIVERY_PARENT_INSTRUCTIONS =
|
|
|
11
11
|
|
|
12
12
|
export type ParentBindingGeneration = string | number | symbol;
|
|
13
13
|
|
|
14
|
-
/** Complete, immutable worker output for presentation and history consumers. */
|
|
15
|
-
export type WorkerDeliveryDetails = WorkerSettlement;
|
|
16
|
-
|
|
17
14
|
export interface WorkerDeliveryMessage {
|
|
18
15
|
readonly customType: "pi-orchestrate-worker-result";
|
|
19
16
|
readonly content: string;
|
|
20
17
|
readonly display: true;
|
|
21
|
-
readonly details:
|
|
18
|
+
readonly details: WorkerSettlement;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
export interface WorkerDeliveryOptions {
|
|
@@ -67,8 +64,8 @@ export class DeliveryCoordinator implements DeliveryService {
|
|
|
67
64
|
private readonly pendingSettlements: WorkerSettlement[] = [];
|
|
68
65
|
private readonly flushingOwners = new Set<string>();
|
|
69
66
|
private readonly synthesisGroups = new Map<string, SynthesisGroupState>();
|
|
70
|
-
//
|
|
71
|
-
// so one watermark is valid.
|
|
67
|
+
// Orchestration settlement sequences are process-scoped and monotonic across
|
|
68
|
+
// owners, so one watermark is valid.
|
|
72
69
|
private highestAcceptedSequence = 0;
|
|
73
70
|
|
|
74
71
|
bind(binding: ParentBinding): void {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { SynthesisGroup } from "../orchestration/model.js";
|
|
2
|
+
|
|
3
|
+
export interface ParentToolCall {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface DispatchDecision {
|
|
9
|
+
readonly mode: "async" | "inline";
|
|
10
|
+
readonly synthesisGroup?: SynthesisGroup;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ClassifiedParentDispatch {
|
|
14
|
+
readonly toolCallId: string;
|
|
15
|
+
readonly decision: DispatchDecision;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
19
|
+
"orchestrate",
|
|
20
|
+
"interactive_send",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// Sole dispatches and homogeneous orchestrate waves detach so the parent turn can
|
|
24
|
+
// end while work continues. Mixed tools stay inline because their shared parent
|
|
25
|
+
// turn still has sibling work; one wave boundary defers one synthesis turn until
|
|
26
|
+
// every admitted member has settled.
|
|
27
|
+
export function classifyParentDispatches(
|
|
28
|
+
toolCalls: readonly ParentToolCall[],
|
|
29
|
+
): readonly ClassifiedParentDispatch[] {
|
|
30
|
+
const isOrchestrateGroup =
|
|
31
|
+
toolCalls.length > 1 &&
|
|
32
|
+
toolCalls.every((toolCall) => toolCall.name === "orchestrate");
|
|
33
|
+
const synthesisGroup = isOrchestrateGroup
|
|
34
|
+
? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
|
|
35
|
+
: undefined;
|
|
36
|
+
|
|
37
|
+
return toolCalls.flatMap((toolCall): ClassifiedParentDispatch[] => {
|
|
38
|
+
if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) return [];
|
|
39
|
+
return [{
|
|
40
|
+
toolCallId: toolCall.id,
|
|
41
|
+
decision: {
|
|
42
|
+
mode: isOrchestrateGroup || toolCalls.length === 1 ? "async" : "inline",
|
|
43
|
+
...(toolCall.name === "orchestrate" && synthesisGroup
|
|
44
|
+
? { synthesisGroup }
|
|
45
|
+
: {}),
|
|
46
|
+
},
|
|
47
|
+
}];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -7,33 +7,36 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
Orchestration,
|
|
9
9
|
orchestrationLayer,
|
|
10
|
-
type AbortTarget,
|
|
11
10
|
type AcceptedRun,
|
|
12
11
|
type CompletedRun,
|
|
13
|
-
type OrchestrationContext,
|
|
14
12
|
type OrchestrationService,
|
|
15
13
|
SHUTDOWN_CLEANUP_GRACE_MS,
|
|
16
|
-
type
|
|
14
|
+
type OwnerSnapshot,
|
|
17
15
|
type SettlementListener,
|
|
18
16
|
type UnsubscribeSettlement,
|
|
19
|
-
} from "
|
|
20
|
-
import {
|
|
21
|
-
|
|
17
|
+
} from "../orchestration/service.js";
|
|
18
|
+
import type {
|
|
19
|
+
AbortTarget,
|
|
20
|
+
OrchestrationContext,
|
|
21
|
+
} from "../orchestration/admission.js";
|
|
22
|
+
import { createChildSessionsLayer } from "../worker/child-sessions.js";
|
|
23
|
+
import type { OrchestrateTaskInput, RunMode } from "../orchestration/model.js";
|
|
22
24
|
|
|
23
25
|
const PROCESS_HOST_KEY = Symbol.for("@zachwill/pi-orchestrate/process-host/v3");
|
|
24
26
|
|
|
25
|
-
type
|
|
27
|
+
type DispatchResult<M extends RunMode> = M extends "async"
|
|
26
28
|
? AcceptedRun
|
|
27
29
|
: CompletedRun;
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
/** Promise-facing orchestration API used by Pi tools and presentation. */
|
|
32
|
+
export interface OrchestrationClient {
|
|
30
33
|
orchestrate<M extends RunMode>(
|
|
31
34
|
context: OrchestrationContext,
|
|
32
35
|
task: OrchestrateTaskInput,
|
|
33
36
|
mode: M,
|
|
34
37
|
signal?: AbortSignal,
|
|
35
38
|
onSettlement?: SettlementListener,
|
|
36
|
-
): Promise<
|
|
39
|
+
): Promise<DispatchResult<M>>;
|
|
37
40
|
sendInteractive<M extends RunMode>(
|
|
38
41
|
context: OrchestrationContext,
|
|
39
42
|
workerId: string,
|
|
@@ -41,20 +44,20 @@ export interface OrchestratorRuntime {
|
|
|
41
44
|
mode: M,
|
|
42
45
|
signal?: AbortSignal,
|
|
43
46
|
onSettlement?: SettlementListener,
|
|
44
|
-
): Promise<
|
|
47
|
+
): Promise<DispatchResult<M>>;
|
|
45
48
|
abort(ownerSessionId: string, target: AbortTarget): Promise<void>;
|
|
46
49
|
closeInteractive(ownerSessionId: string, workerId: string): Promise<void>;
|
|
47
|
-
snapshot(ownerSessionId: string): Promise<
|
|
50
|
+
snapshot(ownerSessionId: string): Promise<OwnerSnapshot>;
|
|
48
51
|
subscribeSettlement(listener: SettlementListener): UnsubscribeSettlement;
|
|
49
52
|
subscribeState(
|
|
50
53
|
ownerSessionId: string,
|
|
51
|
-
listener: (snapshot:
|
|
54
|
+
listener: (snapshot: OwnerSnapshot) => void,
|
|
52
55
|
): () => void;
|
|
53
56
|
shutdown(): Promise<void>;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export interface ProcessHost {
|
|
57
|
-
readonly
|
|
60
|
+
readonly orchestration: OrchestrationClient;
|
|
58
61
|
readonly delivery: DeliveryService;
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -100,7 +103,7 @@ export function getProcessHost(): ProcessHost | undefined {
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/** Pi-facing adapter. Every Promise operation executes one complete Orchestration Effect. */
|
|
103
|
-
export class
|
|
106
|
+
export class ManagedOrchestrationClient<R = never> implements OrchestrationClient {
|
|
104
107
|
constructor(
|
|
105
108
|
private readonly effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
|
|
106
109
|
private readonly orchestration: OrchestrationService,
|
|
@@ -192,7 +195,7 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
192
195
|
return this.run(this.orchestration.closeInteractive(ownerSessionId, workerId));
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
snapshot(ownerSessionId: string): Promise<
|
|
198
|
+
snapshot(ownerSessionId: string): Promise<OwnerSnapshot> {
|
|
196
199
|
// Snapshot is dependency-free and remains readable from a retained host reference
|
|
197
200
|
// after the process root has been disposed.
|
|
198
201
|
return Effect.runPromise(this.orchestration.snapshot(ownerSessionId));
|
|
@@ -204,7 +207,7 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
204
207
|
|
|
205
208
|
subscribeState(
|
|
206
209
|
ownerSessionId: string,
|
|
207
|
-
listener: (snapshot:
|
|
210
|
+
listener: (snapshot: OwnerSnapshot) => void,
|
|
208
211
|
): () => void {
|
|
209
212
|
return this.orchestration.subscribeState(ownerSessionId, listener);
|
|
210
213
|
}
|
|
@@ -232,15 +235,15 @@ export class ProcessHostRuntimeAdapter<R = never> implements OrchestratorRuntime
|
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
|
|
235
|
-
export function
|
|
238
|
+
export function createOrchestrationClient<R>(
|
|
236
239
|
effectRuntime: ManagedRuntime.ManagedRuntime<Orchestration | R, never>,
|
|
237
|
-
):
|
|
240
|
+
): OrchestrationClient {
|
|
238
241
|
// Orchestration acquisition is synchronous; subscriptions must remain reentrant.
|
|
239
242
|
const orchestration = effectRuntime.runSync(Orchestration);
|
|
240
|
-
return new
|
|
243
|
+
return new ManagedOrchestrationClient(effectRuntime, orchestration);
|
|
241
244
|
}
|
|
242
245
|
|
|
243
|
-
export function
|
|
246
|
+
export function makeProcessHostLayer(): Layer.Layer<Orchestration | Delivery> {
|
|
244
247
|
const orchestration = orchestrationLayer().pipe(
|
|
245
248
|
Layer.provide(createChildSessionsLayer()),
|
|
246
249
|
);
|
|
@@ -258,11 +261,11 @@ export function createProcessHost(): ProcessHost {
|
|
|
258
261
|
}
|
|
259
262
|
if (existing) return existing;
|
|
260
263
|
|
|
261
|
-
const effectRuntime = ManagedRuntime.make(
|
|
262
|
-
const
|
|
264
|
+
const effectRuntime = ManagedRuntime.make(makeProcessHostLayer());
|
|
265
|
+
const orchestration = createOrchestrationClient(effectRuntime);
|
|
263
266
|
const delivery = effectRuntime.runSync(Delivery);
|
|
264
267
|
const host: OwnedProcessHost = {
|
|
265
|
-
|
|
268
|
+
orchestration,
|
|
266
269
|
delivery,
|
|
267
270
|
effectRuntime,
|
|
268
271
|
attachments: new Set(),
|
|
@@ -315,7 +318,7 @@ export function destroyProcessHost(
|
|
|
315
318
|
let shutdown: Promise<void>;
|
|
316
319
|
try {
|
|
317
320
|
// shutdown() closes Orchestration admission before returning its bounded teardown Promise.
|
|
318
|
-
shutdown = ownedHost.
|
|
321
|
+
shutdown = ownedHost.orchestration.shutdown();
|
|
319
322
|
} catch (error) {
|
|
320
323
|
shutdown = Promise.reject(error);
|
|
321
324
|
}
|
|
@@ -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
|
}
|
|
@@ -84,14 +88,14 @@ export class StatusController {
|
|
|
84
88
|
private unsubscribeState: (() => void) | undefined;
|
|
85
89
|
private widget: WorkerStatusComponent | undefined;
|
|
86
90
|
|
|
87
|
-
constructor(private readonly
|
|
91
|
+
constructor(private readonly workerState: WorkerStateSource) {}
|
|
88
92
|
|
|
89
93
|
bind(ownerSessionId: string, ctx: ExtensionContext): void {
|
|
90
94
|
if (this.disposed) return;
|
|
91
95
|
this.clearBinding();
|
|
92
96
|
const binding = { ownerSessionId, ctx };
|
|
93
97
|
this.binding = binding;
|
|
94
|
-
this.unsubscribeState = this.
|
|
98
|
+
this.unsubscribeState = this.workerState.subscribeState(ownerSessionId, (snapshot) => {
|
|
95
99
|
if (this.binding !== binding) return;
|
|
96
100
|
this.present(binding.ctx, snapshot);
|
|
97
101
|
});
|
|
@@ -108,7 +112,7 @@ export class StatusController {
|
|
|
108
112
|
this.clearBinding();
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
private present(ctx: ExtensionContext, snapshot:
|
|
115
|
+
private present(ctx: ExtensionContext, snapshot: OwnerSnapshot): void {
|
|
112
116
|
ctx.ui.setStatus(ORCHESTRATION_PRESENTATION_KEY, formatFooterStatus(snapshot));
|
|
113
117
|
if (ctx.mode !== "tui") return;
|
|
114
118
|
const active = activeWorkers(snapshot);
|
|
@@ -145,21 +149,23 @@ export class StatusController {
|
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
export function createStatusController(
|
|
149
|
-
|
|
152
|
+
export function createStatusController(
|
|
153
|
+
workerState: WorkerStateSource,
|
|
154
|
+
): StatusController {
|
|
155
|
+
return new StatusController(workerState);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
export class WorkerStatusComponent implements Component {
|
|
153
159
|
private frameIndex = 0;
|
|
154
|
-
private snapshot:
|
|
160
|
+
private snapshot: OwnerSnapshot;
|
|
155
161
|
private timer: ReturnType<typeof setInterval> | undefined;
|
|
156
162
|
|
|
157
|
-
constructor(snapshot:
|
|
163
|
+
constructor(snapshot: OwnerSnapshot, private readonly theme: Theme, private readonly tui?: RenderRequester) {
|
|
158
164
|
this.snapshot = snapshot;
|
|
159
165
|
this.startTimer();
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
update(snapshot:
|
|
168
|
+
update(snapshot: OwnerSnapshot): void {
|
|
163
169
|
this.snapshot = snapshot;
|
|
164
170
|
if (activeWorkers(snapshot).length > 0) this.startTimer();
|
|
165
171
|
else this.stopTimer();
|
|
@@ -289,8 +295,8 @@ export class WorkerResultComponent implements Component {
|
|
|
289
295
|
}
|
|
290
296
|
}
|
|
291
297
|
|
|
292
|
-
function readSettlement(value: unknown):
|
|
293
|
-
const decoded =
|
|
298
|
+
function readSettlement(value: unknown): WorkerSettlement | undefined {
|
|
299
|
+
const decoded = decodePersistedWorkerSettlement(value);
|
|
294
300
|
return Result.isSuccess(decoded) ? decoded.success : undefined;
|
|
295
301
|
}
|
|
296
302
|
|
|
@@ -307,7 +313,7 @@ function outcomeText(outcome: WorkerOutcome): string {
|
|
|
307
313
|
return "Worker session closed.";
|
|
308
314
|
}
|
|
309
315
|
|
|
310
|
-
function presentedOutcome(result:
|
|
316
|
+
function presentedOutcome(result: WorkerSettlement): string {
|
|
311
317
|
const body = outcomeText(result.outcome);
|
|
312
318
|
if (result.status !== "completed" && result.status !== "ready") return body;
|
|
313
319
|
|
|
@@ -322,7 +328,7 @@ function presentedOutcome(result: WorkerSettlementDetails): string {
|
|
|
322
328
|
return lines.join("\n").trimEnd();
|
|
323
329
|
}
|
|
324
330
|
|
|
325
|
-
function settlementMetadata(result:
|
|
331
|
+
function settlementMetadata(result: WorkerSettlement): string[] {
|
|
326
332
|
return [
|
|
327
333
|
`worker ID ${result.workerId} · run ID ${result.runId}`,
|
|
328
334
|
`status ${result.status} · generation ${result.generation}`,
|
|
@@ -338,7 +344,7 @@ function messageText(content: unknown): string {
|
|
|
338
344
|
return content.flatMap((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? [part.text] : []).join("\n");
|
|
339
345
|
}
|
|
340
346
|
|
|
341
|
-
function activeWorkers(snapshot:
|
|
347
|
+
function activeWorkers(snapshot: OwnerSnapshot): WorkerRecord[] {
|
|
342
348
|
return snapshot.workers.filter((worker) => ACTIVE_STATUSES.has(worker.status));
|
|
343
349
|
}
|
|
344
350
|
|