@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/index.ts
CHANGED
|
@@ -4,11 +4,56 @@ export {
|
|
|
4
4
|
checkpoint,
|
|
5
5
|
compute,
|
|
6
6
|
defineWorkflow,
|
|
7
|
+
defineWorkflowRegistry,
|
|
8
|
+
includeWorkflow,
|
|
9
|
+
includedResult,
|
|
7
10
|
isWorkflowDefinition,
|
|
8
11
|
notify,
|
|
9
12
|
shell,
|
|
10
13
|
} from "./definition.js";
|
|
11
14
|
export { decision, decisionEdge, type DecisionDefinition } from "./decision.js";
|
|
15
|
+
export {
|
|
16
|
+
MAX_PRESENTATION_BLOCKS,
|
|
17
|
+
MAX_PRESENTATION_CODE_UNITS,
|
|
18
|
+
MAX_PRESENTATION_ITEMS,
|
|
19
|
+
MAX_PRESENTATION_STRING_CODE_UNITS,
|
|
20
|
+
MAX_PRESENTATION_TRANSPORT_PARTS,
|
|
21
|
+
decisionDocumentSegments,
|
|
22
|
+
decisionPresentationDigest,
|
|
23
|
+
decisionPresentationFingerprint,
|
|
24
|
+
digestCanonical,
|
|
25
|
+
humanDecisionChannelRequest,
|
|
26
|
+
legacyDecisionPresentation,
|
|
27
|
+
normalizeDecisionPresentation,
|
|
28
|
+
validateHumanDecisionRequestIntegrity,
|
|
29
|
+
type DecisionDocumentSegment,
|
|
30
|
+
} from "./decision-presentation.js";
|
|
31
|
+
export {
|
|
32
|
+
HumanDecisionStore,
|
|
33
|
+
canonicalJson,
|
|
34
|
+
choice,
|
|
35
|
+
createHumanDecisionAttemptId,
|
|
36
|
+
createHumanDecisionRequest,
|
|
37
|
+
defineHumanChoices,
|
|
38
|
+
digest,
|
|
39
|
+
humanDecision,
|
|
40
|
+
humanDecisionEdge,
|
|
41
|
+
humanDecisionStateRoot,
|
|
42
|
+
textInput,
|
|
43
|
+
validateHumanDecisionResponse,
|
|
44
|
+
validateHumanDecisionSubmission,
|
|
45
|
+
type HumanDecisionAcceptance,
|
|
46
|
+
type HumanDecisionChoiceDefinition,
|
|
47
|
+
type HumanDecisionDefinition,
|
|
48
|
+
type HumanDecisionResponseFor,
|
|
49
|
+
} from "./human-decision.js";
|
|
50
|
+
export {
|
|
51
|
+
compileWorkflowDefinition,
|
|
52
|
+
compositionMetadata,
|
|
53
|
+
isCompiledWorkflow,
|
|
54
|
+
type CompileWorkflowOptions,
|
|
55
|
+
type WorkflowCompositionMetadata,
|
|
56
|
+
} from "./composition.js";
|
|
12
57
|
export { WorkflowEngine, appendStepContract } from "./engine.js";
|
|
13
58
|
export { CancelledError, TimeoutError } from "./errors.js";
|
|
14
59
|
export {
|
|
@@ -76,6 +121,9 @@ export {
|
|
|
76
121
|
type ReadRunBundleOptions,
|
|
77
122
|
} from "./store.js";
|
|
78
123
|
export type {
|
|
124
|
+
AcceptedHumanDecision,
|
|
125
|
+
AcceptedHumanDecisionV1,
|
|
126
|
+
AcceptedHumanDecisionV2,
|
|
79
127
|
AgentNodeDefinition,
|
|
80
128
|
AgentStepContract,
|
|
81
129
|
AgentStepExecutor,
|
|
@@ -89,16 +137,57 @@ export type {
|
|
|
89
137
|
ConversationRange,
|
|
90
138
|
ComputeNodeDefinition,
|
|
91
139
|
FunctionActionNodeDefinition,
|
|
140
|
+
DecisionPresentation,
|
|
141
|
+
DecisionPresentationBlock,
|
|
142
|
+
DecisionPresentationBullets,
|
|
143
|
+
DecisionPresentationFields,
|
|
144
|
+
DecisionPresentationParagraph,
|
|
145
|
+
DecisionPresentationPreformatted,
|
|
146
|
+
DecisionPresentationSection,
|
|
147
|
+
HumanDecisionAnswerSource,
|
|
148
|
+
HumanDecisionAudience,
|
|
149
|
+
HumanDecisionCancellationRecord,
|
|
150
|
+
HumanDecisionChannelRequest,
|
|
151
|
+
HumanDecisionChoice,
|
|
152
|
+
HumanDecisionChoiceMap,
|
|
153
|
+
HumanDecisionContinuationRecord,
|
|
154
|
+
HumanDecisionDeliveryRecord,
|
|
155
|
+
HumanDecisionDeliveryRecordV1,
|
|
156
|
+
HumanDecisionDeliveryRecordV2,
|
|
157
|
+
HumanDecisionNodeContract,
|
|
158
|
+
HumanDecisionPrompt,
|
|
159
|
+
HumanDecisionReceipt,
|
|
160
|
+
HumanDecisionReceiptV1,
|
|
161
|
+
HumanDecisionReceiptV2,
|
|
162
|
+
HumanDecisionRequest,
|
|
163
|
+
HumanDecisionRequestV1,
|
|
164
|
+
HumanDecisionRequestV2,
|
|
165
|
+
HumanDecisionResponse,
|
|
166
|
+
HumanDecisionSettlementRecord,
|
|
167
|
+
HumanDecisionSubmission,
|
|
168
|
+
HumanDecisionTextInput,
|
|
169
|
+
LegacyHumanDecisionPrompt,
|
|
92
170
|
MaybePromise,
|
|
93
171
|
NotifyNodeDefinition,
|
|
172
|
+
PresentedHumanDecisionPrompt,
|
|
94
173
|
ShellActionExecution,
|
|
95
174
|
ShellActionNodeDefinition,
|
|
96
175
|
ShellActionResult,
|
|
97
176
|
WorkflowActionContext,
|
|
98
177
|
WorkflowActionReceipt,
|
|
178
|
+
WorkflowCompositionSnapshot,
|
|
99
179
|
WorkflowDefinition,
|
|
100
180
|
WorkflowDefinitionSnapshot,
|
|
101
181
|
WorkflowEdge,
|
|
182
|
+
WorkflowExitDefinition,
|
|
183
|
+
WorkflowExitMap,
|
|
184
|
+
WorkflowExitOutputs,
|
|
185
|
+
WorkflowIncludeDefinition,
|
|
186
|
+
WorkflowIncludedResult,
|
|
187
|
+
WorkflowInputOf,
|
|
188
|
+
WorkflowMountedSource,
|
|
189
|
+
WorkflowMountSnapshot,
|
|
190
|
+
WorkflowValueParser,
|
|
102
191
|
WorkflowEngineOptions,
|
|
103
192
|
WorkflowNodeCommon,
|
|
104
193
|
WorkflowNodeContext,
|
package/src/workflows/loader.ts
CHANGED
|
@@ -5,9 +5,19 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { createJiti } from "jiti";
|
|
7
7
|
import type { BuiltinWorkflowCatalog } from "./catalog.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
compileWorkflowDefinition,
|
|
10
|
+
compositionMetadata,
|
|
11
|
+
type WorkflowCompositionSourceMap,
|
|
12
|
+
} from "./composition.js";
|
|
13
|
+
import { defineWorkflow, isWorkflowDefinition } from "./definition.js";
|
|
9
14
|
import { WorkflowSourceChangedError } from "./errors.js";
|
|
10
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
WorkflowDefinition,
|
|
17
|
+
WorkflowIncludeDefinition,
|
|
18
|
+
WorkflowMountedSource,
|
|
19
|
+
WorkflowSource,
|
|
20
|
+
} from "./types.js";
|
|
11
21
|
|
|
12
22
|
const WORKFLOW_FILE_SUFFIXES = [".workflow.ts", ".workflow.js", ".workflow.mts", ".workflow.mjs"];
|
|
13
23
|
|
|
@@ -25,9 +35,12 @@ export type WorkflowSearchPaths = {
|
|
|
25
35
|
export type ResolvedWorkflow = {
|
|
26
36
|
definition: WorkflowDefinition;
|
|
27
37
|
source: WorkflowSource;
|
|
38
|
+
sources: WorkflowMountedSource[];
|
|
28
39
|
sourceKind: DiscoveredWorkflow["source"];
|
|
29
40
|
};
|
|
30
41
|
|
|
42
|
+
type SingleResolvedWorkflow = Omit<ResolvedWorkflow, "sources">;
|
|
43
|
+
|
|
31
44
|
/** Directories scanned for user workflow files, in precedence order. */
|
|
32
45
|
export function workflowSearchDirs(
|
|
33
46
|
options: WorkflowSearchPaths,
|
|
@@ -59,6 +72,12 @@ export function workflowFileStem(filePath: string): string {
|
|
|
59
72
|
// Alias package imports to this process's workflow API. User files can reload,
|
|
60
73
|
// but their node constructors and validators remain from one engine version.
|
|
61
74
|
const SELF_ENTRY = path.join(path.dirname(fileURLToPath(import.meta.url)), "index");
|
|
75
|
+
const BUILTINS_ENTRY = path.join(
|
|
76
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
77
|
+
"..",
|
|
78
|
+
"builtins",
|
|
79
|
+
"index",
|
|
80
|
+
);
|
|
62
81
|
|
|
63
82
|
/** Load a user workflow module from disk. */
|
|
64
83
|
export async function loadWorkflowFile(filePath: string): Promise<WorkflowDefinition> {
|
|
@@ -66,7 +85,11 @@ export async function loadWorkflowFile(filePath: string): Promise<WorkflowDefini
|
|
|
66
85
|
const jiti = createJiti(pathToFileURL(absolutePath).href, {
|
|
67
86
|
interopDefault: true,
|
|
68
87
|
moduleCache: false,
|
|
69
|
-
alias: {
|
|
88
|
+
alias: {
|
|
89
|
+
"@osolmaz/pi-workflows/builtins": BUILTINS_ENTRY,
|
|
90
|
+
"@osolmaz/pi-workflows": SELF_ENTRY,
|
|
91
|
+
"pi-workflows": SELF_ENTRY,
|
|
92
|
+
},
|
|
70
93
|
});
|
|
71
94
|
const loaded = (await jiti.import(absolutePath, { default: true })) as unknown;
|
|
72
95
|
if (!isWorkflowDefinition(loaded)) {
|
|
@@ -111,12 +134,42 @@ async function listWorkflowFiles(dir: string): Promise<string[]> {
|
|
|
111
134
|
.sort();
|
|
112
135
|
}
|
|
113
136
|
|
|
114
|
-
/** Resolve a workflow
|
|
137
|
+
/** Resolve a workflow and every nested include before returning it. */
|
|
115
138
|
export async function resolveWorkflowRef(
|
|
116
139
|
ref: string,
|
|
117
140
|
options: WorkflowSearchPaths,
|
|
118
141
|
catalog?: BuiltinWorkflowCatalog,
|
|
119
142
|
): Promise<ResolvedWorkflow> {
|
|
143
|
+
const root = await resolveSingleWorkflowRef(ref, options, catalog, options.cwd);
|
|
144
|
+
const sourceMap: WorkflowCompositionSourceMap = new Map([[root.definition, root.source]]);
|
|
145
|
+
const activeSources: string[] = [];
|
|
146
|
+
const definition = await resolveIncludes(
|
|
147
|
+
root.definition,
|
|
148
|
+
sourceBaseDir(root.source),
|
|
149
|
+
options,
|
|
150
|
+
catalog,
|
|
151
|
+
sourceMap,
|
|
152
|
+
activeSources,
|
|
153
|
+
sourceKey(root.source),
|
|
154
|
+
);
|
|
155
|
+
const compiled = compileWorkflowDefinition(definition, {
|
|
156
|
+
rootSource: root.source,
|
|
157
|
+
sourceMap,
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
definition: compiled,
|
|
161
|
+
source: root.source,
|
|
162
|
+
sources: compositionMetadata(compiled)?.sources ?? [],
|
|
163
|
+
sourceKind: root.sourceKind,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function resolveSingleWorkflowRef(
|
|
168
|
+
ref: string,
|
|
169
|
+
options: WorkflowSearchPaths,
|
|
170
|
+
catalog: BuiltinWorkflowCatalog | undefined,
|
|
171
|
+
relativeBase: string,
|
|
172
|
+
): Promise<SingleResolvedWorkflow> {
|
|
120
173
|
if (ref.startsWith("builtin:")) {
|
|
121
174
|
const id = ref.slice("builtin:".length);
|
|
122
175
|
const builtin = catalog?.get(id);
|
|
@@ -128,7 +181,7 @@ export async function resolveWorkflowRef(
|
|
|
128
181
|
};
|
|
129
182
|
}
|
|
130
183
|
if (looksLikePath(ref)) {
|
|
131
|
-
const absolutePath = path.resolve(
|
|
184
|
+
const absolutePath = path.resolve(relativeBase, ref);
|
|
132
185
|
await fs.access(absolutePath);
|
|
133
186
|
return {
|
|
134
187
|
definition: await loadWorkflowFile(absolutePath),
|
|
@@ -142,30 +195,145 @@ export async function resolveWorkflowRef(
|
|
|
142
195
|
const available = discovered.map((workflow) => workflow.name).join(", ") || "(none)";
|
|
143
196
|
throw new Error(`Unknown workflow ${JSON.stringify(ref)}. Available workflows: ${available}`);
|
|
144
197
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
198
|
+
const resolved = await resolveSingleWorkflowRef(match.ref, options, catalog, options.cwd);
|
|
199
|
+
return { ...resolved, sourceKind: match.source };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function resolveIncludes(
|
|
203
|
+
workflow: WorkflowDefinition,
|
|
204
|
+
baseDir: string | undefined,
|
|
205
|
+
options: WorkflowSearchPaths,
|
|
206
|
+
catalog: BuiltinWorkflowCatalog | undefined,
|
|
207
|
+
sourceMap: WorkflowCompositionSourceMap,
|
|
208
|
+
activeSources: string[],
|
|
209
|
+
currentSourceKey: string,
|
|
210
|
+
): Promise<WorkflowDefinition> {
|
|
211
|
+
const cycleAt = activeSources.indexOf(currentSourceKey);
|
|
212
|
+
if (cycleAt >= 0) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Workflow include source cycle: ${[...activeSources.slice(cycleAt), currentSourceKey].join(" -> ")}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
activeSources.push(currentSourceKey);
|
|
218
|
+
const resolvedIncludes: Record<string, WorkflowIncludeDefinition> = {};
|
|
219
|
+
for (const [mountName, include] of Object.entries(workflow.includes ?? {})) {
|
|
220
|
+
let child: WorkflowDefinition;
|
|
221
|
+
let childSource: WorkflowSource | undefined;
|
|
222
|
+
let childBaseDir = baseDir;
|
|
223
|
+
if (typeof include.workflow === "string") {
|
|
224
|
+
if (
|
|
225
|
+
baseDir === undefined &&
|
|
226
|
+
looksLikePath(include.workflow) &&
|
|
227
|
+
!path.isAbsolute(include.workflow)
|
|
228
|
+
) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Built-in workflow ${workflow.name} cannot resolve relative include ${include.workflow}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const resolved = await resolveSingleWorkflowRef(
|
|
234
|
+
include.workflow,
|
|
235
|
+
options,
|
|
236
|
+
catalog,
|
|
237
|
+
baseDir ?? options.cwd,
|
|
238
|
+
);
|
|
239
|
+
child = resolved.definition;
|
|
240
|
+
childSource = resolved.source;
|
|
241
|
+
childBaseDir = sourceBaseDir(resolved.source);
|
|
242
|
+
} else {
|
|
243
|
+
child = include.workflow;
|
|
244
|
+
childSource = await sourceForDirectDefinition(child, catalog);
|
|
245
|
+
childBaseDir = childSource ? sourceBaseDir(childSource) : baseDir;
|
|
246
|
+
}
|
|
247
|
+
if (childSource !== undefined) sourceMap.set(child, childSource);
|
|
248
|
+
assertContractCompatible(include, child, mountName);
|
|
249
|
+
const childKey = childSource ? sourceKey(childSource) : `memory:${child.name}`;
|
|
250
|
+
const resolvedChild = await resolveIncludes(
|
|
251
|
+
child,
|
|
252
|
+
childBaseDir,
|
|
253
|
+
options,
|
|
254
|
+
catalog,
|
|
255
|
+
sourceMap,
|
|
256
|
+
activeSources,
|
|
257
|
+
childKey,
|
|
258
|
+
);
|
|
259
|
+
if (childSource !== undefined) sourceMap.set(resolvedChild, childSource);
|
|
260
|
+
resolvedIncludes[mountName] = { ...include, workflow: resolvedChild };
|
|
261
|
+
}
|
|
262
|
+
activeSources.pop();
|
|
263
|
+
if (Object.keys(resolvedIncludes).length === 0) return workflow;
|
|
264
|
+
const resolved = defineWorkflow({ ...workflow, includes: resolvedIncludes });
|
|
265
|
+
const ownSource = sourceMap.get(workflow);
|
|
266
|
+
if (ownSource !== undefined) sourceMap.set(resolved, ownSource);
|
|
267
|
+
return resolved;
|
|
152
268
|
}
|
|
153
269
|
|
|
154
|
-
|
|
270
|
+
async function sourceForDirectDefinition(
|
|
271
|
+
workflow: WorkflowDefinition,
|
|
272
|
+
catalog?: BuiltinWorkflowCatalog,
|
|
273
|
+
): Promise<WorkflowSource | undefined> {
|
|
274
|
+
const builtin = catalog?.sourceForDefinition(workflow);
|
|
275
|
+
if (builtin !== undefined) return builtin;
|
|
276
|
+
if (workflow.source === undefined) return undefined;
|
|
277
|
+
let filePath: string;
|
|
278
|
+
try {
|
|
279
|
+
filePath = fileURLToPath(workflow.source);
|
|
280
|
+
} catch {
|
|
281
|
+
throw new Error(`Workflow ${workflow.name} source must be a file URL: ${workflow.source}`);
|
|
282
|
+
}
|
|
283
|
+
return { kind: "file", path: filePath, hash: await hashWorkflowSource(filePath) };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function assertContractCompatible(
|
|
287
|
+
include: WorkflowIncludeDefinition,
|
|
288
|
+
child: WorkflowDefinition,
|
|
289
|
+
mountName: string,
|
|
290
|
+
): void {
|
|
291
|
+
if (include.contract === undefined) return;
|
|
292
|
+
if (
|
|
293
|
+
include.contract.contractId !== undefined &&
|
|
294
|
+
child.contractId !== include.contract.contractId
|
|
295
|
+
) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Workflow include ${mountName} contract mismatch: expected ${include.contract.contractId}; got ${child.contractId ?? "none"}`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const expected = Object.keys(include.contract.exits ?? {}).sort();
|
|
301
|
+
const actual = Object.keys(child.exits ?? {}).sort();
|
|
302
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual)) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Workflow include ${mountName} exit contract mismatch: expected ${expected.join(", ") || "none"}; got ${actual.join(", ") || "none"}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if ((include.contract.input === undefined) !== (child.input === undefined)) {
|
|
308
|
+
throw new Error(`Workflow include ${mountName} input contract mismatch`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function sourceBaseDir(source: WorkflowSource): string | undefined {
|
|
313
|
+
return source.kind === "file" ? path.dirname(source.path) : undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function sourceKey(source: WorkflowSource): string {
|
|
317
|
+
return source.kind === "file"
|
|
318
|
+
? `file:${source.path}:${source.hash}`
|
|
319
|
+
: `builtin:${source.id}:${source.revision}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Resolve an already persisted canonical source and its includes. */
|
|
155
323
|
export async function resolveWorkflowSource(
|
|
156
324
|
source: WorkflowSource,
|
|
157
325
|
catalog?: BuiltinWorkflowCatalog,
|
|
158
326
|
runId = source.kind === "builtin" ? `builtin:${source.id}` : source.path,
|
|
327
|
+
options: WorkflowSearchPaths = { cwd: process.cwd() },
|
|
159
328
|
): Promise<WorkflowDefinition> {
|
|
160
329
|
if (source.kind === "builtin") {
|
|
161
330
|
if (catalog === undefined) throw new Error(`No built-in workflow catalog for ${source.id}`);
|
|
162
|
-
|
|
331
|
+
catalog.resolve(source, runId);
|
|
332
|
+
return (await resolveWorkflowRef(`builtin:${source.id}`, options, catalog)).definition;
|
|
163
333
|
}
|
|
164
334
|
const actualHash = await hashWorkflowSource(source.path);
|
|
165
|
-
if (actualHash !== source.hash)
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
return await loadWorkflowFile(source.path);
|
|
335
|
+
if (actualHash !== source.hash) throw new WorkflowSourceChangedError(runId);
|
|
336
|
+
return (await resolveWorkflowRef(source.path, options, catalog)).definition;
|
|
169
337
|
}
|
|
170
338
|
|
|
171
339
|
function looksLikePath(ref: string): boolean {
|
package/src/workflows/schema.ts
CHANGED
|
@@ -134,6 +134,23 @@ export function assertValidCheckpointNode(
|
|
|
134
134
|
fail(`node ${nodeId} summary must be a string`);
|
|
135
135
|
}
|
|
136
136
|
assertOptionalFunction(node.run, `node ${nodeId} run`);
|
|
137
|
+
if (node.humanDecision !== undefined) {
|
|
138
|
+
assertRecord(node.humanDecision, `node ${nodeId} humanDecision`);
|
|
139
|
+
if (
|
|
140
|
+
(typeof node.humanDecision.audience !== "string" ||
|
|
141
|
+
node.humanDecision.audience.length === 0) &&
|
|
142
|
+
typeof node.humanDecision.audience !== "function"
|
|
143
|
+
) {
|
|
144
|
+
fail(`node ${nodeId} humanDecision audience must be a non-empty string or function`);
|
|
145
|
+
}
|
|
146
|
+
assertRecord(node.humanDecision.choices, `node ${nodeId} humanDecision choices`);
|
|
147
|
+
if (Object.keys(node.humanDecision.choices).length === 0) {
|
|
148
|
+
fail(`node ${nodeId} humanDecision choices must not be empty`);
|
|
149
|
+
}
|
|
150
|
+
if (typeof node.humanDecision.request !== "function") {
|
|
151
|
+
fail(`node ${nodeId} humanDecision request must be a function`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
137
154
|
assertCommonNodeFields(node, nodeId);
|
|
138
155
|
}
|
|
139
156
|
|
|
@@ -199,7 +216,10 @@ function assertValidEdgeShape(edge: WorkflowEdge, index: number): void {
|
|
|
199
216
|
*/
|
|
200
217
|
const RESERVED_WORKFLOW_NAMES = new Set(["answer", "cancel", "list", "pause", "resume", "status"]);
|
|
201
218
|
|
|
202
|
-
export function assertValidWorkflowDefinitionShape(
|
|
219
|
+
export function assertValidWorkflowDefinitionShape(
|
|
220
|
+
definition: WorkflowDefinition,
|
|
221
|
+
options: { compiled?: boolean } = {},
|
|
222
|
+
): void {
|
|
203
223
|
assertRecord(definition, "workflow");
|
|
204
224
|
if (typeof definition.name !== "string" || definition.name.length === 0) {
|
|
205
225
|
fail("workflow requires a name");
|
|
@@ -207,6 +227,17 @@ export function assertValidWorkflowDefinitionShape(definition: WorkflowDefinitio
|
|
|
207
227
|
if (RESERVED_WORKFLOW_NAMES.has(definition.name)) {
|
|
208
228
|
fail(`workflow name ${JSON.stringify(definition.name)} is reserved for /workflow subcommands`);
|
|
209
229
|
}
|
|
230
|
+
if (definition.source !== undefined && typeof definition.source !== "string") {
|
|
231
|
+
fail("workflow source must be a string");
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
definition.contractId !== undefined &&
|
|
235
|
+
(typeof definition.contractId !== "string" ||
|
|
236
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(definition.contractId))
|
|
237
|
+
) {
|
|
238
|
+
fail("workflow contractId must be a stable identifier");
|
|
239
|
+
}
|
|
240
|
+
assertOptionalFunction(definition.input, "workflow input");
|
|
210
241
|
if (
|
|
211
242
|
definition.title !== undefined &&
|
|
212
243
|
typeof definition.title !== "string" &&
|
|
@@ -237,7 +268,14 @@ export function assertValidWorkflowDefinitionShape(definition: WorkflowDefinitio
|
|
|
237
268
|
fail("workflow requires at least one node");
|
|
238
269
|
}
|
|
239
270
|
for (const [nodeId, node] of Object.entries(definition.nodes)) {
|
|
240
|
-
|
|
271
|
+
const segments = nodeId.split("/");
|
|
272
|
+
if (
|
|
273
|
+
segments.some(
|
|
274
|
+
(segment) =>
|
|
275
|
+
!NODE_ID_PATTERN.test(segment) || (!options.compiled && segment.startsWith("__piw_")),
|
|
276
|
+
) ||
|
|
277
|
+
(!options.compiled && segments.length !== 1)
|
|
278
|
+
) {
|
|
241
279
|
fail(`node id ${JSON.stringify(nodeId)} must match ${NODE_ID_PATTERN.source}`);
|
|
242
280
|
}
|
|
243
281
|
// Ids like __proto__ or toString would collide with Object prototype
|
|
@@ -248,6 +286,45 @@ export function assertValidWorkflowDefinitionShape(definition: WorkflowDefinitio
|
|
|
248
286
|
assertRecord(node, `node ${nodeId}`);
|
|
249
287
|
assertValidNode(node, nodeId);
|
|
250
288
|
}
|
|
289
|
+
if (definition.includes !== undefined) {
|
|
290
|
+
assertRecord(definition.includes, "workflow includes");
|
|
291
|
+
for (const [mountName, include] of Object.entries(definition.includes)) {
|
|
292
|
+
if (!NODE_ID_PATTERN.test(mountName) || mountName.startsWith("__piw_")) {
|
|
293
|
+
fail(`include name ${JSON.stringify(mountName)} must match ${NODE_ID_PATTERN.source}`);
|
|
294
|
+
}
|
|
295
|
+
if (Object.hasOwn(definition.nodes, mountName)) {
|
|
296
|
+
fail(`include name ${JSON.stringify(mountName)} collides with a node id`);
|
|
297
|
+
}
|
|
298
|
+
assertRecord(include, `include ${mountName}`);
|
|
299
|
+
if (
|
|
300
|
+
typeof include.workflow !== "string" &&
|
|
301
|
+
(include.workflow === null || typeof include.workflow !== "object")
|
|
302
|
+
) {
|
|
303
|
+
fail(`include ${mountName} requires a workflow definition or reference`);
|
|
304
|
+
}
|
|
305
|
+
assertOptionalFunction(include.input, `include ${mountName} input`);
|
|
306
|
+
if (
|
|
307
|
+
include.contract !== undefined &&
|
|
308
|
+
(include.contract === null || typeof include.contract !== "object")
|
|
309
|
+
) {
|
|
310
|
+
fail(`include ${mountName} contract must be a workflow definition`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (definition.exits !== undefined) {
|
|
315
|
+
assertRecord(definition.exits, "workflow exits");
|
|
316
|
+
if (Object.keys(definition.exits).length === 0) fail("workflow exits must not be empty");
|
|
317
|
+
for (const [exitName, exit] of Object.entries(definition.exits)) {
|
|
318
|
+
if (!NODE_ID_PATTERN.test(exitName) || exitName.startsWith("__piw_")) {
|
|
319
|
+
fail(`exit name ${JSON.stringify(exitName)} must match ${NODE_ID_PATTERN.source}`);
|
|
320
|
+
}
|
|
321
|
+
assertRecord(exit, `exit ${exitName}`);
|
|
322
|
+
if (typeof exit.from !== "string" || exit.from.length === 0) {
|
|
323
|
+
fail(`exit ${exitName} requires from`);
|
|
324
|
+
}
|
|
325
|
+
assertOptionalFunction(exit.validate, `exit ${exitName} validate`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
251
328
|
if (!Array.isArray(definition.edges)) {
|
|
252
329
|
fail("workflow edges must be an array");
|
|
253
330
|
}
|
package/src/workflows/store.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { ArtifactWriter, encodeValue } from "./artifacts.js";
|
|
6
|
+
import { compositionMetadata } from "./composition.js";
|
|
6
7
|
import type {
|
|
7
8
|
WorkflowDefinition,
|
|
8
9
|
WorkflowDefinitionSnapshot,
|
|
@@ -1350,6 +1351,8 @@ function createManifest(
|
|
|
1350
1351
|
workflowName: state.workflowName,
|
|
1351
1352
|
...(state.runTitle !== undefined ? { runTitle: state.runTitle } : {}),
|
|
1352
1353
|
...(state.workflowSource !== undefined ? { workflowSource: state.workflowSource } : {}),
|
|
1354
|
+
...(state.workflowSources !== undefined ? { workflowSources: state.workflowSources } : {}),
|
|
1355
|
+
...(state.definitionDigest !== undefined ? { definitionDigest: state.definitionDigest } : {}),
|
|
1353
1356
|
startedAt: state.startedAt,
|
|
1354
1357
|
...(state.finishedAt !== undefined ? { finishedAt: state.finishedAt } : {}),
|
|
1355
1358
|
status: state.status,
|
|
@@ -1368,20 +1371,52 @@ function createManifest(
|
|
|
1368
1371
|
}
|
|
1369
1372
|
|
|
1370
1373
|
export function createDefinitionSnapshot(workflow: WorkflowDefinition): WorkflowDefinitionSnapshot {
|
|
1374
|
+
const composition = compositionMetadata(workflow)?.snapshot;
|
|
1371
1375
|
return {
|
|
1372
1376
|
schema: DEFINITION_SNAPSHOT_SCHEMA,
|
|
1373
1377
|
name: workflow.name,
|
|
1378
|
+
...(workflow.contractId !== undefined ? { contractId: workflow.contractId } : {}),
|
|
1374
1379
|
startAt: workflow.startAt,
|
|
1375
1380
|
nodes: Object.fromEntries(
|
|
1376
|
-
Object.entries(workflow.nodes).map(([nodeId, node]) => [
|
|
1381
|
+
Object.entries(workflow.nodes).map(([nodeId, node]) => [
|
|
1382
|
+
nodeId,
|
|
1383
|
+
snapshotNode(workflow, nodeId, node),
|
|
1384
|
+
]),
|
|
1377
1385
|
),
|
|
1378
1386
|
edges: structuredClone(workflow.edges),
|
|
1387
|
+
...(composition !== undefined ? { composition: structuredClone(composition) } : {}),
|
|
1379
1388
|
};
|
|
1380
1389
|
}
|
|
1381
1390
|
|
|
1382
|
-
function snapshotNode(
|
|
1391
|
+
function snapshotNode(
|
|
1392
|
+
workflow: WorkflowDefinition,
|
|
1393
|
+
nodeId: string,
|
|
1394
|
+
node: WorkflowNodeDefinition,
|
|
1395
|
+
): WorkflowNodeSnapshot {
|
|
1396
|
+
const composition = compositionMetadata(workflow);
|
|
1397
|
+
const entry = composition?.entries[nodeId];
|
|
1398
|
+
const exit = composition?.exits[nodeId];
|
|
1399
|
+
const scope = Object.values(composition?.scopes ?? {})
|
|
1400
|
+
.filter((candidate) => candidate.path !== "" && nodeId.startsWith(`${candidate.path}/`))
|
|
1401
|
+
.sort((a, b) => b.path.length - a.path.length)[0];
|
|
1402
|
+
const mountPath = entry?.mountPath ?? exit?.mountPath ?? scope?.path;
|
|
1403
|
+
const localNodeId =
|
|
1404
|
+
entry !== undefined
|
|
1405
|
+
? entry.mountName
|
|
1406
|
+
: exit !== undefined
|
|
1407
|
+
? exit.exitName
|
|
1408
|
+
: scope !== undefined
|
|
1409
|
+
? nodeId.slice(scope.path.length + 1)
|
|
1410
|
+
: undefined;
|
|
1383
1411
|
const common: WorkflowNodeSnapshot = {
|
|
1384
1412
|
nodeType: node.nodeType,
|
|
1413
|
+
...(mountPath !== undefined ? { mountPath: mountPath.split("/") } : {}),
|
|
1414
|
+
...(localNodeId !== undefined ? { localNodeId } : {}),
|
|
1415
|
+
...(entry !== undefined
|
|
1416
|
+
? { includeTransition: "entry" as const }
|
|
1417
|
+
: exit !== undefined
|
|
1418
|
+
? { includeTransition: "exit" as const }
|
|
1419
|
+
: {}),
|
|
1385
1420
|
...(typeof node.timeoutMs === "number" ? { timeoutMs: node.timeoutMs } : {}),
|
|
1386
1421
|
...(node.statusDetail !== undefined ? { statusDetail: node.statusDetail } : {}),
|
|
1387
1422
|
};
|
|
@@ -1394,6 +1429,14 @@ function snapshotNode(node: WorkflowNodeDefinition): WorkflowNodeSnapshot {
|
|
|
1394
1429
|
if (node.nodeType === "checkpoint" && node.summary !== undefined) {
|
|
1395
1430
|
common.summary = node.summary;
|
|
1396
1431
|
}
|
|
1432
|
+
if (node.nodeType === "checkpoint" && node.humanDecision !== undefined) {
|
|
1433
|
+
common.humanDecision = {
|
|
1434
|
+
audience:
|
|
1435
|
+
typeof node.humanDecision.audience === "string" ? node.humanDecision.audience : "<dynamic>",
|
|
1436
|
+
...(typeof node.humanDecision.audience === "function" ? { dynamicAudience: true } : {}),
|
|
1437
|
+
choices: structuredClone(node.humanDecision.choices),
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1397
1440
|
if (node.nodeType === "action") {
|
|
1398
1441
|
common.actionExecution = "exec" in node ? "shell" : "function";
|
|
1399
1442
|
}
|