@osolmaz/pi-workflows 0.12.0 → 0.12.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 (119) hide show
  1. package/README.md +4 -3
  2. package/dist/builtins/autoimplement.workflow.d.ts +485 -126
  3. package/dist/builtins/autoimplement.workflow.js +17 -105
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/builtins/catalog.js +4 -4
  6. package/dist/builtins/index.d.ts +2 -1
  7. package/dist/builtins/index.js +2 -1
  8. package/dist/builtins/index.js.map +1 -1
  9. package/dist/builtins/monitor.workflow.d.ts +2 -4
  10. package/dist/builtins/monitor.workflow.js +26 -128
  11. package/dist/builtins/monitor.workflow.js.map +1 -1
  12. package/dist/builtins/pi-agent-group.d.ts +72 -0
  13. package/dist/builtins/pi-agent-group.js +1087 -0
  14. package/dist/builtins/pi-agent-group.js.map +1 -0
  15. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  16. package/dist/builtins/plan-approval.workflow.js +92 -14
  17. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  18. package/dist/builtins/plan-change.workflow.d.ts +301 -0
  19. package/dist/builtins/plan-change.workflow.js +256 -0
  20. package/dist/builtins/plan-change.workflow.js.map +1 -0
  21. package/dist/builtins/plan-presentation.js +2 -2
  22. package/dist/builtins/plan-presentation.js.map +1 -1
  23. package/dist/builtins/sanity-check.workflow.d.ts +5 -3
  24. package/dist/builtins/sanity-check.workflow.js +105 -21
  25. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  26. package/dist/extension/decision-channels.d.ts +2 -2
  27. package/dist/extension/decision-channels.js +22 -28
  28. package/dist/extension/decision-channels.js.map +1 -1
  29. package/dist/extension/index.js +62 -33
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/session-events.d.ts +2 -2
  32. package/dist/extension/widget.js +23 -3
  33. package/dist/extension/widget.js.map +1 -1
  34. package/dist/render/graph-render.js +1 -2
  35. package/dist/render/graph-render.js.map +1 -1
  36. package/dist/viewer/render.js +7 -6
  37. package/dist/viewer/render.js.map +1 -1
  38. package/dist/workflows/catalog.js +7 -2
  39. package/dist/workflows/catalog.js.map +1 -1
  40. package/dist/workflows/composition.js +8 -0
  41. package/dist/workflows/composition.js.map +1 -1
  42. package/dist/workflows/decision-presentation.d.ts +1 -1
  43. package/dist/workflows/decision-presentation.js +51 -38
  44. package/dist/workflows/decision-presentation.js.map +1 -1
  45. package/dist/workflows/engine.d.ts +2 -2
  46. package/dist/workflows/engine.js +14 -13
  47. package/dist/workflows/engine.js.map +1 -1
  48. package/dist/workflows/errors.d.ts +13 -0
  49. package/dist/workflows/errors.js +15 -0
  50. package/dist/workflows/errors.js.map +1 -1
  51. package/dist/workflows/human-decision.d.ts +16 -4
  52. package/dist/workflows/human-decision.js +175 -72
  53. package/dist/workflows/human-decision.js.map +1 -1
  54. package/dist/workflows/index.d.ts +2 -2
  55. package/dist/workflows/index.js +1 -1
  56. package/dist/workflows/index.js.map +1 -1
  57. package/dist/workflows/progress.d.ts +1 -0
  58. package/dist/workflows/progress.js +15 -3
  59. package/dist/workflows/progress.js.map +1 -1
  60. package/dist/workflows/schema.js +10 -0
  61. package/dist/workflows/schema.js.map +1 -1
  62. package/dist/workflows/store.js +5 -0
  63. package/dist/workflows/store.js.map +1 -1
  64. package/dist/workflows/types.d.ts +33 -45
  65. package/docs/HUMAN_DECISIONS.md +25 -35
  66. package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
  67. package/docs/MONITOR.md +5 -11
  68. package/docs/WORKFLOW_COMPOSITION.md +8 -7
  69. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  70. package/docs/plans/2026-08-21-sanity-check-plan.md +202 -94
  71. package/docs/run-bundles.md +6 -6
  72. package/docs/workflows.md +26 -6
  73. package/examples/workflows/approved-plan.workflow.ts +19 -46
  74. package/herdr-plugin.toml +1 -1
  75. package/package.json +7 -7
  76. package/schemas/human-decision-accepted-v1.schema.json +15 -3
  77. package/schemas/human-decision-continuation-v1.schema.json +10 -1
  78. package/schemas/human-decision-delivery-v1.schema.json +8 -0
  79. package/schemas/human-decision-receipt-v1.schema.json +8 -0
  80. package/schemas/human-decision-request-v1.schema.json +24 -4
  81. package/skills/autoimplement/SKILL.md +27 -0
  82. package/skills/monitor/SKILL.md +31 -3
  83. package/skills/pi-workflows/SKILL.md +2 -1
  84. package/skills/sanity-check/SKILL.md +44 -0
  85. package/src/builtins/autoimplement.workflow.ts +19 -118
  86. package/src/builtins/catalog.ts +4 -4
  87. package/src/builtins/index.ts +11 -0
  88. package/src/builtins/monitor.workflow.ts +27 -150
  89. package/src/builtins/pi-agent-group.ts +1407 -0
  90. package/src/builtins/plan-approval.workflow.ts +157 -24
  91. package/src/builtins/plan-change.workflow.ts +321 -0
  92. package/src/builtins/plan-presentation.ts +2 -2
  93. package/src/builtins/sanity-check.workflow.ts +186 -41
  94. package/src/extension/decision-channels.ts +29 -59
  95. package/src/extension/index.ts +79 -41
  96. package/src/extension/session-events.ts +2 -2
  97. package/src/extension/widget.ts +24 -5
  98. package/src/render/graph-render.ts +1 -2
  99. package/src/viewer/render.ts +7 -6
  100. package/src/workflows/catalog.ts +7 -2
  101. package/src/workflows/composition.ts +9 -0
  102. package/src/workflows/decision-presentation.ts +56 -43
  103. package/src/workflows/engine.ts +17 -15
  104. package/src/workflows/errors.ts +24 -0
  105. package/src/workflows/human-decision.ts +218 -101
  106. package/src/workflows/index.ts +5 -11
  107. package/src/workflows/progress.ts +18 -3
  108. package/src/workflows/schema.ts +17 -0
  109. package/src/workflows/store.ts +5 -0
  110. package/src/workflows/types.ts +39 -56
  111. package/dist/builtins/sanity-check-session.d.ts +0 -17
  112. package/dist/builtins/sanity-check-session.js +0 -168
  113. package/dist/builtins/sanity-check-session.js.map +0 -1
  114. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  115. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  116. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  117. package/schemas/human-decision-request-v2.schema.json +0 -69
  118. package/schemas/human-decision-resolution-v2.schema.json +0 -27
  119. package/src/builtins/sanity-check-session.ts +0 -205
