@signalridge/pi-workflows 1.2.0 → 1.3.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +142 -17
  3. package/package.json +14 -8
  4. package/skills/workflow-authoring/SKILL.md +30 -0
  5. package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
  6. package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
  7. package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
  8. package/skills/workflow-authoring/examples/defensive-json-parsing.js +63 -0
  9. package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
  10. package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
  11. package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
  12. package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
  13. package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
  14. package/skills/workflow-authoring/examples/structured-output.js +37 -0
  15. package/skills/workflow-authoring/examples/tournament.js +81 -0
  16. package/skills/workflow-authoring/examples/validated-gate.js +63 -0
  17. package/skills/workflow-authoring/references/capabilities.md +44 -0
  18. package/skills/workflow-authoring/references/capability-details.md +294 -0
  19. package/skills/workflow-authoring/references/common-helpers.md +4 -0
  20. package/skills/workflow-authoring/references/debugging.md +29 -0
  21. package/skills/workflow-authoring/references/focused-recipes.md +12 -0
  22. package/skills/workflow-authoring/references/helpers.md +9 -0
  23. package/skills/workflow-authoring/references/lifecycle.md +37 -0
  24. package/skills/workflow-authoring/references/pattern-selection.md +15 -0
  25. package/skills/workflow-authoring/references/quality-helpers.md +8 -0
  26. package/skills/workflow-authoring/references/registry-ownership.md +17 -0
  27. package/skills/workflow-authoring/references/retry-helper.md +5 -0
  28. package/skills/workflow-authoring/references/review.md +43 -0
  29. package/skills/workflow-authoring/references/runtime.md +29 -0
  30. package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
  31. package/skills/workflow-authoring/references/versions.md +13 -0
  32. package/skills/workflow-patterns/SKILL.md +51 -0
  33. package/src/arming.ts +69 -0
  34. package/src/builtins.ts +331 -0
  35. package/src/capabilities.ts +318 -0
  36. package/src/code-review-scope.ts +144 -0
  37. package/src/engine.ts +1158 -2561
  38. package/src/errors.ts +136 -0
  39. package/src/index.ts +750 -103
  40. package/src/journal.ts +594 -1001
  41. package/src/rpc-client.ts +95 -24
  42. package/src/runtime.ts +1708 -0
  43. package/src/saved-workflows.ts +121 -0
  44. package/src/shared-store.ts +132 -0
  45. package/src/ui.ts +77 -40
  46. package/src/workflow-settings.ts +100 -0
  47. package/src/schema.ts +0 -215
  48. package/src/state-machine.ts +0 -79
  49. package/src/synthesis.ts +0 -140
package/src/rpc-client.ts CHANGED
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import type {
3
3
  ManagedSpawnResponse as ProtocolManagedSpawnResponse,
4
4
  ManagedTerminalSnapshot as ProtocolManagedTerminalSnapshot,
5
+ WorkflowTier,
5
6
  } from "@signalridge/pi-subagents-protocol";
