@osolmaz/pi-workflows 0.11.2 → 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.
- package/README.md +15 -10
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +509 -124
- package/dist/builtins/autoimplement.workflow.js +884 -327
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +5 -3
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +4 -1
- package/dist/builtins/index.js +3 -1
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +2 -4
- package/dist/builtins/monitor.workflow.js +26 -128
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/pi-agent-group.d.ts +72 -0
- package/dist/builtins/pi-agent-group.js +1087 -0
- package/dist/builtins/pi-agent-group.js.map +1 -0
- package/dist/builtins/plan-approval.workflow.d.ts +39 -5
- package/dist/builtins/plan-approval.workflow.js +92 -14
- package/dist/builtins/plan-approval.workflow.js.map +1 -1
- package/dist/builtins/plan-change.workflow.d.ts +301 -0
- package/dist/builtins/plan-change.workflow.js +256 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.js +2 -2
- package/dist/builtins/plan-presentation.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +85 -0
- package/dist/builtins/sanity-check.workflow.js +482 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +90 -2
- package/dist/controllers/sqlite.js +380 -5
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -2
- package/dist/extension/decision-channels.js +29 -35
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +348 -80
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-events.d.ts +2 -2
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +23 -3
- package/dist/extension/widget.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/render/graph-render.js +1 -2
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +7 -6
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/catalog.js +7 -2
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/composition.js +8 -0
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/decision-presentation.d.ts +1 -1
- package/dist/workflows/decision-presentation.js +51 -38
- package/dist/workflows/decision-presentation.js.map +1 -1
- package/dist/workflows/engine.d.ts +2 -2
- package/dist/workflows/engine.js +22 -17
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +13 -0
- package/dist/workflows/errors.js +15 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +16 -4
- package/dist/workflows/human-decision.js +175 -72
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/progress.d.ts +1 -0
- package/dist/workflows/progress.js +15 -3
- package/dist/workflows/progress.js.map +1 -1
- package/dist/workflows/schema.js +12 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +8 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +37 -48
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +30 -40
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
- package/docs/MONITOR.md +7 -13
- package/docs/WORKFLOW_COMPOSITION.md +10 -9
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
- package/docs/run-bundles.md +8 -6
- package/docs/workflows.md +85 -22
- package/examples/workflows/approved-plan.workflow.ts +19 -46
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +7 -7
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +16 -4
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +11 -2
- package/schemas/human-decision-delivery-v1.schema.json +9 -1
- package/schemas/human-decision-receipt-v1.schema.json +9 -1
- package/schemas/human-decision-request-v1.schema.json +25 -5
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +93 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +66 -24
- package/skills/pi-workflows/SKILL.md +21 -6
- package/skills/sanity-check/SKILL.md +44 -0
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1075 -370
- package/src/builtins/catalog.ts +5 -3
- package/src/builtins/index.ts +24 -0
- package/src/builtins/monitor.workflow.ts +27 -150
- package/src/builtins/pi-agent-group.ts +1407 -0
- package/src/builtins/plan-approval.workflow.ts +157 -24
- package/src/builtins/plan-change.workflow.ts +321 -0
- package/src/builtins/plan-presentation.ts +2 -2
- package/src/builtins/sanity-check.workflow.ts +769 -0
- package/src/controllers/sqlite.ts +580 -7
- package/src/extension/decision-channels.ts +36 -66
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +438 -90
- package/src/extension/session-events.ts +2 -2
- package/src/extension/step-message.ts +1 -0
- package/src/extension/widget.ts +24 -5
- package/src/herdr/setup.ts +4 -4
- package/src/render/graph-render.ts +1 -2
- package/src/viewer/render.ts +7 -6
- package/src/workflows/catalog.ts +7 -2
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/composition.ts +9 -0
- package/src/workflows/decision-presentation.ts +56 -43
- package/src/workflows/engine.ts +25 -20
- package/src/workflows/errors.ts +24 -0
- package/src/workflows/human-decision.ts +218 -101
- package/src/workflows/index.ts +20 -11
- package/src/workflows/progress.ts +18 -3
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +8 -1
- package/src/workflows/types.ts +43 -59
- package/schemas/human-decision-accepted-v2.schema.json +0 -50
- package/schemas/human-decision-delivery-v2.schema.json +0 -36
- package/schemas/human-decision-receipt-v2.schema.json +0 -39
- package/schemas/human-decision-request-v2.schema.json +0 -69
- package/schemas/human-decision-resolution-v2.schema.json +0 -27
|
@@ -24,6 +24,8 @@ import type {
|
|
|
24
24
|
HumanDecisionSettlementRecord,
|
|
25
25
|
HumanDecisionSubmission,
|
|
26
26
|
HumanDecisionTextInput,
|
|
27
|
+
HumanDecisionTimeout,
|
|
28
|
+
ResolvedHumanDecision,
|
|
27
29
|
WorkflowNodeCommon,
|
|
28
30
|
WorkflowNodeContext,
|
|
29
31
|
} from "./types.js";
|
|
@@ -43,6 +45,14 @@ export type HumanDecisionDefinition<TChoices extends HumanDecisionChoiceMap> =
|
|
|
43
45
|
audience: HumanDecisionAudience;
|
|
44
46
|
choices: TChoices;
|
|
45
47
|
request: (context: WorkflowNodeContext) => HumanDecisionPrompt | Promise<HumanDecisionPrompt>;
|
|
48
|
+
onTimeout?:
|
|
49
|
+
| { afterMs: number; response: HumanDecisionResponseFor<TChoices> }
|
|
50
|
+
| ((
|
|
51
|
+
context: WorkflowNodeContext,
|
|
52
|
+
) =>
|
|
53
|
+
| { afterMs: number; response: HumanDecisionResponseFor<TChoices> }
|
|
54
|
+
| undefined
|
|
55
|
+
| Promise<{ afterMs: number; response: HumanDecisionResponseFor<TChoices> } | undefined>);
|
|
46
56
|
};
|
|
47
57
|
|
|
48
58
|
export type HumanDecisionResponseFor<TChoices extends HumanDecisionChoiceMap> = {
|
|
@@ -98,11 +108,25 @@ export function humanDecision<const TChoices extends HumanDecisionChoiceMap>(
|
|
|
98
108
|
if (typeof definition.request !== "function") {
|
|
99
109
|
throw new Error("Human decision request must be a function");
|
|
100
110
|
}
|
|
101
|
-
const { choices, request, ...common } = definition;
|
|
111
|
+
const { choices, request, onTimeout, ...common } = definition;
|
|
112
|
+
if (
|
|
113
|
+
onTimeout !== undefined &&
|
|
114
|
+
typeof onTimeout !== "function" &&
|
|
115
|
+
(typeof onTimeout.afterMs !== "number" ||
|
|
116
|
+
!Number.isFinite(onTimeout.afterMs) ||
|
|
117
|
+
onTimeout.afterMs <= 0)
|
|
118
|
+
) {
|
|
119
|
+
throw new Error("Human decision onTimeout afterMs must be a finite positive number");
|
|
120
|
+
}
|
|
102
121
|
return checkpoint({
|
|
103
122
|
...common,
|
|
104
123
|
summary: typeof audience === "string" ? `human decision for ${audience}` : "human decision",
|
|
105
|
-
humanDecision: {
|
|
124
|
+
humanDecision: {
|
|
125
|
+
audience,
|
|
126
|
+
choices,
|
|
127
|
+
request,
|
|
128
|
+
...(onTimeout !== undefined ? { onTimeout } : {}),
|
|
129
|
+
},
|
|
106
130
|
}) as CheckpointNodeDefinition & { readonly __humanChoices?: TChoices };
|
|
107
131
|
}
|
|
108
132
|
|
|
@@ -136,11 +160,31 @@ export function createHumanDecisionRequest(input: {
|
|
|
136
160
|
attemptId: string;
|
|
137
161
|
contract: { audience: string; choices: HumanDecisionChoiceMap };
|
|
138
162
|
prompt: HumanDecisionPrompt;
|
|
163
|
+
timeout?: HumanDecisionTimeout;
|
|
139
164
|
createdAt?: string;
|
|
140
165
|
}): HumanDecisionRequest {
|
|
141
166
|
validateChoices(input.contract.choices);
|
|
167
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
142
168
|
const title = requireString(input.prompt.title, "Human decision title");
|
|
143
|
-
|
|
169
|
+
if (input.timeout !== undefined && input.prompt.expiresAt !== undefined) {
|
|
170
|
+
throw new Error("Human decision request cannot combine expiresAt with onTimeout");
|
|
171
|
+
}
|
|
172
|
+
if (
|
|
173
|
+
input.timeout !== undefined &&
|
|
174
|
+
(typeof input.timeout.afterMs !== "number" ||
|
|
175
|
+
!Number.isFinite(input.timeout.afterMs) ||
|
|
176
|
+
input.timeout.afterMs <= 0)
|
|
177
|
+
) {
|
|
178
|
+
throw new Error("Human decision onTimeout afterMs must be a finite positive number");
|
|
179
|
+
}
|
|
180
|
+
const defaultResponse =
|
|
181
|
+
input.timeout === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: validateResponseForChoices(input.contract.choices, input.timeout.response);
|
|
184
|
+
const expiresAt =
|
|
185
|
+
input.timeout === undefined
|
|
186
|
+
? validateExpiry(input.prompt.expiresAt, createdAt)
|
|
187
|
+
: new Date(Date.parse(createdAt) + input.timeout.afterMs).toISOString();
|
|
144
188
|
const common = {
|
|
145
189
|
runId: input.runId,
|
|
146
190
|
workflowName: input.workflowName,
|
|
@@ -150,58 +194,35 @@ export function createHumanDecisionRequest(input: {
|
|
|
150
194
|
title,
|
|
151
195
|
choices: input.contract.choices,
|
|
152
196
|
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
197
|
+
...(defaultResponse !== undefined ? { defaultResponse } : {}),
|
|
153
198
|
} as const;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!Object.hasOwn(input.prompt, "subject") || !Object.hasOwn(input.prompt, "presentation")) {
|
|
163
|
-
throw new Error("Presented human decision requests require subject and presentation");
|
|
164
|
-
}
|
|
165
|
-
const prompt = input.prompt as Extract<HumanDecisionPrompt, { subject: unknown }>;
|
|
166
|
-
assertJsonValue(prompt.subject, "Human decision subject");
|
|
167
|
-
const presentation = normalizeDecisionPresentation(prompt.presentation);
|
|
168
|
-
const revision = prompt.revision ?? 1;
|
|
169
|
-
if (!Number.isInteger(revision) || revision < 1) {
|
|
170
|
-
throw new Error("Human decision revision must be a positive integer");
|
|
171
|
-
}
|
|
172
|
-
const basis = {
|
|
173
|
-
schema: "pi-workflows.human-decision-request.v2" as const,
|
|
174
|
-
...common,
|
|
175
|
-
subject: prompt.subject,
|
|
176
|
-
presentation,
|
|
177
|
-
revision,
|
|
178
|
-
};
|
|
179
|
-
const subjectDigest = digestCanonical(prompt.subject);
|
|
180
|
-
const presentationDigest = digestCanonical(presentation);
|
|
181
|
-
const requestDigest = digestCanonical(basis);
|
|
182
|
-
const decisionId = decisionIdFor(input, requestDigest);
|
|
183
|
-
return {
|
|
184
|
-
...basis,
|
|
185
|
-
decisionId,
|
|
186
|
-
requestDigest,
|
|
187
|
-
subjectDigest,
|
|
188
|
-
presentationDigest,
|
|
189
|
-
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
190
|
-
};
|
|
199
|
+
validatePresentedText(title, "Human decision title");
|
|
200
|
+
validatePresentedChoices(input.contract.choices);
|
|
201
|
+
const prompt = input.prompt;
|
|
202
|
+
assertJsonValue(prompt.subject, "Human decision subject");
|
|
203
|
+
const presentation = normalizeDecisionPresentation(prompt.presentation);
|
|
204
|
+
const revision = prompt.revision ?? 1;
|
|
205
|
+
if (!Number.isInteger(revision) || revision < 1) {
|
|
206
|
+
throw new Error("Human decision revision must be a positive integer");
|
|
191
207
|
}
|
|
192
|
-
const prompt = input.prompt as Extract<HumanDecisionPrompt, { body: unknown }>;
|
|
193
|
-
assertJsonValue(prompt.body, "Human decision body");
|
|
194
208
|
const basis = {
|
|
195
209
|
schema: "pi-workflows.human-decision-request.v1" as const,
|
|
196
210
|
...common,
|
|
197
|
-
|
|
211
|
+
subject: prompt.subject,
|
|
212
|
+
presentation,
|
|
213
|
+
revision,
|
|
198
214
|
};
|
|
199
|
-
const
|
|
215
|
+
const subjectDigest = digestCanonical(prompt.subject);
|
|
216
|
+
const presentationDigest = digestCanonical(presentation);
|
|
217
|
+
const requestDigest = digestCanonical(basis);
|
|
218
|
+
const decisionId = decisionIdFor(input, requestDigest);
|
|
200
219
|
return {
|
|
201
220
|
...basis,
|
|
202
|
-
decisionId
|
|
221
|
+
decisionId,
|
|
203
222
|
requestDigest,
|
|
204
|
-
|
|
223
|
+
subjectDigest,
|
|
224
|
+
presentationDigest,
|
|
225
|
+
createdAt,
|
|
205
226
|
};
|
|
206
227
|
}
|
|
207
228
|
|
|
@@ -220,13 +241,20 @@ function decisionIdFor(
|
|
|
220
241
|
export function validateHumanDecisionResponse(
|
|
221
242
|
request: HumanDecisionRequest,
|
|
222
243
|
value: unknown,
|
|
244
|
+
): HumanDecisionResponse {
|
|
245
|
+
return validateResponseForChoices(request.choices, value);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function validateResponseForChoices(
|
|
249
|
+
choices: HumanDecisionChoiceMap,
|
|
250
|
+
value: unknown,
|
|
223
251
|
): HumanDecisionResponse {
|
|
224
252
|
const response = requireRecord(value, "Human decision response");
|
|
225
253
|
const selected = response.choice;
|
|
226
|
-
if (typeof selected !== "string" || !Object.hasOwn(
|
|
254
|
+
if (typeof selected !== "string" || !Object.hasOwn(choices, selected)) {
|
|
227
255
|
throw new Error(`Human decision choice ${JSON.stringify(selected)} is not available`);
|
|
228
256
|
}
|
|
229
|
-
const selectedChoice =
|
|
257
|
+
const selectedChoice = choices[selected];
|
|
230
258
|
if (!selectedChoice) throw new Error("Human decision choice contract is missing");
|
|
231
259
|
if (selectedChoice.input === undefined) {
|
|
232
260
|
if (response.input !== undefined) {
|
|
@@ -288,34 +316,65 @@ export function humanDecisionStateRoot(runsRoot: string): string {
|
|
|
288
316
|
}
|
|
289
317
|
|
|
290
318
|
export type HumanDecisionAcceptance =
|
|
291
|
-
| { status: "accepted" | "adopted"; decision:
|
|
292
|
-
| { status: "conflict"; decision:
|
|
319
|
+
| { status: "accepted" | "adopted"; decision: ResolvedHumanDecision }
|
|
320
|
+
| { status: "conflict"; decision: ResolvedHumanDecision };
|
|
293
321
|
|
|
294
322
|
type HumanDecisionResolution =
|
|
295
323
|
| {
|
|
296
324
|
schema: "pi-workflows.human-decision-resolution.v1";
|
|
297
325
|
outcome: "accepted";
|
|
298
|
-
decision:
|
|
299
|
-
AcceptedHumanDecision,
|
|
300
|
-
{ schema: "pi-workflows.human-decision-accepted.v1" }
|
|
301
|
-
>;
|
|
326
|
+
decision: ResolvedHumanDecision;
|
|
302
327
|
}
|
|
303
328
|
| {
|
|
304
|
-
schema: "pi-workflows.human-decision-resolution.
|
|
305
|
-
outcome: "accepted";
|
|
306
|
-
decision: Extract<
|
|
307
|
-
AcceptedHumanDecision,
|
|
308
|
-
{ schema: "pi-workflows.human-decision-accepted.v2" }
|
|
309
|
-
>;
|
|
310
|
-
}
|
|
311
|
-
| {
|
|
312
|
-
schema:
|
|
313
|
-
| "pi-workflows.human-decision-resolution.v1"
|
|
314
|
-
| "pi-workflows.human-decision-resolution.v2";
|
|
329
|
+
schema: "pi-workflows.human-decision-resolution.v1";
|
|
315
330
|
outcome: "cancelled";
|
|
316
331
|
cancellation: HumanDecisionCancellationRecord;
|
|
317
332
|
};
|
|
318
333
|
|
|
334
|
+
const INCOMPATIBLE_HUMAN_DECISION_STATE =
|
|
335
|
+
"Human decision state uses an incompatible alpha contract; reset the affected workflow run and decision state.";
|
|
336
|
+
|
|
337
|
+
function requireCurrentDecision(value: unknown): ResolvedHumanDecision {
|
|
338
|
+
const decision = requireRecord(value, "Resolved human decision");
|
|
339
|
+
if (
|
|
340
|
+
decision.schema !== "pi-workflows.human-decision-accepted.v1" ||
|
|
341
|
+
!Object.hasOwn(decision, "subjectDigest") ||
|
|
342
|
+
!Object.hasOwn(decision, "presentationDigest") ||
|
|
343
|
+
!Object.hasOwn(decision, "revision")
|
|
344
|
+
) {
|
|
345
|
+
throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
|
|
346
|
+
}
|
|
347
|
+
return value as ResolvedHumanDecision;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function requireCurrentResolution(value: unknown): HumanDecisionResolution | null {
|
|
351
|
+
if (value === null) return null;
|
|
352
|
+
const resolution = requireRecord(value, "Human decision resolution");
|
|
353
|
+
if (resolution.schema !== "pi-workflows.human-decision-resolution.v1") {
|
|
354
|
+
throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
|
|
355
|
+
}
|
|
356
|
+
if (resolution.outcome === "accepted") {
|
|
357
|
+
return {
|
|
358
|
+
...resolution,
|
|
359
|
+
decision: requireCurrentDecision(resolution.decision),
|
|
360
|
+
} as HumanDecisionResolution;
|
|
361
|
+
}
|
|
362
|
+
if (resolution.outcome === "cancelled") return value as HumanDecisionResolution;
|
|
363
|
+
throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function requireCurrentDelivery(value: unknown): HumanDecisionDeliveryRecord {
|
|
367
|
+
const delivery = requireRecord(value, "Human decision delivery");
|
|
368
|
+
if (
|
|
369
|
+
delivery.schema !== "pi-workflows.human-decision-delivery.v1" ||
|
|
370
|
+
!Object.hasOwn(delivery, "presentationDigest") ||
|
|
371
|
+
!Object.hasOwn(delivery, "phase")
|
|
372
|
+
) {
|
|
373
|
+
throw new Error(INCOMPATIBLE_HUMAN_DECISION_STATE);
|
|
374
|
+
}
|
|
375
|
+
return value as HumanDecisionDeliveryRecord;
|
|
376
|
+
}
|
|
377
|
+
|
|
319
378
|
export class HumanDecisionStore {
|
|
320
379
|
readonly root: string;
|
|
321
380
|
|
|
@@ -412,36 +471,35 @@ export class HumanDecisionStore {
|
|
|
412
471
|
decisionId: request.decisionId,
|
|
413
472
|
requestDigest: request.requestDigest,
|
|
414
473
|
response,
|
|
474
|
+
provenance: "human" as const,
|
|
415
475
|
source: normalized.source,
|
|
416
476
|
idempotencyKey: normalized.idempotencyKey,
|
|
417
477
|
acceptedAt: attemptedAt,
|
|
418
478
|
answerDigest: digest({ response, source: normalized.source }),
|
|
419
479
|
};
|
|
420
|
-
const decision: AcceptedHumanDecision =
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
revision: request.revision,
|
|
428
|
-
}
|
|
429
|
-
: { schema: "pi-workflows.human-decision-accepted.v1", ...commonDecision };
|
|
480
|
+
const decision: AcceptedHumanDecision = {
|
|
481
|
+
schema: "pi-workflows.human-decision-accepted.v1",
|
|
482
|
+
...commonDecision,
|
|
483
|
+
subjectDigest: request.subjectDigest,
|
|
484
|
+
presentationDigest: request.presentationDigest,
|
|
485
|
+
revision: request.revision,
|
|
486
|
+
};
|
|
430
487
|
const resolutionPath = path.join(this.decisionDir(request.decisionId), "resolution.json");
|
|
431
|
-
const resolution = {
|
|
432
|
-
schema:
|
|
433
|
-
|
|
434
|
-
? ("pi-workflows.human-decision-resolution.v2" as const)
|
|
435
|
-
: ("pi-workflows.human-decision-resolution.v1" as const),
|
|
436
|
-
outcome: "accepted" as const,
|
|
488
|
+
const resolution: HumanDecisionResolution = {
|
|
489
|
+
schema: "pi-workflows.human-decision-resolution.v1",
|
|
490
|
+
outcome: "accepted",
|
|
437
491
|
decision,
|
|
438
|
-
}
|
|
492
|
+
};
|
|
439
493
|
const result = await writeImmutableJson(resolutionPath, resolution, false);
|
|
440
494
|
const winner =
|
|
441
495
|
result === "created"
|
|
442
496
|
? resolution
|
|
443
497
|
: ((await readJson(resolutionPath)) as HumanDecisionResolution | null);
|
|
444
498
|
if (winner === null) throw new Error("Human decision resolution became unreadable");
|
|
499
|
+
const winningCancellation = await this.readCancellation(request.decisionId);
|
|
500
|
+
if (winningCancellation !== null) {
|
|
501
|
+
throw new Error(`Human decision request was ${winningCancellation.reason}`);
|
|
502
|
+
}
|
|
445
503
|
if (winner.outcome === "cancelled") {
|
|
446
504
|
throw new Error(`Human decision request was ${winner.cancellation.reason}`);
|
|
447
505
|
}
|
|
@@ -455,6 +513,7 @@ export class HumanDecisionStore {
|
|
|
455
513
|
return { status: "adopted", decision: existing };
|
|
456
514
|
}
|
|
457
515
|
if (
|
|
516
|
+
existing.provenance === "human" &&
|
|
458
517
|
existing.idempotencyKey === decision.idempotencyKey &&
|
|
459
518
|
canonicalJson(existing.response) === canonicalJson(decision.response) &&
|
|
460
519
|
canonicalJson(existing.source) === canonicalJson(decision.source)
|
|
@@ -464,13 +523,74 @@ export class HumanDecisionStore {
|
|
|
464
523
|
return { status: "conflict", decision: existing };
|
|
465
524
|
}
|
|
466
525
|
|
|
526
|
+
async resolveTimeout(
|
|
527
|
+
request: HumanDecisionRequest,
|
|
528
|
+
now = new Date(),
|
|
529
|
+
): Promise<HumanDecisionAcceptance> {
|
|
530
|
+
validateHumanDecisionRequestIntegrity(request);
|
|
531
|
+
if (request.expiresAt === undefined || request.defaultResponse === undefined) {
|
|
532
|
+
throw new Error("Human decision request has no timeout default");
|
|
533
|
+
}
|
|
534
|
+
if (Date.parse(request.expiresAt) > now.getTime()) {
|
|
535
|
+
throw new Error("Human decision timeout default is not eligible yet");
|
|
536
|
+
}
|
|
537
|
+
const response = validateHumanDecisionResponse(request, request.defaultResponse);
|
|
538
|
+
const commonDecision = {
|
|
539
|
+
decisionId: request.decisionId,
|
|
540
|
+
requestDigest: request.requestDigest,
|
|
541
|
+
response,
|
|
542
|
+
provenance: "timeout" as const,
|
|
543
|
+
acceptedAt: now.toISOString(),
|
|
544
|
+
answerDigest: digest({
|
|
545
|
+
provenance: "timeout",
|
|
546
|
+
requestDigest: request.requestDigest,
|
|
547
|
+
response,
|
|
548
|
+
}),
|
|
549
|
+
};
|
|
550
|
+
const decision: ResolvedHumanDecision = {
|
|
551
|
+
schema: "pi-workflows.human-decision-accepted.v1",
|
|
552
|
+
...commonDecision,
|
|
553
|
+
subjectDigest: request.subjectDigest,
|
|
554
|
+
presentationDigest: request.presentationDigest,
|
|
555
|
+
revision: request.revision,
|
|
556
|
+
};
|
|
557
|
+
const resolutionPath = path.join(this.decisionDir(request.decisionId), "resolution.json");
|
|
558
|
+
const resolution: HumanDecisionResolution = {
|
|
559
|
+
schema: "pi-workflows.human-decision-resolution.v1",
|
|
560
|
+
outcome: "accepted",
|
|
561
|
+
decision,
|
|
562
|
+
};
|
|
563
|
+
const result = await writeImmutableJson(resolutionPath, resolution, false);
|
|
564
|
+
const winner =
|
|
565
|
+
result === "created"
|
|
566
|
+
? resolution
|
|
567
|
+
: ((await readJson(resolutionPath)) as HumanDecisionResolution | null);
|
|
568
|
+
if (winner === null) throw new Error("Human decision resolution became unreadable");
|
|
569
|
+
const winningCancellation = await this.readCancellation(request.decisionId);
|
|
570
|
+
if (winningCancellation !== null) {
|
|
571
|
+
throw new Error(`Human decision request was ${winningCancellation.reason}`);
|
|
572
|
+
}
|
|
573
|
+
if (winner.outcome === "cancelled") {
|
|
574
|
+
throw new Error(`Human decision request was ${winner.cancellation.reason}`);
|
|
575
|
+
}
|
|
576
|
+
const existing = winner.decision;
|
|
577
|
+
await writeImmutableJson(
|
|
578
|
+
path.join(this.decisionDir(request.decisionId), "accepted.json"),
|
|
579
|
+
existing,
|
|
580
|
+
);
|
|
581
|
+
if (result === "created") return { status: "accepted", decision: existing };
|
|
582
|
+
return canonicalJson(existing) === canonicalJson(decision)
|
|
583
|
+
? { status: "adopted", decision: existing }
|
|
584
|
+
: { status: "conflict", decision: existing };
|
|
585
|
+
}
|
|
586
|
+
|
|
467
587
|
async listDeliveries(
|
|
468
588
|
decisionId: string,
|
|
469
589
|
channel: string,
|
|
470
590
|
): Promise<HumanDecisionDeliveryRecord[]> {
|
|
471
591
|
const safeChannel = requireSimpleId(channel, "Human decision channel");
|
|
472
592
|
const directory = path.join(this.decisionDir(decisionId), "deliveries", safeChannel);
|
|
473
|
-
return (await readJsonDirectory(directory))
|
|
593
|
+
return (await readJsonDirectory(directory)).map(requireCurrentDelivery);
|
|
474
594
|
}
|
|
475
595
|
|
|
476
596
|
async recordSettlement(
|
|
@@ -505,8 +625,8 @@ export class HumanDecisionStore {
|
|
|
505
625
|
reason: HumanDecisionCancellationRecord["reason"],
|
|
506
626
|
): Promise<"created" | "adopted"> {
|
|
507
627
|
validateHumanDecisionRequestIntegrity(request);
|
|
508
|
-
if ((await this.
|
|
509
|
-
throw new Error("
|
|
628
|
+
if ((await this.readResolved(request.decisionId)) !== null) {
|
|
629
|
+
throw new Error("Resolved human decision cannot be cancelled");
|
|
510
630
|
}
|
|
511
631
|
const filePath = path.join(this.decisionDir(request.decisionId), "cancelled.json");
|
|
512
632
|
const record: HumanDecisionCancellationRecord = {
|
|
@@ -518,10 +638,7 @@ export class HumanDecisionStore {
|
|
|
518
638
|
};
|
|
519
639
|
const resolutionPath = path.join(this.decisionDir(request.decisionId), "resolution.json");
|
|
520
640
|
const resolution: HumanDecisionResolution = {
|
|
521
|
-
schema:
|
|
522
|
-
request.schema === "pi-workflows.human-decision-request.v2"
|
|
523
|
-
? "pi-workflows.human-decision-resolution.v2"
|
|
524
|
-
: "pi-workflows.human-decision-resolution.v1",
|
|
641
|
+
schema: "pi-workflows.human-decision-resolution.v1",
|
|
525
642
|
outcome: "cancelled",
|
|
526
643
|
cancellation: record,
|
|
527
644
|
};
|
|
@@ -532,7 +649,7 @@ export class HumanDecisionStore {
|
|
|
532
649
|
: ((await readJson(resolutionPath)) as HumanDecisionResolution | null);
|
|
533
650
|
if (winner === null) throw new Error("Human decision resolution became unreadable");
|
|
534
651
|
if (winner.outcome === "accepted") {
|
|
535
|
-
throw new Error("
|
|
652
|
+
throw new Error("Resolved human decision cannot be cancelled");
|
|
536
653
|
}
|
|
537
654
|
const existing = winner.cancellation;
|
|
538
655
|
if (
|
|
@@ -551,9 +668,9 @@ export class HumanDecisionStore {
|
|
|
551
668
|
path.join(this.decisionDir(decisionId), "cancelled.json"),
|
|
552
669
|
)) as HumanDecisionCancellationRecord | null;
|
|
553
670
|
if (stored !== null) return stored;
|
|
554
|
-
const resolution = (
|
|
555
|
-
path.join(this.decisionDir(decisionId), "resolution.json"),
|
|
556
|
-
)
|
|
671
|
+
const resolution = requireCurrentResolution(
|
|
672
|
+
await readJson(path.join(this.decisionDir(decisionId), "resolution.json")),
|
|
673
|
+
);
|
|
557
674
|
if (resolution?.outcome !== "cancelled") return null;
|
|
558
675
|
await writeImmutableJson(
|
|
559
676
|
path.join(this.decisionDir(decisionId), "cancelled.json"),
|
|
@@ -562,14 +679,14 @@ export class HumanDecisionStore {
|
|
|
562
679
|
return resolution.cancellation;
|
|
563
680
|
}
|
|
564
681
|
|
|
565
|
-
async
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (stored !== null) return stored;
|
|
570
|
-
const resolution = (
|
|
571
|
-
path.join(this.decisionDir(decisionId), "resolution.json"),
|
|
572
|
-
)
|
|
682
|
+
async readResolved(decisionId: string): Promise<ResolvedHumanDecision | null> {
|
|
683
|
+
const cancellation = await readJson(path.join(this.decisionDir(decisionId), "cancelled.json"));
|
|
684
|
+
if (cancellation !== null) return null;
|
|
685
|
+
const stored = await readJson(path.join(this.decisionDir(decisionId), "accepted.json"));
|
|
686
|
+
if (stored !== null) return requireCurrentDecision(stored);
|
|
687
|
+
const resolution = requireCurrentResolution(
|
|
688
|
+
await readJson(path.join(this.decisionDir(decisionId), "resolution.json")),
|
|
689
|
+
);
|
|
573
690
|
if (resolution?.outcome !== "accepted") return null;
|
|
574
691
|
await writeImmutableJson(
|
|
575
692
|
path.join(this.decisionDir(decisionId), "accepted.json"),
|
package/src/workflows/index.ts
CHANGED
|
@@ -23,8 +23,8 @@ export {
|
|
|
23
23
|
decisionPresentationFingerprint,
|
|
24
24
|
digestCanonical,
|
|
25
25
|
humanDecisionChannelRequest,
|
|
26
|
-
legacyDecisionPresentation,
|
|
27
26
|
normalizeDecisionPresentation,
|
|
27
|
+
valueDecisionPresentation,
|
|
28
28
|
validateHumanDecisionRequestIntegrity,
|
|
29
29
|
type DecisionDocumentSegment,
|
|
30
30
|
} from "./decision-presentation.js";
|
|
@@ -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,
|
|
@@ -122,8 +137,8 @@ export {
|
|
|
122
137
|
} from "./store.js";
|
|
123
138
|
export type {
|
|
124
139
|
AcceptedHumanDecision,
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
DefaultedHumanDecision,
|
|
141
|
+
ResolvedHumanDecision,
|
|
127
142
|
AgentNodeDefinition,
|
|
128
143
|
AgentStepContract,
|
|
129
144
|
AgentStepExecutor,
|
|
@@ -152,24 +167,18 @@ export type {
|
|
|
152
167
|
HumanDecisionChoiceMap,
|
|
153
168
|
HumanDecisionContinuationRecord,
|
|
154
169
|
HumanDecisionDeliveryRecord,
|
|
155
|
-
HumanDecisionDeliveryRecordV1,
|
|
156
|
-
HumanDecisionDeliveryRecordV2,
|
|
157
170
|
HumanDecisionNodeContract,
|
|
158
171
|
HumanDecisionPrompt,
|
|
159
172
|
HumanDecisionReceipt,
|
|
160
|
-
HumanDecisionReceiptV1,
|
|
161
|
-
HumanDecisionReceiptV2,
|
|
162
173
|
HumanDecisionRequest,
|
|
163
|
-
HumanDecisionRequestV1,
|
|
164
|
-
HumanDecisionRequestV2,
|
|
165
174
|
HumanDecisionResponse,
|
|
166
175
|
HumanDecisionSettlementRecord,
|
|
167
176
|
HumanDecisionSubmission,
|
|
168
177
|
HumanDecisionTextInput,
|
|
169
|
-
|
|
178
|
+
HumanDecisionTimeout,
|
|
179
|
+
HumanDecisionTimeoutPolicy,
|
|
170
180
|
MaybePromise,
|
|
171
181
|
NotifyNodeDefinition,
|
|
172
|
-
PresentedHumanDecisionPrompt,
|
|
173
182
|
ShellActionExecution,
|
|
174
183
|
ShellActionNodeDefinition,
|
|
175
184
|
ShellActionResult,
|
|
@@ -18,6 +18,7 @@ export type ProgressEstimate = {
|
|
|
18
18
|
confidence?: ProgressConfidence;
|
|
19
19
|
sourceEstimatedFinishAt?: string;
|
|
20
20
|
unavailableReason?: string;
|
|
21
|
+
elapsedMs?: number;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export type ProgressSample = {
|
|
@@ -42,7 +43,16 @@ export function estimateProgress(
|
|
|
42
43
|
for (const sample of samples) validateProgressData(sample.data as Record<string, unknown>);
|
|
43
44
|
const latest = samples.at(-1) as ProgressSample;
|
|
44
45
|
const data = latest.data;
|
|
45
|
-
const
|
|
46
|
+
const firstAt = Date.parse(samples[0]!.at);
|
|
47
|
+
const endAt = TERMINAL.has(data.status) ? Date.parse(latest.at) : now.getTime();
|
|
48
|
+
const elapsedMs =
|
|
49
|
+
Number.isFinite(firstAt) && Number.isFinite(endAt) ? Math.max(0, endAt - firstAt) : undefined;
|
|
50
|
+
const base: ProgressEstimate = {
|
|
51
|
+
key,
|
|
52
|
+
data,
|
|
53
|
+
sampleCount: 1,
|
|
54
|
+
...(elapsedMs !== undefined ? { elapsedMs } : {}),
|
|
55
|
+
};
|
|
46
56
|
if (TERMINAL.has(data.status)) return base;
|
|
47
57
|
const sourceFinish = validSourceFinish(latest, now);
|
|
48
58
|
if (sourceFinish !== undefined) base.sourceEstimatedFinishAt = sourceFinish;
|
|
@@ -212,10 +222,15 @@ export function formatProgressLine(estimate: ProgressEstimate, now = new Date())
|
|
|
212
222
|
? `${formatRemaining(estimate.remainingLowMs)}–${formatRemaining(estimate.remainingHighMs)}`
|
|
213
223
|
: formatRemaining(estimate.remainingMedianMs);
|
|
214
224
|
eta = `ETA ${range}`;
|
|
215
|
-
} else if (!TERMINAL.has(data.status)) {
|
|
225
|
+
} else if (!TERMINAL.has(data.status) && data.phase === undefined) {
|
|
216
226
|
eta = `ETA unavailable${estimate.unavailableReason ? ` (${estimate.unavailableReason})` : ""}`;
|
|
217
227
|
}
|
|
218
|
-
|
|
228
|
+
const phase = data.phase === undefined ? "" : sanitizeText(data.phase);
|
|
229
|
+
const elapsed =
|
|
230
|
+
data.phase === undefined || estimate.elapsedMs === undefined
|
|
231
|
+
? ""
|
|
232
|
+
: `elapsed ${formatRemaining(estimate.elapsedMs)}`;
|
|
233
|
+
return [label, count, phase, elapsed, eta].filter(Boolean).join(" ");
|
|
219
234
|
}
|
|
220
235
|
|
|
221
236
|
export function formatProgressReport(
|
package/src/workflows/schema.ts
CHANGED
|
@@ -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`);
|
|
@@ -150,6 +151,23 @@ export function assertValidCheckpointNode(
|
|
|
150
151
|
if (typeof node.humanDecision.request !== "function") {
|
|
151
152
|
fail(`node ${nodeId} humanDecision request must be a function`);
|
|
152
153
|
}
|
|
154
|
+
if (
|
|
155
|
+
node.humanDecision.onTimeout !== undefined &&
|
|
156
|
+
typeof node.humanDecision.onTimeout !== "function"
|
|
157
|
+
) {
|
|
158
|
+
assertRecord(node.humanDecision.onTimeout, `node ${nodeId} humanDecision onTimeout`);
|
|
159
|
+
if (
|
|
160
|
+
typeof node.humanDecision.onTimeout.afterMs !== "number" ||
|
|
161
|
+
!Number.isFinite(node.humanDecision.onTimeout.afterMs) ||
|
|
162
|
+
node.humanDecision.onTimeout.afterMs <= 0
|
|
163
|
+
) {
|
|
164
|
+
fail(`node ${nodeId} humanDecision onTimeout afterMs must be a finite positive number`);
|
|
165
|
+
}
|
|
166
|
+
assertRecord(
|
|
167
|
+
node.humanDecision.onTimeout.response,
|
|
168
|
+
`node ${nodeId} humanDecision onTimeout response`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
153
171
|
}
|
|
154
172
|
assertCommonNodeFields(node, nodeId);
|
|
155
173
|
}
|
package/src/workflows/shell.ts
CHANGED
|
@@ -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[],
|
package/src/workflows/store.ts
CHANGED
|
@@ -1417,7 +1417,9 @@ function snapshotNode(
|
|
|
1417
1417
|
: exit !== undefined
|
|
1418
1418
|
? { includeTransition: "exit" as const }
|
|
1419
1419
|
: {}),
|
|
1420
|
-
...(typeof node.timeoutMs === "number"
|
|
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) {
|
|
@@ -1435,6 +1437,11 @@ function snapshotNode(
|
|
|
1435
1437
|
typeof node.humanDecision.audience === "string" ? node.humanDecision.audience : "<dynamic>",
|
|
1436
1438
|
...(typeof node.humanDecision.audience === "function" ? { dynamicAudience: true } : {}),
|
|
1437
1439
|
choices: structuredClone(node.humanDecision.choices),
|
|
1440
|
+
...(node.humanDecision.onTimeout !== undefined &&
|
|
1441
|
+
typeof node.humanDecision.onTimeout !== "function"
|
|
1442
|
+
? { onTimeout: structuredClone(node.humanDecision.onTimeout) }
|
|
1443
|
+
: {}),
|
|
1444
|
+
...(typeof node.humanDecision.onTimeout === "function" ? { dynamicTimeout: true } : {}),
|
|
1438
1445
|
};
|
|
1439
1446
|
}
|
|
1440
1447
|
if (node.nodeType === "action") {
|