@@ -18,6 +18,7 @@ import { compositionMetadata } from "../workflows/composition.js";
18
18
  import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
19
19
  import { WorkflowEngine } from "../workflows/engine.js";
20
20
  import {
21
+ BuiltinWorkflowRevisionChangedError,
21
22
  ClaimLostError,
22
23
  errorMessage,
23
24
  isClaimLostError,
@@ -41,6 +42,7 @@ import {
41
42
  } from "../workflows/store.js";
42
43
  import type {
43
44
  AcceptedHumanDecision,
45
+ ResolvedHumanDecision,
44
46
  AgentStepContract,
45
47
  HumanDecisionRequest,
46
48
  HumanDecisionSubmission,
@@ -48,6 +50,7 @@ import type {
48
50
  WorkflowDefinitionSnapshot,
49
51
  WorkflowRunResult,
50
52
  WorkflowRunState,
53
+ WorkflowSource,
51
54
  WorkflowUpdateRecord,
52
55
  } from "../workflows/types.js";
53
56
  import {
@@ -194,19 +197,14 @@ function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
194
197
  value !== null && typeof value === "object"
195
198
  ? (value as { schema?: unknown }).schema
196
199
  : undefined;
197
- if (
198
- schema !== "pi-workflows.human-decision-request.v1" &&
199
- schema !== "pi-workflows.human-decision-request.v2"
200
- ) {
201
- return null;
202
- }
200
+ if (schema !== "pi-workflows.human-decision-request.v1") return null;
203
201
  return value as HumanDecisionRequest;
204
202
  }
205
203
 
206
204
  type PreparedLaunchOptions = {
207
205
  presentation?: boolean;
208
206
  parentRunId?: string;
209
- humanDecision?: AcceptedHumanDecision;
207
+ humanDecision?: ResolvedHumanDecision;
210
208
  };
211
209
 
212
210
  type StartRunOptions = {
@@ -218,8 +216,8 @@ type StartRunOptions = {
218
216
  signal?: AbortSignal;
219
217
  /** Continue a checkpointed run: input becomes the legacy answer payload. */
220
218
  parentRunId?: string;
221
- /** Accepted verified-human answer for a protected decision continuation. */
222
- humanDecision?: AcceptedHumanDecision;
219
+ /** Durable human or timeout response for a protected decision continuation. */
220
+ humanDecision?: ResolvedHumanDecision;
223
221
  /** Resume a parked run at its stopped node instead of starting fresh. */
224
222
  resume?: boolean;
225
223
  /** An existing queue claim token, when the caller already claimed the run. */
@@ -237,6 +235,29 @@ function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unkn
237
235
  };
238
236
  }
239
237
 
238
+ function continuationSourceChangedError(
239
+ runId: string,
240
+ previous: WorkflowSource,
241
+ current: WorkflowSource,
242
+ ): Error {
243
+ if (
244
+ previous.kind === "builtin" &&
245
+ current.kind === "builtin" &&
246
+ previous.id === current.id &&
247
+ previous.revision !== current.revision
248
+ ) {
249
+ return new BuiltinWorkflowRevisionChangedError({
250
+ runId,
251
+ workflowId: current.id,
252
+ previousRevision: previous.revision,
253
+ currentRevision: current.revision,
254
+ });
255
+ }
256
+ return new Error(
257
+ `Workflow source changed since run ${runId} started; revert the edit to answer its checkpoint`,
258
+ );
259
+ }
260
+
240
261
  function preparedLaunchOptions(options: StartRunOptions): PreparedLaunchOptions {
241
262
  return {
242
263
  ...(options.presentation !== undefined ? { presentation: options.presentation } : {}),
@@ -1210,8 +1231,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
1210
1231
  parent.state.workflowSource !== undefined &&
1211
1232
  !isDeepStrictEqual(parent.state.workflowSource, workflowSource)
1212
1233
  ) {
1213
- throw new Error(
1214
- `Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`,
1234
+ throw continuationSourceChangedError(
1235
+ options.parentRunId,
1236
+ parent.state.workflowSource,
1237
+ workflowSource,
1215
1238
  );
1216
1239
  }
1217
1240
  }
@@ -2061,7 +2084,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2061
2084
  };
2062
2085
  const store = new HumanDecisionStore(new WorkflowRunStore().outputRoot);
2063
2086
  const acceptance = await store.accept(request, submission);
2064
- if (acceptance.status === "conflict") {
2087
+ if (acceptance.status === "conflict" || acceptance.decision.provenance !== "human") {
2065
2088
  throw new Error("That human decision was already answered differently.");
2066
2089
  }
2067
2090
  accepted = acceptance.decision;
@@ -2093,6 +2116,7 @@ export default function piWorkflows(pi: ExtensionAPI) {
2093
2116
  schema: "pi-workflows.human-decision-continuation.v1",
2094
2117
  decisionId: request.decisionId,
2095
2118
  requestDigest: request.requestDigest,
2119
+ provenance: accepted.provenance,
2096
2120
  parentRunId: waiting.parentRunId,
2097
2121
  runId: continuationRunId,
2098
2122
  createdAt: accepted.acceptedAt,
@@ -2218,60 +2242,72 @@ export default function piWorkflows(pi: ExtensionAPI) {
2218
2242
  queueRecord !== undefined &&
2219
2243
  (queueRecord.originSessionId === null ||
2220
2244
  queueRecord.originSessionId === ctx.sessionManager.getSessionId());
2221
- const accepted = await store.readAccepted(request.decisionId);
2222
- if (accepted === null) {
2245
+ let resolved = await store.readResolved(request.decisionId);
2246
+ if (resolved === null) {
2223
2247
  let cancellation = await store.readCancellation(request.decisionId);
2224
- if (
2225
- cancellation === null &&
2226
- request.expiresAt !== undefined &&
2227
- Date.parse(request.expiresAt) <= Date.now()
2228
- ) {
2229
- await store.cancel(request, "expired");
2230
- cancellation = await store.readCancellation(request.decisionId);
2248
+ const expired =
2249
+ request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now();
2250
+ if (cancellation === null && expired) {
2251
+ if (request.defaultResponse === undefined) {
2252
+ await store.cancel(request, "expired");
2253
+ cancellation = await store.readCancellation(request.decisionId);
2254
+ } else {
2255
+ try {
2256
+ resolved = (await store.resolveTimeout(request)).decision;
2257
+ } catch {
2258
+ cancellation = await store.readCancellation(request.decisionId);
2259
+ resolved = await store.readResolved(request.decisionId);
2260
+ }
2261
+ }
2231
2262
  }
2232
2263
  if (cancellation !== null) {
2233
2264
  await settleHumanDecisionChannels(cancellation);
2234
2265
  continue;
2235
2266
  }
2236
- if (!deliverPending || !ownedBySession) continue;
2237
- const channels = audienceChannels(decisionChannelConfig, request.audience);
2238
- for (const channelId of channels) {
2239
- const channel = telegramDecisionChannels.get(channelId);
2240
- if (channel !== undefined) await channel.deliver(humanDecisionChannelRequest(request));
2241
- }
2242
- if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
2243
- lastWaitingRunId = request.runId;
2244
- queueMicrotask(() => void promptHumanDecision(ctx, request));
2267
+ if (resolved === null) {
2268
+ if (!deliverPending || !ownedBySession) continue;
2269
+ const channels = audienceChannels(decisionChannelConfig, request.audience);
2270
+ for (const channelId of channels) {
2271
+ const channel = telegramDecisionChannels.get(channelId);
2272
+ if (channel !== undefined) await channel.deliver(humanDecisionChannelRequest(request));
2273
+ }
2274
+ if (ctx.mode === "tui" && channels.includes("pi") && lastWaitingRunId === null) {
2275
+ lastWaitingRunId = request.runId;
2276
+ queueMicrotask(() => void promptHumanDecision(ctx, request));
2277
+ }
2278
+ continue;
2245
2279
  }
2246
- continue;
2247
2280
  }
2248
2281
 
2249
2282
  if (!ownedBySession) continue;
2283
+ const currentParent = await readRunBundle(runStore.runDirFor(request.runId));
2284
+ if (currentParent === null || currentParent.state.status !== "waiting") continue;
2250
2285
  const runId = `continuation-${request.decisionId.slice("decision-".length)}`;
2251
2286
  const continuation = (await store.readContinuation(request.decisionId)) ?? {
2252
2287
  schema: "pi-workflows.human-decision-continuation.v1" as const,
2253
2288
  decisionId: request.decisionId,
2254
2289
  requestDigest: request.requestDigest,
2290
+ provenance: resolved.provenance,
2255
2291
  parentRunId: request.runId,
2256
2292
  runId,
2257
- createdAt: accepted.acceptedAt,
2293
+ createdAt: resolved.acceptedAt,
2258
2294
  };
2259
2295
  await store.recordContinuation(request.decisionId, continuation);
2260
2296
  const existing = await readRunBundle(runStore.runDirFor(continuation.runId));
2261
2297
  if (existing === null && activeRun === null) {
2262
- if (parent.state.workflowSource === undefined) continue;
2298
+ if (currentParent.state.workflowSource === undefined) continue;
2263
2299
  const workflowRef =
2264
- parent.state.workflowSource.kind === "builtin"
2265
- ? `builtin:${parent.state.workflowSource.id}`
2266
- : parent.state.workflowSource.path;
2267
- await startRun(ctx, workflowRef, parent.state.input, {
2300
+ currentParent.state.workflowSource.kind === "builtin"
2301
+ ? `builtin:${currentParent.state.workflowSource.id}`
2302
+ : currentParent.state.workflowSource.path;
2303
+ await startRun(ctx, workflowRef, currentParent.state.input, {
2268
2304
  parentRunId: request.runId,
2269
- humanDecision: accepted,
2305
+ humanDecision: resolved,
2270
2306
  runId: continuation.runId,
2271
2307
  quiet: true,
2272
2308
  });
2273
2309
  }
2274
- await settleHumanDecisionChannels(accepted);
2310
+ await settleHumanDecisionChannels(resolved);
2275
2311
  if (activeRun !== null) break;
2276
2312
  }
2277
2313
  };
@@ -2352,8 +2388,10 @@ export default function piWorkflows(pi: ExtensionAPI) {
2352
2388
  parent.state.workflowSource !== undefined &&
2353
2389
  !isDeepStrictEqual(parent.state.workflowSource, workflowSource)
2354
2390
  ) {
2355
- throw new Error(
2356
- `Workflow source changed since run ${options.parentRunId} started; revert the edit to answer its checkpoint`,
2391
+ throw continuationSourceChangedError(
2392
+ options.parentRunId,
2393
+ parent.state.workflowSource,
2394
+ workflowSource,
2357
2395
  );
2358
2396
  }
2359
2397
  }
@@ -9,7 +9,7 @@ export type PublicAssistantMessageEvent =
9
9
  | { type: "toolcall_start"; contentIndex: number; partial: unknown }
10
10
  | { type: "toolcall_delta"; contentIndex: number; delta: string; partial: unknown }
11
11
  | { type: "toolcall_end"; contentIndex: number; toolCall: unknown; partial: unknown }
12
- | { type: "done"; reason: "stop" | "length" | "toolUse"; message: unknown }
12
+ | { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred"; message: unknown }
13
13
  | { type: "error"; reason: "aborted" | "error"; error: unknown };
14
14
 
15
15
  export type TurnStartEventLike = { turnIndex: number };
@@ -44,7 +44,7 @@ export type NormalizedAssistantEvent =
44
44
  | { type: "toolcall_start"; contentIndex: number }
45
45
  | { type: "toolcall_delta"; contentIndex: number; delta: string }
46
46
  | { type: "toolcall_end"; contentIndex: number; toolCall: unknown }
47
- | { type: "done"; reason: "stop" | "length" | "toolUse" }
47
+ | { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred" }
48
48
  | { type: "error"; reason: "aborted" | "error" };
49
49
 
50
50
  /** Remove cumulative snapshots while preserving every semantic stream event. */
@@ -162,9 +162,11 @@ function progressLines(
162
162
  : progressTracksFromRecords(updateHistory, now).map((track) => track.estimate);
163
163
  const projected = mergeProgressEstimates(latestProgressEstimates(state, now), measured ?? []);
164
164
  const estimates = mergeProgressEstimates(projected, monitorEstimates(state) ?? []);
165
- const lines = prioritizeProgressEstimates(estimates).map((estimate) =>
166
- formatProgressLine(estimate, now),
167
- );
165
+ const lines = prioritizeWidgetProgress(prioritizeProgressEstimates(estimates)).map((estimate) => {
166
+ const indentation =
167
+ estimate.key.startsWith("agents/") && estimate.key.split("/").length > 2 ? " " : "";
168
+ return `${indentation}${formatProgressLine(estimate, now)}`;
169
+ });
168
170
  const schedule = (state.updates ?? []).find(
169
171
  (record) => record.type === "monitor.schedule" && record.key === "next-check",
170
172
  );
@@ -180,6 +182,24 @@ function progressLines(
180
182
  return lines;
181
183
  }
182
184
 
185
+ function prioritizeWidgetProgress(estimates: ProgressEstimate[]): ProgressEstimate[] {
186
+ const weight = (estimate: ProgressEstimate) => {
187
+ if (!estimate.key.startsWith("agents/")) return 0;
188
+ const child = estimate.key.split("/").length > 2;
189
+ if (!child) return -4;
190
+ if (estimate.data.status === "failed" || estimate.data.status === "blocked") return -3;
191
+ if (
192
+ estimate.data.status === "running" ||
193
+ estimate.data.status === "pending" ||
194
+ estimate.data.status === "waiting"
195
+ ) {
196
+ return -2;
197
+ }
198
+ return 2;
199
+ };
200
+ return [...estimates].sort((left, right) => weight(left) - weight(right));
201
+ }
202
+
183
203
  function monitorEstimates(state: WorkflowRunState): ProgressEstimate[] | undefined {
184
204
  const output = state.outputs.estimate;
185
205
  if (output === null || typeof output !== "object" || Array.isArray(output)) return undefined;
@@ -309,8 +329,7 @@ function nodeRuntimeSegments(
309
329
  })
310
330
  : undefined;
311
331
  const requestAudience =
312
- (request?.schema === "pi-workflows.human-decision-request.v1" ||
313
- request?.schema === "pi-workflows.human-decision-request.v2") &&
332
+ request?.schema === "pi-workflows.human-decision-request.v1" &&
314
333
  typeof request.audience === "string"
315
334
  ? request.audience
316
335
  : human.audience;
@@ -313,8 +313,7 @@ function renderCellText(
313
313
  ? (state.finalOutput as { schema?: unknown }).schema
314
314
  : undefined;
315
315
  const waitingRequest =
316
- waitingSchema === "pi-workflows.human-decision-request.v1" ||
317
- waitingSchema === "pi-workflows.human-decision-request.v2"
316
+ waitingSchema === "pi-workflows.human-decision-request.v1"
318
317
  ? (state.finalOutput as {
319
318
  audience?: unknown;
320
319
  presentationDigest?: unknown;
@@ -147,13 +147,13 @@ function nodeStatusLine(bundle: LoadedRunBundle, nodeId: string, width: number,
147
147
  suffix = ansi.yellow(
148
148
  human === undefined
149
149
  ? " waiting"
150
- : ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request?.schema === "pi-workflows.human-decision-request.v2" ? ` · ${sanitizeText(request.presentation.summary)}` : ""} · ${Object.values(
150
+ : ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request === null ? "" : ` · ${sanitizeText(request.presentation.summary)}`} · ${Object.values(
151
151
  human.choices,
152
152
  )
153
153
  .map((choice) => sanitizeText(choice.label))
154
154
  .join(
155
155
  " / ",
156
- )}${request?.schema === "pi-workflows.human-decision-request.v2" ? ` · ${request.presentationDigest.slice(7, 19)}` : ""}`,
156
+ )}${request === null ? "" : ` · ${request.presentationDigest.slice(7, 19)}`}`,
157
157
  );
158
158
  } else if (result) {
159
159
  glyph = result.outcome === "ok" ? ansi.green("✓") : ansi.red("✗");
@@ -210,8 +210,7 @@ function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
210
210
  function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
211
211
  if (value === null || typeof value !== "object") return null;
212
212
  const schema = (value as { schema?: unknown }).schema;
213
- return schema === "pi-workflows.human-decision-request.v1" ||
214
- schema === "pi-workflows.human-decision-request.v2"
213
+ return schema === "pi-workflows.human-decision-request.v1"
215
214
  ? (value as HumanDecisionRequest)
216
215
  : null;
217
216
  }
@@ -271,12 +270,14 @@ export function renderRunDetailLines(
271
270
  lines.push("");
272
271
  lines.push(ansi.bold("progress"));
273
272
  for (const track of progress) {
274
- lines.push(fitWidth(formatProgressLine(track.estimate, now), size.width));
273
+ const depth = track.key.startsWith("agents/") ? track.key.split("/").length - 2 : 0;
274
+ const indentation = " ".repeat(Math.max(0, depth));
275
+ lines.push(fitWidth(`${indentation}${formatProgressLine(track.estimate, now)}`, size.width));
275
276
  const latest = track.samples.at(-1);
276
277
  lines.push(
277
278
  fitWidth(
278
279
  ansi.dim(
279
- ` ${track.estimate.sampleCount} samples · ${track.estimate.confidence ?? "no"} confidence · updated ${latest?.at ?? "unknown"}`,
280
+ `${indentation} ${track.estimate.sampleCount} samples · ${track.estimate.confidence ?? "no"} confidence · updated ${latest?.at ?? "unknown"}`,
280
281
  ),
281
282
  size.width,
282
283
  ),
@@ -1,5 +1,5 @@
1
1
  import { isWorkflowDefinition } from "./definition.js";
2
- import { WorkflowSourceChangedError } from "./errors.js";
2
+ import { BuiltinWorkflowRevisionChangedError } from "./errors.js";
3
3
  import type { WorkflowDefinition, WorkflowSource } from "./types.js";
4
4
 
5
5
  const BUILTIN_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
@@ -102,7 +102,12 @@ export class BuiltinWorkflowCatalog {
102
102
  throw new Error(`Unknown built-in workflow: ${source.id}`);
103
103
  }
104
104
  if (entry.revision !== source.revision) {
105
- throw new WorkflowSourceChangedError(runId);
105
+ throw new BuiltinWorkflowRevisionChangedError({
106
+ runId,
107
+ workflowId: entry.id,
108
+ previousRevision: source.revision,
109
+ currentRevision: entry.revision,
110
+ });
106
111
  }
107
112
  return entry.definition;
108
113
  }
@@ -480,6 +480,7 @@ function wrapNode(
480
480
  case "checkpoint": {
481
481
  const human = node.humanDecision;
482
482
  const audience = human?.audience;
483
+ const onTimeout = human?.onTimeout;
483
484
  return {
484
485
  ...common,
485
486
  nodeType: "checkpoint",
@@ -496,6 +497,14 @@ function wrapNode(
496
497
  : audience!,
497
498
  choices: human.choices,
498
499
  request: (context: WorkflowNodeContext) => human.request(project(context)),
500
+ ...(onTimeout !== undefined
501
+ ? {
502
+ onTimeout:
503
+ typeof onTimeout === "function"
504
+ ? (context: WorkflowNodeContext) => onTimeout(project(context))
505
+ : onTimeout,
506
+ }
507
+ : {}),
499
508
  },
500
509
  }
501
510
  : {}),
@@ -56,7 +56,18 @@ export function decisionPresentationDigest(presentation: DecisionPresentation):
56
56
  export function validateHumanDecisionRequestIntegrity(
57
57
  request: HumanDecisionRequest,
58
58
  ): HumanDecisionRequest {
59
- if (request.schema === "pi-workflows.human-decision-request.v1") return request;
59
+ if (
60
+ request.schema !== "pi-workflows.human-decision-request.v1" ||
61
+ !Object.hasOwn(request, "subject") ||
62
+ !Object.hasOwn(request, "presentation") ||
63
+ !Object.hasOwn(request, "subjectDigest") ||
64
+ !Object.hasOwn(request, "presentationDigest") ||
65
+ !Object.hasOwn(request, "revision")
66
+ ) {
67
+ throw new Error(
68
+ "Human decision state uses an incompatible alpha contract; reset the affected workflow run and decision state.",
69
+ );
70
+ }
60
71
  const presentation = normalizeDecisionPresentation(request.presentation);
61
72
  const subjectDigest = digestCanonical(request.subject);
62
73
  const presentationDigest = digestCanonical(presentation);
@@ -70,6 +81,7 @@ export function validateHumanDecisionRequestIntegrity(
70
81
  title: request.title,
71
82
  choices: request.choices,
72
83
  ...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}),
84
+ ...(request.defaultResponse !== undefined ? { defaultResponse: request.defaultResponse } : {}),
73
85
  subject: request.subject,
74
86
  presentation,
75
87
  revision: request.revision,
@@ -90,10 +102,7 @@ export function humanDecisionChannelRequest(
90
102
  request: HumanDecisionRequest,
91
103
  ): HumanDecisionChannelRequest {
92
104
  validateHumanDecisionRequestIntegrity(request);
93
- const presentation =
94
- request.schema === "pi-workflows.human-decision-request.v2"
95
- ? normalizeDecisionPresentation(request.presentation)
96
- : legacyDecisionPresentation(request.body);
105
+ const presentation = normalizeDecisionPresentation(request.presentation);
97
106
  const presentationDigest = decisionPresentationDigest(presentation);
98
107
  return {
99
108
  schema: "pi-workflows.human-decision-channel-request.v1",
@@ -108,25 +117,26 @@ export function humanDecisionChannelRequest(
108
117
  title: request.title,
109
118
  presentation,
110
119
  presentationDigest,
111
- revision: request.schema === "pi-workflows.human-decision-request.v2" ? request.revision : 1,
120
+ revision: request.revision,
112
121
  choices: request.choices,
113
122
  createdAt: request.createdAt,
114
123
  ...(request.expiresAt !== undefined ? { expiresAt: request.expiresAt } : {}),
124
+ ...(request.defaultResponse !== undefined ? { defaultResponse: request.defaultResponse } : {}),
115
125
  };
116
126
  }
117
127
 
118
- export function legacyDecisionPresentation(body: unknown): DecisionPresentation {
128
+ export function valueDecisionPresentation(value: unknown): DecisionPresentation {
119
129
  const blocks: DecisionPresentationBlock[] = [];
120
- if (typeof body === "string") {
121
- appendLegacyText(blocks, body, "Decision details");
130
+ if (typeof value === "string") {
131
+ appendValueText(blocks, value, "Decision details");
122
132
  } else {
123
- appendLegacyValue(blocks, body, undefined);
133
+ appendValue(blocks, value, undefined);
124
134
  }
125
135
  const summary =
126
- typeof body === "string"
136
+ typeof value === "string"
127
137
  ? "Review the decision message below."
128
138
  : "Review the decision details below.";
129
- return normalizeDecisionPresentation(boundLegacyPresentation(summary, blocks, body));
139
+ return normalizeDecisionPresentation(boundValuePresentation(summary, blocks, value));
130
140
  }
131
141
 
132
142
  export function decisionDocumentSegments(
@@ -150,6 +160,13 @@ export function decisionDocumentSegments(
150
160
  segments.push({ kind: block.kind, text: block.text });
151
161
  }
152
162
  }
163
+ if (request.expiresAt !== undefined && request.defaultResponse !== undefined) {
164
+ const selected = request.choices[request.defaultResponse.choice];
165
+ segments.push({
166
+ kind: "paragraph",
167
+ text: `If no answer is accepted by ${request.expiresAt}, this decision continues automatically with: ${selected?.label ?? request.defaultResponse.choice}.`,
168
+ });
169
+ }
153
170
  const choices = Object.values(request.choices).map((choice) =>
154
171
  choice.input === undefined
155
172
  ? `• ${choice.label}`
@@ -251,10 +268,10 @@ function normalizeString(value: unknown, label: string): string {
251
268
  return text;
252
269
  }
253
270
 
254
- function boundLegacyPresentation(
271
+ function boundValuePresentation(
255
272
  summary: string,
256
273
  blocks: DecisionPresentationBlock[],
257
- body: unknown,
274
+ value: unknown,
258
275
  ): DecisionPresentation {
259
276
  const candidate: DecisionPresentation = {
260
277
  schema: "pi-workflows.decision-presentation.v1",
@@ -263,13 +280,13 @@ function boundLegacyPresentation(
263
280
  };
264
281
  if (
265
282
  blocks.length <= MAX_PRESENTATION_BLOCKS &&
266
- blocks.every(legacyBlockFitsSchema) &&
283
+ blocks.every(valueBlockFitsSchema) &&
267
284
  presentationCodeUnits(candidate) <= MAX_PRESENTATION_CODE_UNITS
268
285
  ) {
269
286
  return candidate;
270
287
  }
271
- const originalCodeUnits = canonicalJson(body).length;
272
- const omission = `Some legacy decision content is not shown because it exceeds the readable presentation limit. Inspect the durable v1 request before answering. Body digest: ${digestCanonical(body)}. Original body: ${originalCodeUnits} UTF-16 code units. Generated blocks: ${blocks.length}.`;
288
+ const originalCodeUnits = canonicalJson(value).length;
289
+ const omission = `Some decision content is not shown because it exceeds the readable presentation limit. Inspect the canonical source value before answering. Value digest: ${digestCanonical(value)}. Original value: ${originalCodeUnits} UTF-16 code units. Generated blocks: ${blocks.length}.`;
273
290
  const budget = MAX_PRESENTATION_CODE_UNITS - summary.length - omission.length;
274
291
  const kept: DecisionPresentationBlock[] = [];
275
292
  let used = 0;
@@ -278,7 +295,7 @@ function boundLegacyPresentation(
278
295
  const units = presentationBlockCodeUnits(block);
279
296
  if (
280
297
  kept.length >= MAX_PRESENTATION_BLOCKS ||
281
- !legacyBlockFitsSchema(block) ||
298
+ !valueBlockFitsSchema(block) ||
282
299
  used + units > budget
283
300
  ) {
284
301
  omitted = true;
@@ -298,7 +315,7 @@ function boundLegacyPresentation(
298
315
  };
299
316
  }
300
317
 
301
- function legacyBlockFitsSchema(block: DecisionPresentationBlock): boolean {
318
+ function valueBlockFitsSchema(block: DecisionPresentationBlock): boolean {
302
319
  if (block.kind === "paragraph" || block.kind === "preformatted") {
303
320
  return block.text.length <= MAX_PRESENTATION_STRING_CODE_UNITS;
304
321
  }
@@ -332,47 +349,43 @@ function presentationCodeUnits(presentation: DecisionPresentation): number {
332
349
  return total;
333
350
  }
334
351
 
335
- function appendLegacyValue(
352
+ function appendValue(
336
353
  blocks: DecisionPresentationBlock[],
337
354
  value: unknown,
338
355
  label: string | undefined,
339
356
  ): void {
340
357
  if (isScalar(value)) {
341
- const scalar = legacyScalar(value);
358
+ const scalar = valueScalar(value);
342
359
  if (label === undefined || scalar.length > MAX_PRESENTATION_STRING_CODE_UNITS) {
343
- appendLegacyText(
344
- blocks,
345
- scalar,
346
- label === undefined ? "Decision details" : legacyLabel(label),
347
- );
360
+ appendValueText(blocks, scalar, label === undefined ? "Decision details" : valueLabel(label));
348
361
  } else {
349
- blocks.push({ kind: "fields", items: [{ label: legacyLabel(label), value: scalar }] });
362
+ blocks.push({ kind: "fields", items: [{ label: valueLabel(label), value: scalar }] });
350
363
  }
351
364
  return;
352
365
  }
353
366
  if (Array.isArray(value)) {
354
- if (label !== undefined) blocks.push({ kind: "section", title: legacyLabel(label) });
367
+ if (label !== undefined) blocks.push({ kind: "section", title: valueLabel(label) });
355
368
  if (value.length === 0) {
356
369
  blocks.push({ kind: "paragraph", text: "None" });
357
370
  return;
358
371
  }
359
372
  if (
360
373
  value.every(isScalar) &&
361
- value.every((item) => legacyScalar(item).length <= MAX_PRESENTATION_STRING_CODE_UNITS)
374
+ value.every((item) => valueScalar(item).length <= MAX_PRESENTATION_STRING_CODE_UNITS)
362
375
  ) {
363
- const items = value.map(legacyScalar);
376
+ const items = value.map(valueScalar);
364
377
  for (let index = 0; index < items.length; index += MAX_PRESENTATION_ITEMS) {
365
378
  blocks.push({ kind: "bullets", items: items.slice(index, index + MAX_PRESENTATION_ITEMS) });
366
379
  }
367
380
  return;
368
381
  }
369
- value.forEach((item, index) => appendLegacyValue(blocks, item, `Item ${index + 1}`));
382
+ value.forEach((item, index) => appendValue(blocks, item, `Item ${index + 1}`));
370
383
  return;
371
384
  }
372
385
  const entries = Object.entries(value as Record<string, unknown>).sort(([left], [right]) =>
373
386
  compareKeys(left, right),
374
387
  );
375
- if (label !== undefined) blocks.push({ kind: "section", title: legacyLabel(label) });
388
+ if (label !== undefined) blocks.push({ kind: "section", title: valueLabel(label) });
376
389
  if (entries.length === 0) {
377
390
  blocks.push({ kind: "paragraph", text: "None" });
378
391
  return;
@@ -380,34 +393,34 @@ function appendLegacyValue(
380
393
  const fields = entries
381
394
  .filter(
382
395
  ([, child]) =>
383
- isScalar(child) && legacyScalar(child).length <= MAX_PRESENTATION_STRING_CODE_UNITS,
396
+ isScalar(child) && valueScalar(child).length <= MAX_PRESENTATION_STRING_CODE_UNITS,
384
397
  )
385
- .map(([key, child]) => ({ label: legacyLabel(key), value: legacyScalar(child) }));
398
+ .map(([key, child]) => ({ label: valueLabel(key), value: valueScalar(child) }));
386
399
  for (let index = 0; index < fields.length; index += MAX_PRESENTATION_ITEMS) {
387
400
  blocks.push({ kind: "fields", items: fields.slice(index, index + MAX_PRESENTATION_ITEMS) });
388
401
  }
389
402
  for (const [key, child] of entries.filter(
390
403
  ([, child]) =>
391
- !isScalar(child) || legacyScalar(child).length > MAX_PRESENTATION_STRING_CODE_UNITS,
404
+ !isScalar(child) || valueScalar(child).length > MAX_PRESENTATION_STRING_CODE_UNITS,
392
405
  )) {
393
- appendLegacyValue(blocks, child, key);
406
+ appendValue(blocks, child, key);
394
407
  }
395
408
  }
396
409
 
397
- function appendLegacyText(
410
+ function appendValueText(
398
411
  blocks: DecisionPresentationBlock[],
399
412
  value: string,
400
413
  section: string,
401
414
  ): void {
402
- const text = sanitizeLegacyText(value);
415
+ const text = sanitizeValueText(value);
403
416
  blocks.push({ kind: "section", title: section });
404
417
  for (const part of splitByCodeUnits(text, MAX_PRESENTATION_STRING_CODE_UNITS)) {
405
418
  blocks.push({ kind: "paragraph", text: part.trim().length === 0 ? "None" : part });
406
419
  }
407
420
  }
408
421
 
409
- function legacyLabel(value: string): string {
410
- const sanitized = sanitizeLegacyText(value)
422
+ function valueLabel(value: string): string {
423
+ const sanitized = sanitizeValueText(value)
411
424
  .replaceAll(/[_-]+/gu, " ")
412
425
  .replaceAll(/([a-z0-9])([A-Z])/gu, "$1 $2")
413
426
  .replaceAll(/\s+/gu, " ")
@@ -416,13 +429,13 @@ function legacyLabel(value: string): string {
416
429
  return `${sanitized[0]!.toUpperCase()}${sanitized.slice(1)}`;
417
430
  }
418
431
 
419
- function legacyScalar(value: unknown): string {
432
+ function valueScalar(value: unknown): string {
420
433
  if (value === null) return "None";
421
434
  if (typeof value === "boolean") return value ? "Yes" : "No";
422
- return sanitizeLegacyText(String(value));
435
+ return sanitizeValueText(String(value));
423
436
  }
424
437
 
425
- function sanitizeLegacyText(value: string): string {
438
+ function sanitizeValueText(value: string): string {
426
439
  return value
427
440
  .replaceAll("\r\n", "\n")
428
441
  .replaceAll("\r", "\n")