6
7
  import {
7
8
  PROTOCOL_VERSION,
@@ -11,7 +12,26 @@ import {
11
12
  workflowTierCapabilityMatch,
12
13
  } from "@signalridge/pi-subagents-protocol";
13
14
  import type { WorkflowOwner } from "./journal.js";
14
- import type { WorkflowTask } from "./schema.js";
15
+
16
+ /** Minimal dispatch task for a script-run agent() call. */
17
+ export interface DispatchTask {
18
+ subagent_type: string;
19
+ prompt: string;
20
+ description: string;
21
+ tier?: WorkflowTier;
22
+ /** Exact provider/model reference; resolution remains inside pi-subagents. */
23
+ model?: string;
24
+ /** Optional thinking suffix/override resolved by pi-subagents. */
25
+ thinking?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "off";
26
+ /** Per-call worktree request owned by pi-subagents. */
27
+ isolation?: "worktree";
28
+ /** Forwarded toolset hint (e.g. "web-research") for pi-subagents to augment tools. */
29
+ toolset?: string;
30
+ /** Thread hint for sequential same-thread calls within one run. */
31
+ thread?: string;
32
+ /** Denied tools — prevents recursive workflow fan-out. */
33
+ excludeTools?: string[];
34
+ }
15
35
 
16
36
  export interface WorkflowEventBus {
17
37
  on(event: string, handler: (data: unknown) => void): () => void;
@@ -85,7 +105,7 @@ function callRpc<T>(
85
105
 
86
106
  export const REQUIRED_SUBAGENTS_PROTOCOL = PROTOCOL_VERSION;
87
107
  export const PROTOCOL_DIAGNOSTIC =
88
- "@signalridge/pi-workflows requires @signalridge/pi-subagents protocol v3 with managedSpawn, lifecycleOwner, ownedStop, ownedQuiescence, and workflowTiers capabilities. " +
108
+ "@signalridge/pi-workflows requires @signalridge/pi-subagents protocol v3 with managedSpawn, lifecycleOwner, ownedStop, ownedQuiescence, workflowTiers, and managedPolicy capabilities. " +
89
109
  "Install @signalridge/pi-subagents and @signalridge/pi-workflows exactly once; Pi loads both from their configured pi.extensions manifests.";
90
110
 
91
111
  export const CHILD_CONTEXT_QUERY_TIMEOUT_MS = 250;
@@ -217,10 +237,11 @@ export async function checkManagedSpawnProtocol(events: WorkflowEventBus, signal
217
237
  reply.capabilities.managedSpawn !== true ||
218
238
  reply.capabilities.lifecycleOwner !== true ||
219
239
  reply.capabilities.ownedStop !== true ||
220
- reply.capabilities.ownedQuiescence !== true
240
+ reply.capabilities.ownedQuiescence !== true ||
241
+ reply.capabilities.managedPolicy !== true
221
242
  ) {
222
243
  throw new Error(
223
- `subagents protocol v${REQUIRED_SUBAGENTS_PROTOCOL} with managed spawning, owned stop, and owned quiescence capability is required`,
244
+ `subagents protocol v${REQUIRED_SUBAGENTS_PROTOCOL} with managed spawning, owned stop, owned quiescence, and managed policy capabilities is required`,
224
245
  );
225
246
  }
226
247
  throw new Error("subagents protocol ping did not return a valid version/capability envelope");
@@ -233,6 +254,9 @@ export async function checkManagedSpawnProtocol(events: WorkflowEventBus, signal
233
254
  if (!workflowTierCapabilityMatch(ping.capabilities)) {
234
255
  throw new Error("subagents workflow tier capability is required before tiered managed spawning");
235
256
  }
257
+ if (ping.capabilities.managedPolicy !== true) {
258
+ throw new Error("subagents managed policy capability is required before policy-bearing managed spawning");
259
+ }
236
260
  }
237
261
 
238
262
  export type ManagedTerminalSnapshot = ProtocolManagedTerminalSnapshot;
@@ -247,16 +271,19 @@ export interface ManagedSpawnClient {
247
271
  * runtime context and is not sent over RPC.
248
272
  */
249
273
  spawn(
250
- task: WorkflowTask,
274
+ task: DispatchTask,
251
275
  runId: string,
252
276
  nodeId: string,
253
277
  attemptId?: string,
254
278
  legacyAttemptId?: string,
279
+ signal?: AbortSignal,
255
280
  ): Promise<ManagedSpawnResponse | string>;
256
281
  /** Unscoped v2 stop, retained for existing callers. */
257
282
  stop(agentId: string): Promise<void>;
258
283
  /** Owner-scoped v3 stop used by workflow lifecycle operations. */
259
284
  stopOwned?(agentId: string, owner: WorkflowOwner): Promise<void>;
285
+ /** Best-effort reconciliation for a spawn whose reply was lost after allocation. */
286
+ reconcileManaged?(spawnKey: string, owner: WorkflowOwner): Promise<ManagedSpawnResponse | string | undefined>;
260
287
  quiesceOwned?(
261
288
  runId: string,
262
289
  agentIds: string[],
@@ -281,26 +308,60 @@ export function createManagedSpawnClient(
281
308
  ): Promise<T> => callRpc(events, channel, payload, timeoutMs, signal);
282
309
 
283
310
  return {
284
- async spawn(task, runId, nodeId, attemptId) {
285
- const effectiveAttemptId = attemptId ?? `${runId}/${nodeId}/attempt-1`;
286
- const data = await call<unknown>(
287
- "subagents:rpc:spawn-managed",
288
- {
289
- requestId: requestId(),
290
- spawnKey: `${runId}/${nodeId}/${effectiveAttemptId}`,
291
- type: task.subagent_type,
292
- prompt: task.prompt,
293
- description: task.description,
294
- ...(task.tier === undefined ? {} : { tier: task.tier }),
295
- owner: {
296
- extension: "pi-workflows",
297
- runId,
298
- nodeId,
299
- attemptId: effectiveAttemptId,
311
+ /**
312
+ * Dispatch one managed agent call.
313
+ *
314
+ * `nodeId` is the runtime's `call-${callIndex}` and `attemptId` is
315
+ * `attempt-${generation}`. The spawnKey is opaque to pi-subagents, so the
316
+ * generation is the client-side rotation knob: when a resume replays a
317
+ * changed call (local callHash differs from the journaled one), the engine
318
+ * bumps the generation and this key changes — pi-subagents then treats the
319
+ * new key as a fresh spawn instead of throwing its fingerprint-conflict
320
+ * error for the same key with a different fingerprint. Same key + same
321
+ * fingerprint still reuses the persisted tombstone (per-call cache).
322
+ */
323
+ async spawn(task, runId, nodeId, attemptId, _legacyAttemptId, signal) {
324
+ const effectiveAttemptId = attemptId ?? `attempt-1`;
325
+ const spawnKey = `${runId}/${nodeId}/${effectiveAttemptId}`;
326
+ const requestSignal =
327
+ signal && sessionSignal ? AbortSignal.any([signal, sessionSignal]) : (signal ?? sessionSignal);
328
+ let data: unknown;
329
+ try {
330
+ data = await call<unknown>(
331
+ "subagents:rpc:spawn-managed",
332
+ {
333
+ requestId: requestId(),
334
+ spawnKey,
335
+ type: task.subagent_type,
336
+ prompt: task.prompt,
337
+ description: task.description,
338
+ ...(task.tier === undefined ? {} : { tier: task.tier }),
339
+ ...(task.toolset === undefined ? {} : { toolset: task.toolset }),
340
+ ...(task.thread === undefined ? {} : { thread: task.thread }),
341
+ ...(task.excludeTools === undefined ? {} : { excludeTools: task.excludeTools }),
342
+ ...(task.model === undefined ? {} : { model: task.model }),
343
+ ...(task.thinking === undefined ? {} : { thinking: task.thinking }),
344
+ ...(task.isolation === undefined ? {} : { isolation: task.isolation }),
345
+ owner: { extension: "pi-workflows", runId, nodeId, attemptId: effectiveAttemptId },
300
346
  },
301
- },
302
- sessionSignal,
303
- );
347
+ requestSignal,
348
+ );
349
+ } catch (error: unknown) {
350
+ // Reconciliation must not inherit the signal that caused the original
351
+ // request to fail: shutdown/abort can happen after allocation but before
352
+ // the reply. Fire it independently and preserve the original error.
353
+ void call<unknown>(
354
+ "subagents:rpc:reconcile-managed",
355
+ {
356
+ requestId: requestId(),
357
+ spawnKey,
358
+ owner: { extension: "pi-workflows", runId, nodeId, attemptId: effectiveAttemptId },
359
+ },
360
+ undefined,
361
+ 5_000,
362
+ ).catch(() => undefined);
363
+ throw error;
364
+ }
304
365
  if (!isRecord(data) || typeof data.id !== "string" || data.id.length === 0) {
305
366
  throw new Error("subagents managed spawn returned an invalid agent id");
306
367
  }
@@ -320,6 +381,16 @@ export function createManagedSpawnClient(
320
381
  sessionSignal,
321
382
  );
322
383
  },
384
+ async reconcileManaged(spawnKey, owner) {
385
+ const data = await call<unknown>(
386
+ "subagents:rpc:reconcile-managed",
387
+ { requestId: requestId(), spawnKey, owner },
388
+ undefined,
389
+ 5_000,
390
+ );
391
+ if (!isRecord(data) || typeof data.id !== "string") return undefined;
392
+ return parseManagedSpawnResponse(data);
393
+ },
323
394
  async quiesceOwned(runId, agentIds, timeoutMs = 5_000, owners) {
324
395
  const data = await call<unknown>(
325
396
  "subagents:rpc:quiesce-owned",