@zachwill/pi-orchestrate 0.7.1 → 0.8.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 +2 -2
- package/extension/catalog.ts +4 -2
- package/extension/contract.ts +2 -2
- package/extension/delivery.ts +5 -5
- package/extension/index.ts +3 -3
- package/extension/presentation.ts +13 -13
- package/extension/runtime.ts +5 -5
- package/extension/tools.ts +4 -4
- package/extension/worker-session.ts +3 -3
- package/extension/worker-settlement.ts +22 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -134,9 +134,9 @@ lifecycle: interactive
|
|
|
134
134
|
Inspect only the assigned scope. Do not modify files. Return concise findings with file paths.
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
Required fields are `name`, `description`, a nonempty `tools` list
|
|
137
|
+
Required fields are `name`, `description`, and a nonempty `tools` list. The Markdown body is the worker's nonempty system prompt.
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
`lifecycle` is optional, accepts `one-shot` or `interactive`, and defaults to `one-shot`. Other optional fields are `model`, `thinking`, `skills`, and `compaction`. An omitted `model` inherits the parent's model. For `skills`, omission uses normal discovery, a list is an exact allowlist, and `[]` disables skills.
|
|
140
140
|
|
|
141
141
|
Definitions are strict, regular non-symlink `.md` files up to 64 KiB. Supported Pi tools are `read`, `bash`, `edit`, `write`, `grep`, `find`, and `ls`. Grant the smallest useful set: a read-only prompt does not prevent writes when the worker has tools with write authority.
|
|
142
142
|
|
package/extension/catalog.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
7
7
|
import { basename, extname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { Result, Schema, SchemaGetter, type SchemaIssue } from "effect";
|
|
9
|
+
import { Effect, Result, Schema, SchemaGetter, type SchemaIssue } from "effect";
|
|
10
10
|
import type {
|
|
11
11
|
CatalogDiagnostic,
|
|
12
12
|
WorkerCatalog,
|
|
@@ -76,7 +76,9 @@ const WorkerFrontmatter = Schema.Struct({
|
|
|
76
76
|
tools: commaList(Schema.Literals(SUPPORTED_TOOL_NAMES)),
|
|
77
77
|
skills: Schema.optionalKey(commaList(Schema.NonEmptyString, true)),
|
|
78
78
|
compaction: Schema.optionalKey(Compaction),
|
|
79
|
-
lifecycle: Schema.Literals(["one-shot", "interactive"])
|
|
79
|
+
lifecycle: Schema.Literals(["one-shot", "interactive"]).pipe(
|
|
80
|
+
Schema.withDecodingDefaultKey(Effect.succeed("one-shot")),
|
|
81
|
+
),
|
|
80
82
|
});
|
|
81
83
|
const decodeWorkerFrontmatter = Schema.decodeUnknownResult(WorkerFrontmatter, {
|
|
82
84
|
errors: "all",
|
package/extension/contract.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { WorkerCatalog } from "./domain.js";
|
|
|
3
3
|
const CONTRACT_START = "<!-- pi-orchestrate:contract:start -->";
|
|
4
4
|
const CONTRACT_END = "<!-- pi-orchestrate:contract:end -->";
|
|
5
5
|
|
|
6
|
-
function
|
|
6
|
+
function sortedWorkers(catalog: WorkerCatalog) {
|
|
7
7
|
return [...catalog.workers].sort((left, right) => {
|
|
8
8
|
if (left.name < right.name) return -1;
|
|
9
9
|
if (left.name > right.name) return 1;
|
|
@@ -12,7 +12,7 @@ function contractWorkers(catalog: WorkerCatalog) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function formatCatalog(catalog: WorkerCatalog): string {
|
|
15
|
-
const workers =
|
|
15
|
+
const workers = sortedWorkers(catalog);
|
|
16
16
|
if (workers.length === 0) return "- No trusted workers are available for this session.";
|
|
17
17
|
|
|
18
18
|
return workers
|
package/extension/delivery.ts
CHANGED
|
@@ -45,7 +45,7 @@ export class DeliveryCoordinator {
|
|
|
45
45
|
private readonly pendingSettlements: WorkerSettlement[] = [];
|
|
46
46
|
private readonly flushingOwners = new Set<string>();
|
|
47
47
|
private readonly synthesisGroups = new Map<string, SynthesisGroupState>();
|
|
48
|
-
private readonly
|
|
48
|
+
private readonly finalSynthesisGroupEventIds = new Set<string>();
|
|
49
49
|
private highestAcceptedSequence = 0;
|
|
50
50
|
|
|
51
51
|
bind(binding: ParentBinding): void {
|
|
@@ -115,7 +115,7 @@ export class DeliveryCoordinator {
|
|
|
115
115
|
this.pendingSettlements.length = 0;
|
|
116
116
|
this.flushingOwners.clear();
|
|
117
117
|
this.synthesisGroups.clear();
|
|
118
|
-
this.
|
|
118
|
+
this.finalSynthesisGroupEventIds.clear();
|
|
119
119
|
this.highestAcceptedSequence = 0;
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -240,12 +240,12 @@ export class DeliveryCoordinator {
|
|
|
240
240
|
}
|
|
241
241
|
if (state.acceptedEventIds.length !== state.expected) return;
|
|
242
242
|
const finalEventId = state.acceptedEventIds.at(-1);
|
|
243
|
-
if (finalEventId) this.
|
|
243
|
+
if (finalEventId) this.finalSynthesisGroupEventIds.add(finalEventId);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
private isFinalBoundary(settlement: WorkerSettlement): boolean {
|
|
247
247
|
if (!settlement.synthesisGroupId) return true;
|
|
248
|
-
return this.
|
|
248
|
+
return this.finalSynthesisGroupEventIds.has(settlement.eventId);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
private finishSynthesisGroup(settlement: WorkerSettlement): void {
|
|
@@ -253,7 +253,7 @@ export class DeliveryCoordinator {
|
|
|
253
253
|
this.synthesisGroups.delete(
|
|
254
254
|
synthesisGroupKey(settlement.ownerSessionId, settlement.synthesisGroupId),
|
|
255
255
|
);
|
|
256
|
-
this.
|
|
256
|
+
this.finalSynthesisGroupEventIds.delete(settlement.eventId);
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
|
package/extension/index.ts
CHANGED
|
@@ -125,16 +125,16 @@ export function createOrchestrationExtension(
|
|
|
125
125
|
);
|
|
126
126
|
const ownerSessionId = activeBinding?.ownerSessionId;
|
|
127
127
|
if (!ownerSessionId) return;
|
|
128
|
-
const
|
|
128
|
+
const isOrchestrateGroup =
|
|
129
129
|
toolCalls.length > 1 &&
|
|
130
130
|
toolCalls.every((toolCall) => toolCall.name === "orchestrate");
|
|
131
|
-
const synthesisGroup =
|
|
131
|
+
const synthesisGroup = isOrchestrateGroup
|
|
132
132
|
? { id: `orchestrate:${toolCalls[0]?.id ?? "group"}`, size: toolCalls.length }
|
|
133
133
|
: undefined;
|
|
134
134
|
|
|
135
135
|
for (const toolCall of toolCalls) {
|
|
136
136
|
if (!DISPATCH_TOOL_NAMES.has(toolCall.name)) continue;
|
|
137
|
-
const mode =
|
|
137
|
+
const mode = isOrchestrateGroup || toolCalls.length === 1
|
|
138
138
|
? "async"
|
|
139
139
|
: "inline";
|
|
140
140
|
dispatchDecisions.set(toolCall.id, {
|
|
@@ -47,7 +47,7 @@ const ACTIVE_STATUSES: ReadonlySet<WorkerStatus> = new Set(["starting", "running
|
|
|
47
47
|
|
|
48
48
|
export type PresentationRuntime = Pick<OrchestratorRuntime, "snapshot" | "subscribeState">;
|
|
49
49
|
|
|
50
|
-
type
|
|
50
|
+
type DecodedSettlement = WorkerSettlementDetails;
|
|
51
51
|
|
|
52
52
|
interface StatusBinding {
|
|
53
53
|
readonly ownerSessionId: string;
|
|
@@ -216,12 +216,12 @@ export class WorkerStatusComponent implements Component {
|
|
|
216
216
|
const turns = formatTurnMarker(worker);
|
|
217
217
|
const context = `${formatContextTokens(numberOrZero(worker.usage?.contextTokens))} ctx`;
|
|
218
218
|
const usageFields = width >= 28 ? [turns, context] : width >= 12 ? [turns] : [];
|
|
219
|
-
const
|
|
220
|
-
const
|
|
219
|
+
const workerName = this.theme.fg("muted", this.theme.italic(worker.worker));
|
|
220
|
+
const workerNameFits = visibleWidth(
|
|
221
221
|
`⠋ · ${worker.worker} · ${usageFields.join(" · ")}`,
|
|
222
222
|
) + 10 <= width;
|
|
223
|
-
const suffixFields = width >= 72 &&
|
|
224
|
-
? [
|
|
223
|
+
const suffixFields = width >= 72 && workerNameFits
|
|
224
|
+
? [workerName, ...usageFields]
|
|
225
225
|
: usageFields;
|
|
226
226
|
const prefix = `${glyph} `;
|
|
227
227
|
const suffix = suffixFields.length ? ` · ${suffixFields.join(" · ")}` : "";
|
|
@@ -280,11 +280,11 @@ export class WorkerResultComponent implements Component {
|
|
|
280
280
|
const elapsed = elapsedBetween(details.startedAt, details.settledAt);
|
|
281
281
|
const qualifier = resultQualifier(details);
|
|
282
282
|
const title = this.theme.bold(details.title);
|
|
283
|
-
const
|
|
283
|
+
const workerName = this.theme.fg("muted", this.theme.italic(details.worker));
|
|
284
284
|
const suffix = [qualifier, elapsed].filter(Boolean).join(" · ");
|
|
285
285
|
const header = [
|
|
286
286
|
this.theme.fg(color, `${statusIcon(details)} ${title}`),
|
|
287
|
-
|
|
287
|
+
workerName,
|
|
288
288
|
...(suffix ? [this.theme.fg(color, suffix)] : []),
|
|
289
289
|
].join(" · ");
|
|
290
290
|
const outcome = presentedOutcome(details);
|
|
@@ -308,7 +308,7 @@ export class WorkerResultComponent implements Component {
|
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
function readSettlement(value: unknown):
|
|
311
|
+
function readSettlement(value: unknown): DecodedSettlement | undefined {
|
|
312
312
|
const decoded = decodePersistedWorkerSettlementDetails(value);
|
|
313
313
|
return Result.isSuccess(decoded) ? decoded.success : undefined;
|
|
314
314
|
}
|
|
@@ -319,13 +319,13 @@ function workerAnimation(status: WorkerStatus) {
|
|
|
319
319
|
return WORKER_ANIMATIONS.running;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
function resultColor(status:
|
|
322
|
+
function resultColor(status: DecodedSettlement["status"]): "success" | "error" | "warning" {
|
|
323
323
|
if (status === "failed") return "error";
|
|
324
324
|
if (status === "aborted") return "warning";
|
|
325
325
|
return "success";
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
function resultQualifier(result:
|
|
328
|
+
function resultQualifier(result: DecodedSettlement): string | undefined {
|
|
329
329
|
if (result.status === "aborted") return "aborted";
|
|
330
330
|
if (result.status === "failed" && result.failureStage === "startup") {
|
|
331
331
|
return "could not start";
|
|
@@ -335,7 +335,7 @@ function resultQualifier(result: SafeSettlement): string | undefined {
|
|
|
335
335
|
return undefined;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
function statusIcon(result:
|
|
338
|
+
function statusIcon(result: DecodedSettlement): string {
|
|
339
339
|
if (result.status === "failed") return "✗";
|
|
340
340
|
if (result.status === "aborted") return "■";
|
|
341
341
|
return "✓";
|
|
@@ -348,7 +348,7 @@ function outcomeText(outcome: WorkerOutcome): string {
|
|
|
348
348
|
return "Worker session closed.";
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
function presentedOutcome(result:
|
|
351
|
+
function presentedOutcome(result: DecodedSettlement): string {
|
|
352
352
|
const body = outcomeText(result.outcome);
|
|
353
353
|
if (result.status !== "completed" && result.status !== "ready") return body;
|
|
354
354
|
|
|
@@ -363,7 +363,7 @@ function presentedOutcome(result: SafeSettlement): string {
|
|
|
363
363
|
return lines.join("\n").trimEnd();
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
-
function settlementMetadata(result:
|
|
366
|
+
function settlementMetadata(result: DecodedSettlement): string[] {
|
|
367
367
|
return [
|
|
368
368
|
`worker ID ${result.workerId} · run ID ${result.runId}`,
|
|
369
369
|
`status ${result.status} · generation ${result.generation}`,
|
package/extension/runtime.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface AcceptedRun {
|
|
|
66
66
|
readonly workerId: WorkerId;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export interface
|
|
69
|
+
export interface RunResult {
|
|
70
70
|
readonly workerId: WorkerId;
|
|
71
71
|
readonly worker: string;
|
|
72
72
|
readonly title: string;
|
|
@@ -82,7 +82,7 @@ export interface CompletedRun {
|
|
|
82
82
|
readonly id: RunId;
|
|
83
83
|
readonly ownerSessionId: string;
|
|
84
84
|
readonly mode: RunMode;
|
|
85
|
-
readonly result:
|
|
85
|
+
readonly result: RunResult;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export interface RuntimeSnapshot {
|
|
@@ -1238,7 +1238,7 @@ function isActiveWorkerStatus(status: WorkerRecord["status"]): boolean {
|
|
|
1238
1238
|
|
|
1239
1239
|
function isSettledWorkerStatus(
|
|
1240
1240
|
status: WorkerRecord["status"],
|
|
1241
|
-
): status is
|
|
1241
|
+
): status is RunResult["status"] {
|
|
1242
1242
|
return status === "completed" || status === "ready" || status === "failed" || status === "aborted";
|
|
1243
1243
|
}
|
|
1244
1244
|
|
|
@@ -1336,11 +1336,11 @@ function freezeCompletedRun(
|
|
|
1336
1336
|
if (record.settledAt === undefined) {
|
|
1337
1337
|
throw new Error("Completed run requires a settlement timestamp");
|
|
1338
1338
|
}
|
|
1339
|
-
const result:
|
|
1339
|
+
const result: RunResult = Object.freeze({
|
|
1340
1340
|
workerId: record.id,
|
|
1341
1341
|
worker: record.worker,
|
|
1342
1342
|
title: record.title,
|
|
1343
|
-
status: record.status as
|
|
1343
|
+
status: record.status as RunResult["status"],
|
|
1344
1344
|
outcome: Object.freeze(copyOutcome(outcome)),
|
|
1345
1345
|
usage: Object.freeze(copyUsage(record.usage)),
|
|
1346
1346
|
startedAt: record.startedAt,
|
package/extension/tools.ts
CHANGED
|
@@ -34,10 +34,10 @@ import {
|
|
|
34
34
|
import type {
|
|
35
35
|
AbortTarget,
|
|
36
36
|
AcceptedRun,
|
|
37
|
-
CompletedResult,
|
|
38
37
|
CompletedRun,
|
|
39
38
|
OrchestrationContext,
|
|
40
39
|
OrchestratorRuntime,
|
|
40
|
+
RunResult,
|
|
41
41
|
RuntimeSnapshot,
|
|
42
42
|
SettlementListener,
|
|
43
43
|
WorkerSettlement,
|
|
@@ -446,7 +446,7 @@ function completedRunDetails(run: CompletedRun) {
|
|
|
446
446
|
};
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
function completedResultDetails(result:
|
|
449
|
+
function completedResultDetails(result: RunResult) {
|
|
450
450
|
return {
|
|
451
451
|
worker_id: result.workerId,
|
|
452
452
|
worker: result.worker,
|
|
@@ -766,10 +766,10 @@ class InlineResultComponent implements Component {
|
|
|
766
766
|
const appearance = inlineResultAppearance(result.status);
|
|
767
767
|
const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
|
|
768
768
|
const title = this.theme.bold(result.title);
|
|
769
|
-
const
|
|
769
|
+
const workerName = this.theme.fg("muted", this.theme.italic(result.worker));
|
|
770
770
|
const header = [
|
|
771
771
|
this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
|
|
772
|
-
|
|
772
|
+
workerName,
|
|
773
773
|
...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
|
|
774
774
|
].join(" · ");
|
|
775
775
|
container.addChild(new WidthBoundComponent(new Text(header, 0, 0), 1));
|
|
@@ -106,17 +106,17 @@ const WorkerAgentSessionAcquisitionOperation = Schema.Literals([
|
|
|
106
106
|
]);
|
|
107
107
|
type WorkerAgentSessionAcquisitionOperation = typeof WorkerAgentSessionAcquisitionOperation.Type;
|
|
108
108
|
|
|
109
|
-
export class WorkerModelAcquisitionError extends Schema.
|
|
109
|
+
export class WorkerModelAcquisitionError extends Schema.TaggedError<WorkerModelAcquisitionError>()(
|
|
110
110
|
"WorkerSession.ModelAcquisitionError",
|
|
111
111
|
{ operation: WorkerModelAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
|
|
112
112
|
) {}
|
|
113
113
|
|
|
114
|
-
export class WorkerResourceAcquisitionError extends Schema.
|
|
114
|
+
export class WorkerResourceAcquisitionError extends Schema.TaggedError<WorkerResourceAcquisitionError>()(
|
|
115
115
|
"WorkerSession.ResourceAcquisitionError",
|
|
116
116
|
{ operation: WorkerResourceAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
|
|
117
117
|
) {}
|
|
118
118
|
|
|
119
|
-
export class WorkerAgentSessionAcquisitionError extends Schema.
|
|
119
|
+
export class WorkerAgentSessionAcquisitionError extends Schema.TaggedError<WorkerAgentSessionAcquisitionError>()(
|
|
120
120
|
"WorkerSession.AgentSessionAcquisitionError",
|
|
121
121
|
{ operation: WorkerAgentSessionAcquisitionOperation, message: Schema.String, cause: Schema.Defect() },
|
|
122
122
|
) {}
|
|
@@ -3,6 +3,7 @@ import type { RunId, WorkerId } from "./domain.js";
|
|
|
3
3
|
|
|
4
4
|
const NonnegativeFinite = Schema.Finite.check(Schema.isGreaterThanOrEqualTo(0));
|
|
5
5
|
const NonnegativeInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
6
|
+
const PositiveInteger = Schema.Int.check(Schema.isGreaterThan(0));
|
|
6
7
|
|
|
7
8
|
const WorkerUsage = Schema.Struct({
|
|
8
9
|
input: NonnegativeFinite,
|
|
@@ -45,11 +46,11 @@ const FailureStage = Schema.Literals([
|
|
|
45
46
|
/** Canonical schema for settlement details written by the current runtime. */
|
|
46
47
|
export const WorkerSettlementDetails = Schema.Struct({
|
|
47
48
|
eventId: Schema.String,
|
|
48
|
-
sequence:
|
|
49
|
+
sequence: PositiveInteger,
|
|
49
50
|
ownerSessionId: Schema.String,
|
|
50
51
|
runId: Schema.String,
|
|
51
52
|
workerId: Schema.String,
|
|
52
|
-
generation:
|
|
53
|
+
generation: PositiveInteger,
|
|
53
54
|
mode: Schema.Literals(["async", "inline"]),
|
|
54
55
|
worker: Schema.String,
|
|
55
56
|
title: Schema.String,
|
|
@@ -60,7 +61,7 @@ export const WorkerSettlementDetails = Schema.Struct({
|
|
|
60
61
|
startedAt: NonnegativeInteger,
|
|
61
62
|
settledAt: NonnegativeInteger,
|
|
62
63
|
synthesisGroupId: Schema.optionalKey(Schema.String),
|
|
63
|
-
synthesisGroupSize: Schema.optionalKey(
|
|
64
|
+
synthesisGroupSize: Schema.optionalKey(PositiveInteger),
|
|
64
65
|
sessionFile: Schema.optionalKey(Schema.String),
|
|
65
66
|
failureStage: Schema.optionalKey(FailureStage),
|
|
66
67
|
}).check(
|
|
@@ -68,6 +69,24 @@ export const WorkerSettlementDetails = Schema.Struct({
|
|
|
68
69
|
if (settlement.outcome.status !== settlement.status) {
|
|
69
70
|
return "outcome status must match settlement status";
|
|
70
71
|
}
|
|
72
|
+
if (settlement.status === "ready" && settlement.lifecycle !== "interactive") {
|
|
73
|
+
return "ready settlement requires an interactive lifecycle";
|
|
74
|
+
}
|
|
75
|
+
if (settlement.status === "completed" && settlement.lifecycle !== "one-shot") {
|
|
76
|
+
return "completed settlement requires a one-shot lifecycle";
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
(settlement.synthesisGroupId === undefined) !==
|
|
80
|
+
(settlement.synthesisGroupSize === undefined)
|
|
81
|
+
) {
|
|
82
|
+
return "synthesis group ID and size must occur together";
|
|
83
|
+
}
|
|
84
|
+
if (
|
|
85
|
+
settlement.synthesisGroupSize !== undefined &&
|
|
86
|
+
settlement.synthesisGroupSize < 2
|
|
87
|
+
) {
|
|
88
|
+
return "synthesis group size must be at least 2";
|
|
89
|
+
}
|
|
71
90
|
if (settlement.settledAt < settlement.startedAt) {
|
|
72
91
|
return "settlement timestamp must not precede start timestamp";
|
|
73
92
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zachwill/pi-orchestrate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Concurrent worker orchestration for Pi",
|
|
6
6
|
"files": ["extension/", "examples/", "README.md", "LICENSE"],
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"test": "bun test"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"effect": "4.0.0-
|
|
28
|
+
"effect": "4.0.0-rc.111"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@earendil-works/pi-agent-core": "^0.80.10",
|