@osolmaz/pi-workflows 0.11.2 → 0.12.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.
Files changed (123) hide show
  1. package/README.md +11 -7
  2. package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
  3. package/dist/builtins/autoimplement-command-batches.js +258 -0
  4. package/dist/builtins/autoimplement-command-batches.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +29 -3
  6. package/dist/builtins/autoimplement.workflow.js +876 -231
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/catalog.js +3 -1
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +2 -0
  11. package/dist/builtins/index.js +1 -0
  12. package/dist/builtins/index.js.map +1 -1
  13. package/dist/builtins/sanity-check-session.d.ts +17 -0
  14. package/dist/builtins/sanity-check-session.js +168 -0
  15. package/dist/builtins/sanity-check-session.js.map +1 -0
  16. package/dist/builtins/sanity-check.workflow.d.ts +83 -0
  17. package/dist/builtins/sanity-check.workflow.js +398 -0
  18. package/dist/builtins/sanity-check.workflow.js.map +1 -0
  19. package/dist/controllers/sqlite.d.ts +90 -2
  20. package/dist/controllers/sqlite.js +380 -5
  21. package/dist/controllers/sqlite.js.map +1 -1
  22. package/dist/extension/decision-channels.js +7 -7
  23. package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
  24. package/dist/extension/deferred-turn-coordinator.js +143 -0
  25. package/dist/extension/deferred-turn-coordinator.js.map +1 -0
  26. package/dist/extension/deferred-turn.d.ts +44 -0
  27. package/dist/extension/deferred-turn.js +110 -0
  28. package/dist/extension/deferred-turn.js.map +1 -0
  29. package/dist/extension/index.js +286 -47
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/step-message.d.ts +1 -0
  32. package/dist/extension/step-message.js.map +1 -1
  33. package/dist/herdr/setup.js +4 -4
  34. package/dist/workflows/command-batch.d.ts +38 -0
  35. package/dist/workflows/command-batch.js +176 -0
  36. package/dist/workflows/command-batch.js.map +1 -0
  37. package/dist/workflows/engine.js +8 -4
  38. package/dist/workflows/engine.js.map +1 -1
  39. package/dist/workflows/index.d.ts +1 -0
  40. package/dist/workflows/index.js +1 -0
  41. package/dist/workflows/index.js.map +1 -1
  42. package/dist/workflows/schema.js +2 -1
  43. package/dist/workflows/schema.js.map +1 -1
  44. package/dist/workflows/shell.d.ts +4 -0
  45. package/dist/workflows/shell.js +6 -0
  46. package/dist/workflows/shell.js.map +1 -1
  47. package/dist/workflows/store.js +3 -1
  48. package/dist/workflows/store.js.map +1 -1
  49. package/dist/workflows/types.d.ts +4 -3
  50. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  51. package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
  52. package/docs/CONTROLLERS.md +2 -2
  53. package/docs/DEFERRED_TURNS.md +298 -0
  54. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  55. package/docs/HUMAN_DECISIONS.md +7 -7
  56. package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
  57. package/docs/MONITOR.md +2 -2
  58. package/docs/WORKFLOW_COMPOSITION.md +2 -2
  59. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  60. package/docs/WORKFLOW_UPDATES.md +28 -5
  61. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  62. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  63. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  64. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  65. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  66. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  67. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  68. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  69. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  70. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  71. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  72. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  73. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  74. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  75. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  76. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  77. package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
  78. package/docs/run-bundles.md +3 -1
  79. package/docs/workflows.md +62 -19
  80. package/examples/workflows/command-batch.workflow.ts +50 -0
  81. package/examples/workflows/sanity-check.workflow.ts +1 -0
  82. package/herdr-plugin.toml +3 -3
  83. package/package.json +1 -1
  84. package/schemas/decision-presentation-v1.schema.json +1 -1
  85. package/schemas/human-decision-accepted-v1.schema.json +1 -1
  86. package/schemas/human-decision-accepted-v2.schema.json +1 -1
  87. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  88. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  89. package/schemas/human-decision-continuation-v1.schema.json +1 -1
  90. package/schemas/human-decision-delivery-v1.schema.json +1 -1
  91. package/schemas/human-decision-delivery-v2.schema.json +1 -1
  92. package/schemas/human-decision-receipt-v1.schema.json +1 -1
  93. package/schemas/human-decision-receipt-v2.schema.json +1 -1
  94. package/schemas/human-decision-request-v1.schema.json +1 -1
  95. package/schemas/human-decision-request-v2.schema.json +1 -1
  96. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  97. package/schemas/human-decision-resolution-v2.schema.json +1 -1
  98. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  99. package/skills/autodoc/SKILL.md +33 -2
  100. package/skills/autoimplement/SKILL.md +66 -12
  101. package/skills/autoplan/SKILL.md +26 -2
  102. package/skills/monitor/SKILL.md +35 -21
  103. package/skills/pi-workflows/SKILL.md +19 -5
  104. package/src/builtins/autoimplement-command-batches.ts +364 -0
  105. package/src/builtins/autoimplement.workflow.ts +1065 -261
  106. package/src/builtins/catalog.ts +3 -1
  107. package/src/builtins/index.ts +13 -0
  108. package/src/builtins/sanity-check-session.ts +205 -0
  109. package/src/builtins/sanity-check.workflow.ts +624 -0
  110. package/src/controllers/sqlite.ts +580 -7
  111. package/src/extension/decision-channels.ts +7 -7
  112. package/src/extension/deferred-turn-coordinator.ts +171 -0
  113. package/src/extension/deferred-turn.ts +166 -0
  114. package/src/extension/index.ts +359 -49
  115. package/src/extension/step-message.ts +1 -0
  116. package/src/herdr/setup.ts +4 -4
  117. package/src/workflows/command-batch.ts +254 -0
  118. package/src/workflows/engine.ts +8 -5
  119. package/src/workflows/index.ts +15 -0
  120. package/src/workflows/schema.ts +2 -1
  121. package/src/workflows/shell.ts +10 -0
  122. package/src/workflows/store.ts +3 -1
  123. package/src/workflows/types.ts +4 -3
