@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 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, and `lifecycle` (`one-shot` or `interactive`). The Markdown body is the worker's nonempty system prompt.
137
+ Required fields are `name`, `description`, and a nonempty `tools` list. The Markdown body is the worker's nonempty system prompt.
138
138
 
139
- 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.
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
 
@@ -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",
@@ -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 contractWorkers(catalog: WorkerCatalog) {
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 = contractWorkers(catalog);
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
@@ -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 finalSynthesisGroupEvents = new Set<string>();
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.finalSynthesisGroupEvents.clear();
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.finalSynthesisGroupEvents.add(finalEventId);
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.finalSynthesisGroupEvents.has(settlement.eventId);
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.finalSynthesisGroupEvents.delete(settlement.eventId);
256
+ this.finalSynthesisGroupEventIds.delete(settlement.eventId);
257
257
  }
258
258
  }
259
259
 
@@ -125,16 +125,16 @@ export function createOrchestrationExtension(
125
125
  );
126
126
  const ownerSessionId = activeBinding?.ownerSessionId;
127
127
  if (!ownerSessionId) return;
128
- const groupedOrchestration =
128
+ const isOrchestrateGroup =
129
129
  toolCalls.length > 1 &&
130
130
  toolCalls.every((toolCall) => toolCall.name === "orchestrate");
131
- const synthesisGroup = groupedOrchestration
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 = groupedOrchestration || toolCalls.length === 1
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 SafeSettlement = WorkerSettlementDetails;
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 workerType = this.theme.fg("muted", this.theme.italic(worker.worker));
220
- const workerTypeFits = visibleWidth(
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 && workerTypeFits
224
- ? [workerType, ...usageFields]
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 workerType = this.theme.fg("muted", this.theme.italic(details.worker));
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
- workerType,
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): SafeSettlement | undefined {
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: SafeSettlement["status"]): "success" | "error" | "warning" {
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: SafeSettlement): string | undefined {
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: SafeSettlement): string {
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: SafeSettlement): string {
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: SafeSettlement): string[] {
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}`,
@@ -66,7 +66,7 @@ export interface AcceptedRun {
66
66
  readonly workerId: WorkerId;
67
67
  }
68
68
 
69
- export interface CompletedResult {
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: CompletedResult;
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 CompletedResult["status"] {
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: CompletedResult = Object.freeze({
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 CompletedResult["status"],
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,
@@ -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: CompletedResult) {
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 workerType = this.theme.fg("muted", this.theme.italic(result.worker));
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
- workerType,
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.TaggedErrorClass<WorkerModelAcquisitionError>()(
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.TaggedErrorClass<WorkerResourceAcquisitionError>()(
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.TaggedErrorClass<WorkerAgentSessionAcquisitionError>()(
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: NonnegativeInteger,
49
+ sequence: PositiveInteger,
49
50
  ownerSessionId: Schema.String,
50
51
  runId: Schema.String,
51
52
  workerId: Schema.String,
52
- generation: NonnegativeInteger,
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(NonnegativeInteger),
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.7.1",
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-beta.99"
28
+ "effect": "4.0.0-rc.111"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@earendil-works/pi-agent-core": "^0.80.10",