@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.
Files changed (147) hide show
  1. package/README.md +64 -19
  2. package/dist/builtins/autodoc.workflow.d.ts +58 -0
  3. package/dist/builtins/autodoc.workflow.js +266 -0
  4. package/dist/builtins/autodoc.workflow.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +353 -0
  6. package/dist/builtins/autoimplement.workflow.js +961 -0
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -0
  8. package/dist/builtins/autoplan.workflow.d.ts +58 -0
  9. package/dist/builtins/autoplan.workflow.js +190 -0
  10. package/dist/builtins/autoplan.workflow.js.map +1 -0
  11. package/dist/builtins/catalog.js +9 -1
  12. package/dist/builtins/catalog.js.map +1 -1
  13. package/dist/builtins/index.d.ts +6 -0
  14. package/dist/builtins/index.js +7 -0
  15. package/dist/builtins/index.js.map +1 -0
  16. package/dist/builtins/monitor.workflow.d.ts +29 -3
  17. package/dist/builtins/monitor.workflow.js +325 -13
  18. package/dist/builtins/monitor.workflow.js.map +1 -1
  19. package/dist/builtins/plan-approval.workflow.d.ts +83 -0
  20. package/dist/builtins/plan-approval.workflow.js +148 -0
  21. package/dist/builtins/plan-approval.workflow.js.map +1 -0
  22. package/dist/builtins/plan-presentation.d.ts +7 -0
  23. package/dist/builtins/plan-presentation.js +44 -0
  24. package/dist/builtins/plan-presentation.js.map +1 -0
  25. package/dist/extension/decision-channels.d.ts +132 -0
  26. package/dist/extension/decision-channels.js +1082 -0
  27. package/dist/extension/decision-channels.js.map +1 -0
  28. package/dist/extension/index.d.ts +1 -0
  29. package/dist/extension/index.js +426 -11
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/widget.js +26 -2
  32. package/dist/extension/widget.js.map +1 -1
  33. package/dist/extension/workflow-tool.d.ts +1 -37
  34. package/dist/extension/workflow-tool.js +1 -42
  35. package/dist/extension/workflow-tool.js.map +1 -1
  36. package/dist/host/rpc-bridge.js +4 -21
  37. package/dist/host/rpc-bridge.js.map +1 -1
  38. package/dist/render/graph-render.js +46 -10
  39. package/dist/render/graph-render.js.map +1 -1
  40. package/dist/viewer/render.js +36 -2
  41. package/dist/viewer/render.js.map +1 -1
  42. package/dist/workflows/catalog.d.ts +1 -0
  43. package/dist/workflows/catalog.js +6 -0
  44. package/dist/workflows/catalog.js.map +1 -1
  45. package/dist/workflows/composition.d.ts +45 -0
  46. package/dist/workflows/composition.js +485 -0
  47. package/dist/workflows/composition.js.map +1 -0
  48. package/dist/workflows/decision-presentation.d.ts +18 -0
  49. package/dist/workflows/decision-presentation.js +417 -0
  50. package/dist/workflows/decision-presentation.js.map +1 -0
  51. package/dist/workflows/decision.d.ts +11 -5
  52. package/dist/workflows/decision.js.map +1 -1
  53. package/dist/workflows/definition.d.ts +22 -3
  54. package/dist/workflows/definition.js +46 -3
  55. package/dist/workflows/definition.js.map +1 -1
  56. package/dist/workflows/engine.d.ts +2 -1
  57. package/dist/workflows/engine.js +199 -18
  58. package/dist/workflows/engine.js.map +1 -1
  59. package/dist/workflows/graph.js +8 -6
  60. package/dist/workflows/graph.js.map +1 -1
  61. package/dist/workflows/human-decision.d.ts +87 -0
  62. package/dist/workflows/human-decision.js +583 -0
  63. package/dist/workflows/human-decision.js.map +1 -0
  64. package/dist/workflows/index.d.ts +5 -2
  65. package/dist/workflows/index.js +4 -1
  66. package/dist/workflows/index.js.map +1 -1
  67. package/dist/workflows/loader.d.ts +5 -4
  68. package/dist/workflows/loader.js +118 -18
  69. package/dist/workflows/loader.js.map +1 -1
  70. package/dist/workflows/schema.d.ts +3 -1
  71. package/dist/workflows/schema.js +64 -2
  72. package/dist/workflows/schema.js.map +1 -1
  73. package/dist/workflows/store.js +39 -2
  74. package/dist/workflows/store.js.map +1 -1
  75. package/dist/workflows/tool-input.d.ts +72 -0
  76. package/dist/workflows/tool-input.js +141 -0
  77. package/dist/workflows/tool-input.js.map +1 -0
  78. package/dist/workflows/types.d.ts +300 -2
  79. package/docs/CONTROLLERS.md +1 -1
  80. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  81. package/docs/HUMAN_DECISIONS.md +371 -0
  82. package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
  83. package/docs/MONITOR.md +46 -18
  84. package/docs/WORKFLOW_COMPOSITION.md +333 -0
  85. package/docs/plans/2026-08-19-human-decision-gates-plan.md +296 -0
  86. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +172 -0
  87. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +45 -0
  88. package/docs/plans/2026-08-19-workflow-composition-plan.md +308 -0
  89. package/docs/run-bundles.md +45 -10
  90. package/docs/workflows.md +106 -15
  91. package/examples/workflows/approved-plan.workflow.ts +58 -0
  92. package/examples/workflows/autoimplement.workflow.ts +1 -92
  93. package/examples/workflows/autoplan.workflow.ts +1 -0
  94. package/examples/workflows/human-decision.workflow.ts +62 -0
  95. package/herdr-plugin.toml +1 -1
  96. package/package.json +6 -1
  97. package/schemas/decision-presentation-v1.schema.json +83 -0
  98. package/schemas/human-decision-accepted-v1.schema.json +44 -0
  99. package/schemas/human-decision-accepted-v2.schema.json +50 -0
  100. package/schemas/human-decision-answer-attempt-v1.schema.json +37 -0
  101. package/schemas/human-decision-cancellation-v1.schema.json +15 -0
  102. package/schemas/human-decision-continuation-v1.schema.json +16 -0
  103. package/schemas/human-decision-delivery-v1.schema.json +28 -0
  104. package/schemas/human-decision-delivery-v2.schema.json +36 -0
  105. package/schemas/human-decision-receipt-v1.schema.json +33 -0
  106. package/schemas/human-decision-receipt-v2.schema.json +39 -0
  107. package/schemas/human-decision-request-v1.schema.json +59 -0
  108. package/schemas/human-decision-request-v2.schema.json +69 -0
  109. package/schemas/human-decision-resolution-v1.schema.json +27 -0
  110. package/schemas/human-decision-resolution-v2.schema.json +27 -0
  111. package/schemas/human-decision-settlement-v1.schema.json +28 -0
  112. package/skills/autodoc/SKILL.md +43 -0
  113. package/skills/autoimplement/SKILL.md +58 -0
  114. package/skills/autoimplement/agents/openai.yaml +4 -0
  115. package/skills/autoplan/SKILL.md +25 -0
  116. package/skills/monitor/SKILL.md +8 -1
  117. package/skills/pi-workflows/SKILL.md +5 -3
  118. package/src/builtins/autodoc.workflow.ts +325 -0
  119. package/src/builtins/autoimplement.workflow.ts +1123 -0
  120. package/src/builtins/autoplan.workflow.ts +231 -0
  121. package/src/builtins/catalog.ts +9 -1
  122. package/src/builtins/index.ts +32 -0
  123. package/src/builtins/monitor.workflow.ts +396 -15
  124. package/src/builtins/plan-approval.workflow.ts +190 -0
  125. package/src/builtins/plan-presentation.ts +57 -0
  126. package/src/extension/decision-channels.ts +1533 -0
  127. package/src/extension/index.ts +499 -9
  128. package/src/extension/widget.ts +39 -2
  129. package/src/extension/workflow-tool.ts +5 -81
  130. package/src/host/rpc-bridge.ts +7 -30
  131. package/src/render/graph-render.ts +54 -5
  132. package/src/viewer/render.ts +56 -3
  133. package/src/workflows/catalog.ts +7 -0
  134. package/src/workflows/composition.ts +642 -0
  135. package/src/workflows/decision-presentation.ts +501 -0
  136. package/src/workflows/decision.ts +12 -5
  137. package/src/workflows/definition.ts +118 -8
  138. package/src/workflows/engine.ts +257 -20
  139. package/src/workflows/graph.ts +8 -6
  140. package/src/workflows/human-decision.ts +819 -0
  141. package/src/workflows/index.ts +89 -0
  142. package/src/workflows/loader.ts +186 -18
  143. package/src/workflows/schema.ts +79 -2
  144. package/src/workflows/store.ts +45 -2
  145. package/src/workflows/tool-input.ts +202 -0
  146. package/src/workflows/types.ts +374 -2
  147. package/examples/workflows/elegant-solution.workflow.ts +0 -95
@@ -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,
@@ -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 { isWorkflowDefinition } from "./definition.js";
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 { WorkflowDefinition, WorkflowSource } from "./types.js";
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: { "@osolmaz/pi-workflows": SELF_ENTRY },
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 name, stable built-in ref, or direct user file path. */
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(options.cwd, ref);
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
- if (match.source === "builtin") return await resolveWorkflowRef(match.ref, options, catalog);
146
- const absolutePath = path.resolve(match.ref);
147
- return {
148
- definition: await loadWorkflowFile(absolutePath),
149
- source: { kind: "file", path: absolutePath, hash: await hashWorkflowSource(absolutePath) },
150
- sourceKind: match.source,
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
- /** Resolve an already persisted canonical source. */
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
- return catalog.resolve(source, runId);
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
- throw new WorkflowSourceChangedError(runId);
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 {
@@ -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(definition: WorkflowDefinition): void {
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
- if (!NODE_ID_PATTERN.test(nodeId)) {
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
  }
@@ -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]) => [nodeId, snapshotNode(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(node: WorkflowNodeDefinition): WorkflowNodeSnapshot {
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
  }