@@ -0,0 +1,254 @@
1
+ import path from "node:path";
2
+ import { CancelledError, TimeoutError, errorMessage, isAbortLikeError } from "./errors.js";
3
+ import { runShellAction, shellOutputTruncation, shellResultFromError } from "./shell.js";
4
+ import type { MaybePromise, ShellActionResult } from "./types.js";
5
+
6
+ export const COMMAND_BATCH_RESULT_SCHEMA = "pi-workflows.command-batch-result.v1";
7
+ export const MAX_COMMAND_BATCH_ITEMS = 64;
8
+ export const MAX_COMMAND_BATCH_CONCURRENCY = 8;
9
+ export const MAX_COMMAND_BATCH_TIMEOUT_MS = 60 * 60_000;
10
+ export const MAX_COMMAND_BATCH_OUTPUT_CHARS = 1_000_000;
11
+
12
+ const MAX_ERROR_CHARS = 2_000;
13
+ const ITEM_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
14
+
15
+ export type CommandBatchItem = {
16
+ id: string;
17
+ command: string;
18
+ args: string[];
19
+ cwd: string;
20
+ timeoutMs: number;
21
+ maxOutputChars: number;
22
+ };
23
+
24
+ export type CommandBatchRequest = {
25
+ items: CommandBatchItem[];
26
+ maxConcurrency: number;
27
+ };
28
+
29
+ export type CommandBatchItemOutcome = "succeeded" | "failed" | "timedOut" | "cancelled";
30
+
31
+ export type CommandBatchItemResult = ShellActionResult & {
32
+ id: string;
33
+ outcome: CommandBatchItemOutcome;
34
+ stdoutTruncated: boolean;
35
+ stderrTruncated: boolean;
36
+ error?: string;
37
+ };
38
+
39
+ export type CommandBatchResult = {
40
+ schema: typeof COMMAND_BATCH_RESULT_SCHEMA;
41
+ items: CommandBatchItemResult[];
42
+ completed: number;
43
+ total: number;
44
+ };
45
+
46
+ export type RunCommandBatchOptions = {
47
+ signal?: AbortSignal;
48
+ onItemSettled?: (
49
+ result: CommandBatchItemResult,
50
+ completed: number,
51
+ total: number,
52
+ ) => MaybePromise<void>;
53
+ };
54
+
55
+ export function validateCommandBatchRequest(value: unknown): CommandBatchRequest {
56
+ const request = requireRecord(value, "command batch request");
57
+ for (const key of Object.keys(request)) {
58
+ if (key !== "items" && key !== "maxConcurrency") {
59
+ throw new Error(`command batch request.${key} is not supported`);
60
+ }
61
+ }
62
+ if (!Array.isArray(request.items)) {
63
+ throw new Error("command batch items must be an array");
64
+ }
65
+ if (request.items.length > MAX_COMMAND_BATCH_ITEMS) {
66
+ throw new Error(`command batch items must contain at most ${MAX_COMMAND_BATCH_ITEMS} entries`);
67
+ }
68
+ const ids = new Set<string>();
69
+ const items = request.items.map((item, index) => {
70
+ const normalized = validateItem(item, index);
71
+ if (ids.has(normalized.id)) {
72
+ throw new Error(`command batch item id is duplicated: ${normalized.id}`);
73
+ }
74
+ ids.add(normalized.id);
75
+ return normalized;
76
+ });
77
+ const maxConcurrency = positiveInteger(
78
+ request.maxConcurrency,
79
+ "command batch maxConcurrency",
80
+ MAX_COMMAND_BATCH_CONCURRENCY,
81
+ );
82
+ return { items, maxConcurrency };
83
+ }
84
+
85
+ export async function runCommandBatch(
86
+ input: CommandBatchRequest,
87
+ options: RunCommandBatchOptions = {},
88
+ ): Promise<CommandBatchResult> {
89
+ const request = validateCommandBatchRequest(input);
90
+ const total = request.items.length;
91
+ if (total === 0) {
92
+ return { schema: COMMAND_BATCH_RESULT_SCHEMA, items: [], completed: 0, total: 0 };
93
+ }
94
+
95
+ const results = Array.from<CommandBatchItemResult | undefined>({ length: total });
96
+ const signal = options.signal;
97
+ let nextIndex = 0;
98
+ let completed = 0;
99
+
100
+ const worker = async () => {
101
+ while (!signal?.aborted) {
102
+ const index = nextIndex;
103
+ if (index >= total) return;
104
+ nextIndex += 1;
105
+ if (signal?.aborted) return;
106
+
107
+ const item = request.items[index];
108
+ if (item === undefined) return;
109
+ const result = await runItem(item, signal);
110
+ results[index] = result;
111
+ completed += 1;
112
+ try {
113
+ await options.onItemSettled?.(result, completed, total);
114
+ } catch {
115
+ // Completion callbacks are observational and cannot change batch execution.
116
+ }
117
+ }
118
+ };
119
+
120
+ const workerCount = Math.min(request.maxConcurrency, total);
121
+ await Promise.all(Array.from({ length: workerCount }, worker));
122
+
123
+ for (let index = 0; index < total; index += 1) {
124
+ if (results[index] !== undefined) continue;
125
+ const item = request.items[index];
126
+ if (item !== undefined) results[index] = cancelledResult(item);
127
+ }
128
+
129
+ return {
130
+ schema: COMMAND_BATCH_RESULT_SCHEMA,
131
+ items: results as CommandBatchItemResult[],
132
+ completed,
133
+ total,
134
+ };
135
+ }
136
+
137
+ function validateItem(value: unknown, index: number): CommandBatchItem {
138
+ const item = requireRecord(value, `command batch items[${index}]`);
139
+ const allowed = new Set(["id", "command", "args", "cwd", "timeoutMs", "maxOutputChars"]);
140
+ for (const key of Object.keys(item)) {
141
+ if (!allowed.has(key)) throw new Error(`command batch items[${index}].${key} is not supported`);
142
+ }
143
+ const id = requireString(item.id, `command batch items[${index}].id`);
144
+ if (!ITEM_ID_PATTERN.test(id)) {
145
+ throw new Error(`command batch items[${index}].id is invalid`);
146
+ }
147
+ const command = requireString(item.command, `command batch items[${index}].command`);
148
+ if (!Array.isArray(item.args) || item.args.some((arg) => typeof arg !== "string")) {
149
+ throw new Error(`command batch items[${index}].args must be an array of strings`);
150
+ }
151
+ const cwd = requireString(item.cwd, `command batch items[${index}].cwd`);
152
+ if (!path.isAbsolute(cwd)) {
153
+ throw new Error(`command batch items[${index}].cwd must be absolute`);
154
+ }
155
+ const timeoutMs = positiveInteger(
156
+ item.timeoutMs,
157
+ `command batch items[${index}].timeoutMs`,
158
+ MAX_COMMAND_BATCH_TIMEOUT_MS,
159
+ );
160
+ const maxOutputChars = positiveInteger(
161
+ item.maxOutputChars,
162
+ `command batch items[${index}].maxOutputChars`,
163
+ MAX_COMMAND_BATCH_OUTPUT_CHARS,
164
+ );
165
+ return { id, command, args: [...item.args] as string[], cwd, timeoutMs, maxOutputChars };
166
+ }
167
+
168
+ async function runItem(
169
+ item: CommandBatchItem,
170
+ signal?: AbortSignal,
171
+ ): Promise<CommandBatchItemResult> {
172
+ try {
173
+ const result = await runShellAction(
174
+ {
175
+ command: item.command,
176
+ args: item.args,
177
+ cwd: item.cwd,
178
+ timeoutMs: item.timeoutMs,
179
+ maxOutputChars: item.maxOutputChars,
180
+ },
181
+ signal,
182
+ );
183
+ return itemResult(item.id, "succeeded", result);
184
+ } catch (error) {
185
+ const result = shellResultFromError(error) ?? emptyShellResult(item);
186
+ const outcome =
187
+ error instanceof TimeoutError
188
+ ? "timedOut"
189
+ : error instanceof CancelledError || isAbortLikeError(error)
190
+ ? "cancelled"
191
+ : "failed";
192
+ return itemResult(item.id, outcome, result, boundedError(error));
193
+ }
194
+ }
195
+
196
+ function itemResult(
197
+ id: string,
198
+ outcome: CommandBatchItemOutcome,
199
+ result: ShellActionResult,
200
+ error?: string,
201
+ ): CommandBatchItemResult {
202
+ const truncation = shellOutputTruncation(result);
203
+ return {
204
+ id,
205
+ outcome,
206
+ ...result,
207
+ stdoutTruncated: truncation.stdout,
208
+ stderrTruncated: truncation.stderr,
209
+ ...(error !== undefined ? { error } : {}),
210
+ };
211
+ }
212
+
213
+ function cancelledResult(item: CommandBatchItem): CommandBatchItemResult {
214
+ return itemResult(item.id, "cancelled", emptyShellResult(item), "Command was not started");
215
+ }
216
+
217
+ function emptyShellResult(item: CommandBatchItem): ShellActionResult {
218
+ return {
219
+ command: item.command,
220
+ args: [...item.args],
221
+ cwd: item.cwd,
222
+ stdout: "",
223
+ stderr: "",
224
+ exitCode: null,
225
+ signal: null,
226
+ durationMs: 0,
227
+ };
228
+ }
229
+
230
+ function boundedError(error: unknown): string {
231
+ const message = errorMessage(error);
232
+ return message.length <= MAX_ERROR_CHARS ? message : `${message.slice(0, MAX_ERROR_CHARS)}…`;
233
+ }
234
+
235
+ function positiveInteger(value: unknown, label: string, maximum: number): number {
236
+ if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > maximum) {
237
+ throw new Error(`${label} must be an integer from 1 through ${maximum}`);
238
+ }
239
+ return value as number;
240
+ }
241
+
242
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
243
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
244
+ throw new Error(`${label} must be an object`);
245
+ }
246
+ return value as Record<string, unknown>;
247
+ }
248
+
249
+ function requireString(value: unknown, label: string): string {
250
+ if (typeof value !== "string" || value.trim().length === 0) {
251
+ throw new Error(`${label} must be a non-empty string`);
252
+ }
253
+ return value;
254
+ }
@@ -983,9 +983,11 @@ export class WorkflowEngine {
983
983
  if (abort.signal.aborted) {
984
984
  throw abortError(abort.signal);
985
985
  }
986
- timer = setTimeout(() => {
987
- abort.abort(new TimeoutError(timeoutMs));
988
- }, timeoutMs);
986
+ if (timeoutMs !== null) {
987
+ timer = setTimeout(() => {
988
+ abort.abort(new TimeoutError(timeoutMs));
989
+ }, timeoutMs);
990
+ }
989
991
  this.activeAttempt = { runDir, state, nodeId, attemptId, signal: abort.signal };
990
992
  const dispatched = this.dispatchNode(
991
993
  workflow,
@@ -1047,8 +1049,9 @@ export class WorkflowEngine {
1047
1049
  node: WorkflowNodeDefinition,
1048
1050
  context: WorkflowNodeContext,
1049
1051
  abort: AbortController,
1050
- ): Promise<number> {
1052
+ ): Promise<number | null> {
1051
1053
  const configured = node.timeoutMs;
1054
+ if (configured === null) return null;
1052
1055
  if (typeof configured !== "function") {
1053
1056
  return assertValidTimeout(configured ?? this.defaultNodeTimeoutMs);
1054
1057
  }
@@ -1061,7 +1064,7 @@ export class WorkflowEngine {
1061
1064
  Promise.resolve(configured(context)),
1062
1065
  abortRejection(abort.signal),
1063
1066
  ]);
1064
- return assertValidTimeout(resolved);
1067
+ return resolved === null ? null : assertValidTimeout(resolved);
1065
1068
  } finally {
1066
1069
  clearTimeout(timer);
1067
1070
  }
@@ -74,6 +74,21 @@ export {
74
74
  type WorkflowSearchPaths,
75
75
  } from "./loader.js";
76
76
  export { renderShellCommand, runShellAction } from "./shell.js";
77
+ export {
78
+ COMMAND_BATCH_RESULT_SCHEMA,
79
+ MAX_COMMAND_BATCH_CONCURRENCY,
80
+ MAX_COMMAND_BATCH_ITEMS,
81
+ MAX_COMMAND_BATCH_OUTPUT_CHARS,
82
+ MAX_COMMAND_BATCH_TIMEOUT_MS,
83
+ runCommandBatch,
84
+ validateCommandBatchRequest,
85
+ type CommandBatchItem,
86
+ type CommandBatchItemOutcome,
87
+ type CommandBatchItemResult,
88
+ type CommandBatchRequest,
89
+ type CommandBatchResult,
90
+ type RunCommandBatchOptions,
91
+ } from "./command-batch.js";
77
92
  export {
78
93
  estimateProgress,
79
94
  formatProgressLine,
@@ -35,10 +35,11 @@ function assertOptionalFunction(value: unknown, description: string): void {
35
35
  function assertCommonNodeFields(node: WorkflowNodeDefinition, nodeId: string): void {
36
36
  if (
37
37
  node.timeoutMs !== undefined &&
38
+ node.timeoutMs !== null &&
38
39
  typeof node.timeoutMs !== "function" &&
39
40
  (typeof node.timeoutMs !== "number" || !Number.isFinite(node.timeoutMs) || node.timeoutMs <= 0)
40
41
  ) {
41
- fail(`node ${nodeId} timeoutMs must be a finite positive number or function`);
42
+ fail(`node ${nodeId} timeoutMs must be null, a finite positive number, or a function`);
42
43
  }
43
44
  if (node.statusDetail !== undefined && typeof node.statusDetail !== "string") {
44
45
  fail(`node ${nodeId} statusDetail must be a string`);
@@ -26,6 +26,16 @@ export function renderShellCommand(command: string, args: string[]): string {
26
26
  return renderedArgs.length > 0 ? `${command} ${renderedArgs}` : command;
27
27
  }
28
28
 
29
+ export function shellOutputTruncation(result: ShellActionResult): {
30
+ stdout: boolean;
31
+ stderr: boolean;
32
+ } {
33
+ return {
34
+ stdout: result.stdout.endsWith(TRUNCATION_MARKER),
35
+ stderr: result.stderr.endsWith(TRUNCATION_MARKER),
36
+ };
37
+ }
38
+
29
39
  function shellFailure(
30
40
  spec: ShellActionExecution,
31
41
  args: string[],
@@ -1417,7 +1417,9 @@ function snapshotNode(
1417
1417
  : exit !== undefined
1418
1418
  ? { includeTransition: "exit" as const }
1419
1419
  : {}),
1420
- ...(typeof node.timeoutMs === "number" ? { timeoutMs: node.timeoutMs } : {}),
1420
+ ...(typeof node.timeoutMs === "number" || node.timeoutMs === null
1421
+ ? { timeoutMs: node.timeoutMs }
1422
+ : {}),
1421
1423
  ...(node.statusDetail !== undefined ? { statusDetail: node.statusDetail } : {}),
1422
1424
  };
1423
1425
  if (node.nodeType === "agent" && node.expectedOutput !== undefined) {
@@ -71,9 +71,10 @@ export type WorkflowProgressData = {
71
71
  export type WorkflowNodeCommon = {
72
72
  /**
73
73
  * Per-node timeout or a callback that derives it from the run context.
74
- * Falls back to the engine default (15 minutes).
74
+ * Null disables the wall-clock deadline. Omission falls back to the engine
75
+ * default (15 minutes).
75
76
  */
76
- timeoutMs?: number | ((context: WorkflowNodeContext) => MaybePromise<number>);
77
+ timeoutMs?: number | null | ((context: WorkflowNodeContext) => MaybePromise<number | null>);
77
78
  /** Short human-readable label shown in the viewer while the node runs. */
78
79
  statusDetail?: string;
79
80
  };
@@ -715,7 +716,7 @@ export type WorkflowRunState = {
715
716
 
716
717
  export type WorkflowNodeSnapshot = {
717
718
  nodeType: WorkflowNodeDefinition["nodeType"];
718
- timeoutMs?: number;
719
+ timeoutMs?: number | null;
719
720
  statusDetail?: string;
720
721
  summary?: string;
721
722
  expectedOutput?: string;