@osolmaz/pi-workflows 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -19
- package/dist/builtins/autodoc.workflow.d.ts +58 -0
- package/dist/builtins/autodoc.workflow.js +266 -0
- package/dist/builtins/autodoc.workflow.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +353 -0
- package/dist/builtins/autoimplement.workflow.js +961 -0
- package/dist/builtins/autoimplement.workflow.js.map +1 -0
- package/dist/builtins/autoplan.workflow.d.ts +58 -0
- package/dist/builtins/autoplan.workflow.js +190 -0
- package/dist/builtins/autoplan.workflow.js.map +1 -0
- package/dist/builtins/catalog.js +9 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +6 -0
- package/dist/builtins/index.js +7 -0
- package/dist/builtins/index.js.map +1 -0
- package/dist/builtins/monitor.workflow.d.ts +29 -3
- package/dist/builtins/monitor.workflow.js +325 -13
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plan-approval.workflow.d.ts +83 -0
- package/dist/builtins/plan-approval.workflow.js +148 -0
- package/dist/builtins/plan-approval.workflow.js.map +1 -0
- package/dist/builtins/plan-presentation.d.ts +7 -0
- package/dist/builtins/plan-presentation.js +44 -0
- package/dist/builtins/plan-presentation.js.map +1 -0
- package/dist/extension/decision-channels.d.ts +132 -0
- package/dist/extension/decision-channels.js +1082 -0
- package/dist/extension/decision-channels.js.map +1 -0
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +426 -11
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/widget.js +26 -2
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +1 -37
- package/dist/extension/workflow-tool.js +1 -42
- package/dist/extension/workflow-tool.js.map +1 -1
- package/dist/host/rpc-bridge.js +4 -21
- package/dist/host/rpc-bridge.js.map +1 -1
- package/dist/render/graph-render.js +46 -10
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/render.js +36 -2
- package/dist/viewer/render.js.map +1 -1
- package/dist/workflows/catalog.d.ts +1 -0
- package/dist/workflows/catalog.js +6 -0
- package/dist/workflows/catalog.js.map +1 -1
- package/dist/workflows/composition.d.ts +45 -0
- package/dist/workflows/composition.js +485 -0
- package/dist/workflows/composition.js.map +1 -0
- package/dist/workflows/decision-presentation.d.ts +18 -0
- package/dist/workflows/decision-presentation.js +417 -0
- package/dist/workflows/decision-presentation.js.map +1 -0
- package/dist/workflows/decision.d.ts +11 -5
- package/dist/workflows/decision.js.map +1 -1
- package/dist/workflows/definition.d.ts +22 -3
- package/dist/workflows/definition.js +46 -3
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +2 -1
- package/dist/workflows/engine.js +199 -18
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/graph.js +8 -6
- package/dist/workflows/graph.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +87 -0
- package/dist/workflows/human-decision.js +583 -0
- package/dist/workflows/human-decision.js.map +1 -0
- package/dist/workflows/index.d.ts +5 -2
- package/dist/workflows/index.js +4 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +5 -4
- package/dist/workflows/loader.js +118 -18
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.d.ts +3 -1
- package/dist/workflows/schema.js +64 -2
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.js +39 -2
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +72 -0
- package/dist/workflows/tool-input.js +141 -0
- package/dist/workflows/tool-input.js.map +1 -0
- package/dist/workflows/types.d.ts +300 -2
- package/docs/CONTROLLERS.md +1 -1
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +371 -0
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
- package/docs/MONITOR.md +46 -18
- package/docs/WORKFLOW_COMPOSITION.md +333 -0
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +296 -0
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +172 -0
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +45 -0
- package/docs/plans/2026-08-19-workflow-composition-plan.md +308 -0
- package/docs/run-bundles.md +45 -10
- package/docs/workflows.md +106 -15
- package/examples/workflows/approved-plan.workflow.ts +58 -0
- package/examples/workflows/autoimplement.workflow.ts +1 -92
- package/examples/workflows/autoplan.workflow.ts +1 -0
- package/examples/workflows/human-decision.workflow.ts +62 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +6 -1
- package/schemas/decision-presentation-v1.schema.json +83 -0
- package/schemas/human-decision-accepted-v1.schema.json +44 -0
- package/schemas/human-decision-accepted-v2.schema.json +50 -0
- package/schemas/human-decision-answer-attempt-v1.schema.json +37 -0
- package/schemas/human-decision-cancellation-v1.schema.json +15 -0
- package/schemas/human-decision-continuation-v1.schema.json +16 -0
- package/schemas/human-decision-delivery-v1.schema.json +28 -0
- package/schemas/human-decision-delivery-v2.schema.json +36 -0
- package/schemas/human-decision-receipt-v1.schema.json +33 -0
- package/schemas/human-decision-receipt-v2.schema.json +39 -0
- package/schemas/human-decision-request-v1.schema.json +59 -0
- package/schemas/human-decision-request-v2.schema.json +69 -0
- package/schemas/human-decision-resolution-v1.schema.json +27 -0
- package/schemas/human-decision-resolution-v2.schema.json +27 -0
- package/schemas/human-decision-settlement-v1.schema.json +28 -0
- package/skills/autodoc/SKILL.md +43 -0
- package/skills/autoimplement/SKILL.md +58 -0
- package/skills/autoimplement/agents/openai.yaml +4 -0
- package/skills/autoplan/SKILL.md +25 -0
- package/skills/monitor/SKILL.md +8 -1
- package/skills/pi-workflows/SKILL.md +5 -3
- package/src/builtins/autodoc.workflow.ts +325 -0
- package/src/builtins/autoimplement.workflow.ts +1123 -0
- package/src/builtins/autoplan.workflow.ts +231 -0
- package/src/builtins/catalog.ts +9 -1
- package/src/builtins/index.ts +32 -0
- package/src/builtins/monitor.workflow.ts +396 -15
- package/src/builtins/plan-approval.workflow.ts +190 -0
- package/src/builtins/plan-presentation.ts +57 -0
- package/src/extension/decision-channels.ts +1533 -0
- package/src/extension/index.ts +499 -9
- package/src/extension/widget.ts +39 -2
- package/src/extension/workflow-tool.ts +5 -81
- package/src/host/rpc-bridge.ts +7 -30
- package/src/render/graph-render.ts +54 -5
- package/src/viewer/render.ts +56 -3
- package/src/workflows/catalog.ts +7 -0
- package/src/workflows/composition.ts +642 -0
- package/src/workflows/decision-presentation.ts +501 -0
- package/src/workflows/decision.ts +12 -5
- package/src/workflows/definition.ts +118 -8
- package/src/workflows/engine.ts +257 -20
- package/src/workflows/graph.ts +8 -6
- package/src/workflows/human-decision.ts +819 -0
- package/src/workflows/index.ts +89 -0
- package/src/workflows/loader.ts +186 -18
- package/src/workflows/schema.ts +79 -2
- package/src/workflows/store.ts +45 -2
- package/src/workflows/tool-input.ts +202 -0
- package/src/workflows/types.ts +374 -2
- package/examples/workflows/elegant-solution.workflow.ts +0 -95
package/src/workflows/engine.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { resolveArtifacts } from "./artifacts.js";
|
|
4
|
+
import {
|
|
5
|
+
compileWorkflowDefinition,
|
|
6
|
+
compositionMetadata,
|
|
7
|
+
isCompiledWorkflow,
|
|
8
|
+
} from "./composition.js";
|
|
4
9
|
import {
|
|
5
10
|
CancelledError,
|
|
6
11
|
errorMessage,
|
|
@@ -12,10 +17,22 @@ import {
|
|
|
12
17
|
WorkflowSourceChangedError,
|
|
13
18
|
} from "./errors.js";
|
|
14
19
|
import { resolveNext, resolveNextForOutcome, validateWorkflowDefinition } from "./graph.js";
|
|
20
|
+
import {
|
|
21
|
+
HumanDecisionStore,
|
|
22
|
+
createHumanDecisionRequest,
|
|
23
|
+
validateHumanDecisionResponse,
|
|
24
|
+
} from "./human-decision.js";
|
|
15
25
|
import { extractJsonValue } from "./json.js";
|
|
16
26
|
import { runShellAction, shellResultFromError } from "./shell.js";
|
|
17
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
RUN_STATE_SCHEMA,
|
|
29
|
+
WorkflowRunStore,
|
|
30
|
+
createDefinitionSnapshot,
|
|
31
|
+
createRunId,
|
|
32
|
+
readRunBundle,
|
|
33
|
+
} from "./store.js";
|
|
18
34
|
import type {
|
|
35
|
+
AcceptedHumanDecision,
|
|
19
36
|
AgentNodeDefinition,
|
|
20
37
|
AgentStepExecutor,
|
|
21
38
|
ActionNodeDefinition,
|
|
@@ -32,6 +49,7 @@ import type {
|
|
|
32
49
|
WorkflowNodeOutcome,
|
|
33
50
|
WorkflowNodeResult,
|
|
34
51
|
WorkflowNotificationSink,
|
|
52
|
+
HumanDecisionRequest,
|
|
35
53
|
WorkflowRunResult,
|
|
36
54
|
WorkflowRunState,
|
|
37
55
|
WorkflowSource,
|
|
@@ -215,10 +233,12 @@ export class WorkflowEngine {
|
|
|
215
233
|
input: unknown,
|
|
216
234
|
options: { workflowSource?: WorkflowSource; runId?: string } = {},
|
|
217
235
|
): Promise<WorkflowRunResult> {
|
|
236
|
+
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
218
237
|
validateWorkflowDefinition(workflow);
|
|
219
238
|
// Fail before any bundle exists so bad input cannot leave a partial run
|
|
220
239
|
// on disk or silently change shape when state.json round-trips.
|
|
221
|
-
const
|
|
240
|
+
const suppliedInput = input === undefined ? null : input;
|
|
241
|
+
const normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
222
242
|
assertJsonSerializable(normalizedInput, "Workflow run input");
|
|
223
243
|
if (options.runId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(options.runId)) {
|
|
224
244
|
throw new Error(`Invalid workflow run id: ${JSON.stringify(options.runId)}`);
|
|
@@ -270,6 +290,7 @@ export class WorkflowEngine {
|
|
|
270
290
|
runId: string,
|
|
271
291
|
options: { workflowSource?: WorkflowSource; force?: boolean } = {},
|
|
272
292
|
): Promise<WorkflowRunResult> {
|
|
293
|
+
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
273
294
|
validateWorkflowDefinition(workflow);
|
|
274
295
|
// Reset before any await: a park or cancel landing during preparation
|
|
275
296
|
// must survive, or a host drain would hang while the run executes.
|
|
@@ -279,7 +300,7 @@ export class WorkflowEngine {
|
|
|
279
300
|
const bundle = await this.store.prepareRunResume(runId);
|
|
280
301
|
const { runDir } = bundle;
|
|
281
302
|
const state = bundle.state;
|
|
282
|
-
const sourceMismatch =
|
|
303
|
+
const sourceMismatch = workflowIdentityMismatch(state, workflow, options.workflowSource);
|
|
283
304
|
if (sourceMismatch && options.force !== true) {
|
|
284
305
|
throw new WorkflowSourceChangedError(runId);
|
|
285
306
|
}
|
|
@@ -330,7 +351,7 @@ export class WorkflowEngine {
|
|
|
330
351
|
state,
|
|
331
352
|
runDir,
|
|
332
353
|
point.nodeId,
|
|
333
|
-
state.steps
|
|
354
|
+
countExecutableSteps(workflow, state.steps),
|
|
334
355
|
point.lastOutput,
|
|
335
356
|
);
|
|
336
357
|
} catch (error) {
|
|
@@ -352,8 +373,14 @@ export class WorkflowEngine {
|
|
|
352
373
|
workflow: WorkflowDefinition,
|
|
353
374
|
parentRunId: string,
|
|
354
375
|
input: unknown,
|
|
355
|
-
options: {
|
|
376
|
+
options: {
|
|
377
|
+
workflowSource?: WorkflowSource;
|
|
378
|
+
runId?: string;
|
|
379
|
+
force?: boolean;
|
|
380
|
+
humanDecision?: AcceptedHumanDecision;
|
|
381
|
+
} = {},
|
|
356
382
|
): Promise<WorkflowRunResult> {
|
|
383
|
+
workflow = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
357
384
|
validateWorkflowDefinition(workflow);
|
|
358
385
|
this.cancelled = false;
|
|
359
386
|
this.paused = false;
|
|
@@ -367,12 +394,44 @@ export class WorkflowEngine {
|
|
|
367
394
|
`Cannot continue workflow run ${parentRunId} with status ${parent.state.status}`,
|
|
368
395
|
);
|
|
369
396
|
}
|
|
370
|
-
const sourceMismatch =
|
|
397
|
+
const sourceMismatch = workflowIdentityMismatch(parent.state, workflow, options.workflowSource);
|
|
371
398
|
if (sourceMismatch && options.force !== true) {
|
|
372
399
|
throw new WorkflowSourceChangedError(parentRunId);
|
|
373
400
|
}
|
|
374
401
|
|
|
375
|
-
const
|
|
402
|
+
const waitingNodeId = parent.state.waitingOn;
|
|
403
|
+
const waitingNode = workflow.nodes[waitingNodeId];
|
|
404
|
+
const humanContract =
|
|
405
|
+
waitingNode?.nodeType === "checkpoint" ? waitingNode.humanDecision : undefined;
|
|
406
|
+
let acceptedResponse: unknown;
|
|
407
|
+
let acceptedNodeId: string | undefined;
|
|
408
|
+
let normalizedInput: unknown;
|
|
409
|
+
if (humanContract !== undefined) {
|
|
410
|
+
if (options.humanDecision === undefined) {
|
|
411
|
+
throw new Error(`Checkpoint ${waitingNodeId} requires an accepted verified human decision`);
|
|
412
|
+
}
|
|
413
|
+
const request = parent.state.finalOutput as HumanDecisionRequest;
|
|
414
|
+
if (
|
|
415
|
+
(request?.schema !== "pi-workflows.human-decision-request.v1" &&
|
|
416
|
+
request?.schema !== "pi-workflows.human-decision-request.v2") ||
|
|
417
|
+
request.decisionId !== options.humanDecision.decisionId ||
|
|
418
|
+
request.requestDigest !== options.humanDecision.requestDigest
|
|
419
|
+
) {
|
|
420
|
+
throw new Error("Accepted human decision does not match the waiting request");
|
|
421
|
+
}
|
|
422
|
+
const durableDecision = await new HumanDecisionStore(this.store.outputRoot).readAccepted(
|
|
423
|
+
request.decisionId,
|
|
424
|
+
);
|
|
425
|
+
if (durableDecision === null || !isDeepStrictEqual(durableDecision, options.humanDecision)) {
|
|
426
|
+
throw new Error("Accepted human decision does not match the durable decision record");
|
|
427
|
+
}
|
|
428
|
+
acceptedResponse = validateHumanDecisionResponse(request, durableDecision.response);
|
|
429
|
+
acceptedNodeId = request.nodeId;
|
|
430
|
+
normalizedInput = await resolveArtifacts(parent.state.input, parent.runDir);
|
|
431
|
+
} else {
|
|
432
|
+
const suppliedInput = input === undefined ? null : input;
|
|
433
|
+
normalizedInput = workflow.input ? await workflow.input(suppliedInput) : suppliedInput;
|
|
434
|
+
}
|
|
376
435
|
assertJsonSerializable(normalizedInput, "Workflow run input");
|
|
377
436
|
if (options.runId !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(options.runId)) {
|
|
378
437
|
throw new Error(`Invalid workflow run id: ${JSON.stringify(options.runId)}`);
|
|
@@ -399,6 +458,39 @@ export class WorkflowEngine {
|
|
|
399
458
|
parent.state.steps,
|
|
400
459
|
parent.runDir,
|
|
401
460
|
)) as WorkflowRunState["steps"];
|
|
461
|
+
if (humanContract !== undefined && options.humanDecision !== undefined) {
|
|
462
|
+
const receipt = {
|
|
463
|
+
decisionId: options.humanDecision.decisionId,
|
|
464
|
+
requestDigest: options.humanDecision.requestDigest,
|
|
465
|
+
nodeId: acceptedNodeId ?? waitingNodeId,
|
|
466
|
+
response: options.humanDecision.response,
|
|
467
|
+
acceptedAt: options.humanDecision.acceptedAt,
|
|
468
|
+
answerDigest: options.humanDecision.answerDigest,
|
|
469
|
+
};
|
|
470
|
+
state.humanDecision =
|
|
471
|
+
options.humanDecision.schema === "pi-workflows.human-decision-accepted.v2"
|
|
472
|
+
? {
|
|
473
|
+
schema: "pi-workflows.human-decision-receipt.v2",
|
|
474
|
+
...receipt,
|
|
475
|
+
subjectDigest: options.humanDecision.subjectDigest,
|
|
476
|
+
presentationDigest: options.humanDecision.presentationDigest,
|
|
477
|
+
revision: options.humanDecision.revision,
|
|
478
|
+
}
|
|
479
|
+
: { schema: "pi-workflows.human-decision-receipt.v1", ...receipt };
|
|
480
|
+
state.outputs[waitingNodeId] = acceptedResponse;
|
|
481
|
+
const priorResult = state.results[waitingNodeId];
|
|
482
|
+
if (priorResult === undefined) {
|
|
483
|
+
throw new Error(`Waiting human decision result is missing for ${waitingNodeId}`);
|
|
484
|
+
}
|
|
485
|
+
state.results[waitingNodeId] = { ...priorResult, output: acceptedResponse };
|
|
486
|
+
const stepIndex = state.steps.findLastIndex((step) => step.nodeId === waitingNodeId);
|
|
487
|
+
if (stepIndex < 0)
|
|
488
|
+
throw new Error(`Waiting human decision step is missing for ${waitingNodeId}`);
|
|
489
|
+
const priorStep = state.steps[stepIndex];
|
|
490
|
+
if (priorStep === undefined)
|
|
491
|
+
throw new Error("Waiting human decision step became unavailable");
|
|
492
|
+
state.steps[stepIndex] = { ...priorStep, output: acceptedResponse };
|
|
493
|
+
}
|
|
402
494
|
state.carriedStepCount = state.steps.length;
|
|
403
495
|
|
|
404
496
|
const runDir = await this.store.initializeRunBundle(workflow, state);
|
|
@@ -428,7 +520,7 @@ export class WorkflowEngine {
|
|
|
428
520
|
state,
|
|
429
521
|
runDir,
|
|
430
522
|
point.nodeId,
|
|
431
|
-
state.steps
|
|
523
|
+
countExecutableSteps(workflow, state.steps),
|
|
432
524
|
point.lastOutput,
|
|
433
525
|
);
|
|
434
526
|
} catch (error) {
|
|
@@ -546,6 +638,7 @@ export class WorkflowEngine {
|
|
|
546
638
|
runId: string | undefined,
|
|
547
639
|
): Promise<WorkflowRunState> {
|
|
548
640
|
const now = new Date().toISOString();
|
|
641
|
+
const composition = compositionMetadata(workflow);
|
|
549
642
|
return {
|
|
550
643
|
schema: RUN_STATE_SCHEMA,
|
|
551
644
|
traceSeq: 0,
|
|
@@ -553,6 +646,10 @@ export class WorkflowEngine {
|
|
|
553
646
|
workflowName: workflow.name,
|
|
554
647
|
...(await this.resolveTitleBounded(workflow, input)),
|
|
555
648
|
...(workflowSource !== undefined ? { workflowSource } : {}),
|
|
649
|
+
...(composition?.sources.length ? { workflowSources: composition.sources } : {}),
|
|
650
|
+
...(composition?.snapshot.mounts.length
|
|
651
|
+
? { definitionDigest: definitionDigest(workflow) }
|
|
652
|
+
: {}),
|
|
556
653
|
startedAt: now,
|
|
557
654
|
updatedAt: now,
|
|
558
655
|
status: "running",
|
|
@@ -573,17 +670,24 @@ export class WorkflowEngine {
|
|
|
573
670
|
initialLastOutput?: unknown,
|
|
574
671
|
): Promise<void> {
|
|
575
672
|
const maxSteps = workflow.maxSteps ?? this.maxSteps;
|
|
673
|
+
const composition = compositionMetadata(workflow);
|
|
576
674
|
let currentNodeId: string | null = startNodeId;
|
|
577
675
|
let executedSteps = executedStepsBase;
|
|
578
676
|
let lastOutput: unknown = initialLastOutput;
|
|
579
677
|
|
|
580
678
|
while (currentNodeId !== null) {
|
|
581
679
|
await this.holdWhilePaused(state, runDir);
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
680
|
+
const isTransition =
|
|
681
|
+
composition?.entries[currentNodeId] !== undefined ||
|
|
682
|
+
composition?.exits[currentNodeId] !== undefined;
|
|
683
|
+
if (!isTransition) {
|
|
684
|
+
executedSteps += 1;
|
|
685
|
+
if (executedSteps > maxSteps) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`Workflow exceeded maxSteps=${maxSteps}; aborting to avoid an unbounded loop`,
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
assertInvocationStepLimit(composition, currentNodeId, state.steps);
|
|
587
691
|
}
|
|
588
692
|
|
|
589
693
|
const node = workflow.nodes[currentNodeId];
|
|
@@ -597,7 +701,7 @@ export class WorkflowEngine {
|
|
|
597
701
|
// as in-flight, and resume reruns it with a fresh attempt.
|
|
598
702
|
throw new RunParkedError();
|
|
599
703
|
}
|
|
600
|
-
this.recordAttempt(state, attempt);
|
|
704
|
+
this.recordAttempt(workflow, state, attempt);
|
|
601
705
|
// The terminal node event carries the output, receipt, and conversation
|
|
602
706
|
// linkage so the trace alone is sufficient to reconstruct the run.
|
|
603
707
|
await this.persist(runDir, state, {
|
|
@@ -622,6 +726,35 @@ export class WorkflowEngine {
|
|
|
622
726
|
continue;
|
|
623
727
|
}
|
|
624
728
|
|
|
729
|
+
const entered = composition?.entries[attempt.result.nodeId];
|
|
730
|
+
if (entered !== undefined) {
|
|
731
|
+
const value = attempt.result.output as { invocation?: number } | undefined;
|
|
732
|
+
await this.persist(runDir, state, {
|
|
733
|
+
scope: "run",
|
|
734
|
+
type: "include_entered",
|
|
735
|
+
payload: {
|
|
736
|
+
mountPath: entered.mountPath.split("/"),
|
|
737
|
+
workflowName: entered.workflowName,
|
|
738
|
+
invocation: value?.invocation ?? 1,
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
const exited = composition?.exits[attempt.result.nodeId];
|
|
743
|
+
if (exited !== undefined) {
|
|
744
|
+
const entrySteps = state.steps.filter((step) => step.nodeId === exited.mountPath);
|
|
745
|
+
await this.persist(runDir, state, {
|
|
746
|
+
scope: "run",
|
|
747
|
+
type: "include_exited",
|
|
748
|
+
payload: {
|
|
749
|
+
mountPath: exited.mountPath.split("/"),
|
|
750
|
+
workflowName: composition?.scopes[exited.mountPath]?.workflowName ?? exited.mountName,
|
|
751
|
+
invocation: entrySteps.length,
|
|
752
|
+
exit: exited.exitName,
|
|
753
|
+
output: attempt.result.output ?? null,
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
625
758
|
lastOutput = attempt.result.output;
|
|
626
759
|
if (node.nodeType === "checkpoint") {
|
|
627
760
|
await this.finishRun(runDir, state, "waiting", {
|
|
@@ -678,13 +811,13 @@ export class WorkflowEngine {
|
|
|
678
811
|
state: WorkflowRunState,
|
|
679
812
|
attempt: NodeAttempt,
|
|
680
813
|
): string | null {
|
|
814
|
+
if (attempt.result.outcome === "cancelled" || this.cancelled) {
|
|
815
|
+
throw new CancelledError();
|
|
816
|
+
}
|
|
681
817
|
const next = resolveNextForOutcome(workflow.edges, attempt.result.nodeId, attempt.result);
|
|
682
818
|
if (next !== null) {
|
|
683
819
|
return next;
|
|
684
820
|
}
|
|
685
|
-
if (attempt.result.outcome === "cancelled" || this.cancelled) {
|
|
686
|
-
throw new CancelledError();
|
|
687
|
-
}
|
|
688
821
|
if (attempt.result.outcome === "timed_out") {
|
|
689
822
|
state.status = "timed_out";
|
|
690
823
|
}
|
|
@@ -693,7 +826,11 @@ export class WorkflowEngine {
|
|
|
693
826
|
: new Error(attempt.result.error ?? `Workflow node failed: ${attempt.result.nodeId}`);
|
|
694
827
|
}
|
|
695
828
|
|
|
696
|
-
private recordAttempt(
|
|
829
|
+
private recordAttempt(
|
|
830
|
+
workflow: WorkflowDefinition,
|
|
831
|
+
state: WorkflowRunState,
|
|
832
|
+
attempt: NodeAttempt,
|
|
833
|
+
): void {
|
|
697
834
|
state.results[attempt.result.nodeId] = attempt.result;
|
|
698
835
|
if (attempt.result.outcome === "ok") {
|
|
699
836
|
state.outputs[attempt.result.nodeId] = attempt.result.output;
|
|
@@ -702,6 +839,11 @@ export class WorkflowEngine {
|
|
|
702
839
|
// must not survive next to a non-ok latest result.
|
|
703
840
|
delete state.outputs[attempt.result.nodeId];
|
|
704
841
|
}
|
|
842
|
+
const exit = compositionMetadata(workflow)?.exits[attempt.result.nodeId];
|
|
843
|
+
if (exit !== undefined && attempt.result.outcome === "ok") {
|
|
844
|
+
state.outputs[exit.mountPath] = attempt.result.output;
|
|
845
|
+
state.results[exit.mountPath] = { ...attempt.result, nodeId: exit.mountPath };
|
|
846
|
+
}
|
|
705
847
|
const step: WorkflowStepRecord = {
|
|
706
848
|
attemptId: attempt.result.attemptId,
|
|
707
849
|
nodeId: attempt.result.nodeId,
|
|
@@ -977,7 +1119,12 @@ export class WorkflowEngine {
|
|
|
977
1119
|
case "action":
|
|
978
1120
|
return await this.runActionNode(node, context, nodeId, attemptId, signal, meta);
|
|
979
1121
|
case "checkpoint":
|
|
980
|
-
return await runCheckpointNode(node, context
|
|
1122
|
+
return await runCheckpointNode(node, context, {
|
|
1123
|
+
store: this.store,
|
|
1124
|
+
workflowName: workflow.name,
|
|
1125
|
+
nodeId,
|
|
1126
|
+
attemptId,
|
|
1127
|
+
});
|
|
981
1128
|
}
|
|
982
1129
|
}
|
|
983
1130
|
|
|
@@ -1154,7 +1301,30 @@ export class WorkflowEngine {
|
|
|
1154
1301
|
async function runCheckpointNode(
|
|
1155
1302
|
node: CheckpointNodeDefinition,
|
|
1156
1303
|
context: WorkflowNodeContext,
|
|
1304
|
+
execution: {
|
|
1305
|
+
store: WorkflowRunStore;
|
|
1306
|
+
workflowName: string;
|
|
1307
|
+
nodeId: string;
|
|
1308
|
+
attemptId: string;
|
|
1309
|
+
},
|
|
1157
1310
|
): Promise<NodeExecution> {
|
|
1311
|
+
if (node.humanDecision !== undefined) {
|
|
1312
|
+
const prompt = await node.humanDecision.request(context);
|
|
1313
|
+
const audience =
|
|
1314
|
+
typeof node.humanDecision.audience === "function"
|
|
1315
|
+
? await node.humanDecision.audience(context)
|
|
1316
|
+
: node.humanDecision.audience;
|
|
1317
|
+
const request = createHumanDecisionRequest({
|
|
1318
|
+
runId: context.state.runId,
|
|
1319
|
+
workflowName: execution.workflowName,
|
|
1320
|
+
nodeId: execution.nodeId,
|
|
1321
|
+
attemptId: execution.attemptId,
|
|
1322
|
+
contract: { audience, choices: node.humanDecision.choices },
|
|
1323
|
+
prompt,
|
|
1324
|
+
});
|
|
1325
|
+
await new HumanDecisionStore(execution.store.outputRoot).createRequest(request);
|
|
1326
|
+
return { output: request, promptText: null };
|
|
1327
|
+
}
|
|
1158
1328
|
const output = node.run ? await node.run(context) : { summary: node.summary ?? "checkpoint" };
|
|
1159
1329
|
return { output, promptText: null };
|
|
1160
1330
|
}
|
|
@@ -1234,6 +1404,54 @@ function abortRejection(signal: AbortSignal): Promise<never> {
|
|
|
1234
1404
|
});
|
|
1235
1405
|
}
|
|
1236
1406
|
|
|
1407
|
+
function countExecutableSteps(workflow: WorkflowDefinition, steps: WorkflowStepRecord[]): number {
|
|
1408
|
+
const metadata = compositionMetadata(workflow);
|
|
1409
|
+
if (metadata === undefined) return steps.length;
|
|
1410
|
+
return steps.filter(
|
|
1411
|
+
(step) =>
|
|
1412
|
+
metadata.entries[step.nodeId] === undefined && metadata.exits[step.nodeId] === undefined,
|
|
1413
|
+
).length;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function assertInvocationStepLimit(
|
|
1417
|
+
metadata: ReturnType<typeof compositionMetadata>,
|
|
1418
|
+
nodeId: string,
|
|
1419
|
+
steps: WorkflowStepRecord[],
|
|
1420
|
+
): void {
|
|
1421
|
+
if (metadata === undefined) return;
|
|
1422
|
+
const scopes = Object.values(metadata.scopes)
|
|
1423
|
+
.filter(
|
|
1424
|
+
(candidate) =>
|
|
1425
|
+
candidate.path !== "" &&
|
|
1426
|
+
candidate.maxSteps !== undefined &&
|
|
1427
|
+
nodeId.startsWith(`${candidate.path}/`),
|
|
1428
|
+
)
|
|
1429
|
+
.sort((a, b) => a.path.length - b.path.length);
|
|
1430
|
+
for (const scope of scopes) {
|
|
1431
|
+
let entryIndex = -1;
|
|
1432
|
+
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
1433
|
+
if (steps[index]?.nodeId === scope.path) {
|
|
1434
|
+
entryIndex = index;
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (entryIndex < 0) throw new Error(`Workflow include entry is missing: ${scope.path}`);
|
|
1439
|
+
const attempts = steps
|
|
1440
|
+
.slice(entryIndex + 1)
|
|
1441
|
+
.filter(
|
|
1442
|
+
(step) =>
|
|
1443
|
+
step.nodeId.startsWith(`${scope.path}/`) &&
|
|
1444
|
+
metadata.entries[step.nodeId] === undefined &&
|
|
1445
|
+
metadata.exits[step.nodeId] === undefined,
|
|
1446
|
+
).length;
|
|
1447
|
+
if (attempts >= (scope.maxSteps as number)) {
|
|
1448
|
+
throw new Error(
|
|
1449
|
+
`Included workflow ${scope.workflowName} at ${scope.path} exceeded maxSteps=${scope.maxSteps}`,
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1237
1455
|
/**
|
|
1238
1456
|
* Outputs are persisted to the run bundle, so they must be JSON-serializable.
|
|
1239
1457
|
* Failing here turns a bad callback return value into a normal node failure
|
|
@@ -1255,6 +1473,25 @@ function workflowSourceMismatch(
|
|
|
1255
1473
|
);
|
|
1256
1474
|
}
|
|
1257
1475
|
|
|
1476
|
+
function workflowIdentityMismatch(
|
|
1477
|
+
state: WorkflowRunState,
|
|
1478
|
+
workflow: WorkflowDefinition,
|
|
1479
|
+
source: WorkflowSource | undefined,
|
|
1480
|
+
): boolean {
|
|
1481
|
+
if (workflowSourceMismatch(state, source)) return true;
|
|
1482
|
+
const metadata = compositionMetadata(workflow);
|
|
1483
|
+
const currentSources = metadata?.sources ?? [];
|
|
1484
|
+
if (!isDeepStrictEqual(state.workflowSources ?? [], currentSources)) return true;
|
|
1485
|
+
const currentDigest = metadata?.snapshot.mounts.length ? definitionDigest(workflow) : undefined;
|
|
1486
|
+
return state.definitionDigest !== currentDigest;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function definitionDigest(workflow: WorkflowDefinition): string {
|
|
1490
|
+
return `sha256:${createHash("sha256")
|
|
1491
|
+
.update(JSON.stringify(createDefinitionSnapshot(workflow)))
|
|
1492
|
+
.digest("hex")}`;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1258
1495
|
function assertJsonSerializable(value: unknown, what: string): void {
|
|
1259
1496
|
let encoded: string | undefined;
|
|
1260
1497
|
try {
|
package/src/workflows/graph.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compileWorkflowDefinition, isCompiledWorkflow } from "./composition.js";
|
|
1
2
|
import { assertValidWorkflowDefinitionShape } from "./schema.js";
|
|
2
3
|
import type { WorkflowDefinition, WorkflowEdge, WorkflowNodeResult } from "./types.js";
|
|
3
4
|
|
|
@@ -6,17 +7,18 @@ import type { WorkflowDefinition, WorkflowEdge, WorkflowNodeResult } from "./typ
|
|
|
6
7
|
* at most one outgoing edge per node.
|
|
7
8
|
*/
|
|
8
9
|
export function validateWorkflowDefinition(workflow: WorkflowDefinition): void {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const executable = isCompiledWorkflow(workflow) ? workflow : compileWorkflowDefinition(workflow);
|
|
11
|
+
assertValidWorkflowDefinitionShape(executable, { compiled: true });
|
|
12
|
+
if (!Object.hasOwn(executable.nodes, executable.startAt)) {
|
|
13
|
+
throw new Error(`Workflow start node is missing: ${executable.startAt}`);
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const outgoingEdges = new Set<string>();
|
|
15
|
-
for (const edge of
|
|
16
|
-
validateWorkflowEdge(
|
|
17
|
+
for (const edge of executable.edges) {
|
|
18
|
+
validateWorkflowEdge(executable, edge, outgoingEdges);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
assertAllNodesReachable(
|
|
21
|
+
assertAllNodesReachable(executable);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Reject nodes that no path from `startAt` can ever reach. */
|