@marcoscale98/pi-extensible-workflows 5.14.1-fork.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/README.md +64 -0
  3. package/dist/src/agent-execution.d.ts +338 -0
  4. package/dist/src/agent-execution.js +1633 -0
  5. package/dist/src/background-widget.d.ts +74 -0
  6. package/dist/src/background-widget.js +1340 -0
  7. package/dist/src/budget.d.ts +38 -0
  8. package/dist/src/budget.js +188 -0
  9. package/dist/src/changelog.d.ts +6 -0
  10. package/dist/src/changelog.js +112 -0
  11. package/dist/src/decoders.d.ts +96 -0
  12. package/dist/src/decoders.js +629 -0
  13. package/dist/src/execution.d.ts +19 -0
  14. package/dist/src/execution.js +749 -0
  15. package/dist/src/herdr.d.ts +53 -0
  16. package/dist/src/herdr.js +270 -0
  17. package/dist/src/host-delivery.d.ts +95 -0
  18. package/dist/src/host-delivery.js +376 -0
  19. package/dist/src/host-navigator.d.ts +83 -0
  20. package/dist/src/host-navigator.js +1087 -0
  21. package/dist/src/host-phases.d.ts +89 -0
  22. package/dist/src/host-phases.js +320 -0
  23. package/dist/src/host-recovery.d.ts +141 -0
  24. package/dist/src/host-recovery.js +356 -0
  25. package/dist/src/host-runtime.d.ts +78 -0
  26. package/dist/src/host-runtime.js +378 -0
  27. package/dist/src/host-view.d.ts +116 -0
  28. package/dist/src/host-view.js +868 -0
  29. package/dist/src/host.d.ts +40 -0
  30. package/dist/src/host.js +1641 -0
  31. package/dist/src/index.d.ts +29 -0
  32. package/dist/src/index.js +13725 -0
  33. package/dist/src/index.js.map +6 -0
  34. package/dist/src/io.d.ts +14 -0
  35. package/dist/src/io.js +82 -0
  36. package/dist/src/package-identity.d.ts +4 -0
  37. package/dist/src/package-identity.js +6 -0
  38. package/dist/src/paths.d.ts +14 -0
  39. package/dist/src/paths.js +51 -0
  40. package/dist/src/persistence.d.ts +5 -0
  41. package/dist/src/persistence.js +5 -0
  42. package/dist/src/pi-runtime-adapter.d.ts +27 -0
  43. package/dist/src/pi-runtime-adapter.js +243 -0
  44. package/dist/src/pi-runtime-runner.d.ts +42 -0
  45. package/dist/src/pi-runtime-runner.js +505 -0
  46. package/dist/src/registry.d.ts +48 -0
  47. package/dist/src/registry.js +379 -0
  48. package/dist/src/retention.d.ts +17 -0
  49. package/dist/src/retention.js +195 -0
  50. package/dist/src/runtime/agent-runner.d.ts +143 -0
  51. package/dist/src/runtime/agent-runner.js +12 -0
  52. package/dist/src/runtime/index.d.ts +2 -0
  53. package/dist/src/runtime/index.js +2 -0
  54. package/dist/src/runtime/namespace.d.ts +14 -0
  55. package/dist/src/runtime/namespace.js +1 -0
  56. package/dist/src/runtime/workflow-result.d.ts +2 -0
  57. package/dist/src/runtime/workflow-result.js +6 -0
  58. package/dist/src/session-handoff.d.ts +2 -0
  59. package/dist/src/session-handoff.js +82 -0
  60. package/dist/src/session-lease.d.ts +12 -0
  61. package/dist/src/session-lease.js +180 -0
  62. package/dist/src/store.d.ts +95 -0
  63. package/dist/src/store.js +951 -0
  64. package/dist/src/subagent-manager-handle.d.ts +4 -0
  65. package/dist/src/subagent-manager-handle.js +6 -0
  66. package/dist/src/tool-timing.d.ts +12 -0
  67. package/dist/src/tool-timing.js +33 -0
  68. package/dist/src/trajectory-contracts.d.ts +8 -0
  69. package/dist/src/trajectory-contracts.js +9 -0
  70. package/dist/src/trajectory-host-handle.d.ts +12 -0
  71. package/dist/src/trajectory-host-handle.js +6 -0
  72. package/dist/src/trajectory.d.ts +128 -0
  73. package/dist/src/trajectory.js +594 -0
  74. package/dist/src/types.d.ts +960 -0
  75. package/dist/src/types.js +60 -0
  76. package/dist/src/utils.d.ts +57 -0
  77. package/dist/src/utils.js +225 -0
  78. package/dist/src/validation.d.ts +29 -0
  79. package/dist/src/validation.js +1100 -0
  80. package/dist/src/workflow-artifacts.d.ts +14 -0
  81. package/dist/src/workflow-artifacts.js +40 -0
  82. package/dist/starter/index.d.ts +1 -0
  83. package/dist/starter/index.js +103 -0
  84. package/dist/starter/index.js.map +6 -0
  85. package/dist/starter/review-loop.d.ts +72 -0
  86. package/dist/starter/review-loop.js +43 -0
  87. package/dist/starter/roles/developer.md +6 -0
  88. package/dist/starter/roles/oracle.md +18 -0
  89. package/dist/starter/roles/researcher.md +20 -0
  90. package/dist/starter/roles/reviewer.md +19 -0
  91. package/dist/starter/roles/scout.md +18 -0
  92. package/dist/subagents/index.d.ts +8 -0
  93. package/dist/subagents/index.js +8021 -0
  94. package/dist/subagents/index.js.map +6 -0
  95. package/dist/subagents/src/contracts.d.ts +173 -0
  96. package/dist/subagents/src/contracts.js +68 -0
  97. package/dist/subagents/src/decode.d.ts +15 -0
  98. package/dist/subagents/src/decode.js +182 -0
  99. package/dist/subagents/src/index.d.ts +12 -0
  100. package/dist/subagents/src/index.js +180 -0
  101. package/dist/subagents/src/manager.d.ts +3 -0
  102. package/dist/subagents/src/manager.js +1636 -0
  103. package/dist/subagents/src/navigator.d.ts +5 -0
  104. package/dist/subagents/src/navigator.js +627 -0
  105. package/dist/subagents/src/view.d.ts +70 -0
  106. package/dist/subagents/src/view.js +387 -0
  107. package/dist/subagents/src/worktree.d.ts +34 -0
  108. package/dist/subagents/src/worktree.js +68 -0
  109. package/dist/trajectory/assets/favicon.png +0 -0
  110. package/dist/trajectory/assets/index.html +772 -0
  111. package/dist/trajectory/assets/marked.min.js +6 -0
  112. package/dist/trajectory/assets/morphdom.min.js +1 -0
  113. package/dist/trajectory/index.d.ts +5 -0
  114. package/dist/trajectory/index.js +3459 -0
  115. package/dist/trajectory/index.js.map +6 -0
  116. package/dist/trajectory/src/assets/favicon.png +0 -0
  117. package/dist/trajectory/src/assets/index.html +772 -0
  118. package/dist/trajectory/src/assets/marked.min.js +6 -0
  119. package/dist/trajectory/src/assets/morphdom.min.js +1 -0
  120. package/dist/trajectory/src/export.d.ts +25 -0
  121. package/dist/trajectory/src/export.js +80 -0
  122. package/dist/trajectory/src/index.d.ts +26 -0
  123. package/dist/trajectory/src/index.js +732 -0
  124. package/dist/trajectory/src/server.d.ts +7 -0
  125. package/dist/trajectory/src/server.js +2534 -0
  126. package/dist/trajectory/src/server.js.map +6 -0
  127. package/examples/advanced-patterns/auto-model-router.mjs +78 -0
  128. package/examples/workflow-extension-template/README.md +37 -0
  129. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  130. package/examples/workflow-extension-template/index.js +51 -0
  131. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  132. package/package.json +105 -0
  133. package/skills/pi-extensible-workflows/SKILL.md +160 -0
  134. package/src/agent-execution.ts +1533 -0
  135. package/src/background-widget.ts +1496 -0
  136. package/src/budget.ts +109 -0
  137. package/src/changelog.ts +114 -0
  138. package/src/decoders.ts +567 -0
  139. package/src/execution.ts +652 -0
  140. package/src/herdr.ts +240 -0
  141. package/src/host-delivery.ts +346 -0
  142. package/src/host-navigator.ts +871 -0
  143. package/src/host-phases.ts +264 -0
  144. package/src/host-recovery.ts +333 -0
  145. package/src/host-runtime.ts +373 -0
  146. package/src/host-view.ts +828 -0
  147. package/src/host.ts +1502 -0
  148. package/src/index.ts +29 -0
  149. package/src/io.ts +73 -0
  150. package/src/package-identity.ts +7 -0
  151. package/src/paths.ts +49 -0
  152. package/src/persistence.ts +11 -0
  153. package/src/pi-runtime-adapter.ts +247 -0
  154. package/src/pi-runtime-runner.ts +436 -0
  155. package/src/registry.ts +321 -0
  156. package/src/retention.ts +178 -0
  157. package/src/runtime/agent-runner.ts +158 -0
  158. package/src/runtime/index.ts +2 -0
  159. package/src/runtime/namespace.ts +16 -0
  160. package/src/runtime/workflow-result.ts +8 -0
  161. package/src/session-handoff.ts +78 -0
  162. package/src/session-lease.ts +125 -0
  163. package/src/store.ts +832 -0
  164. package/src/subagent-manager-handle.ts +8 -0
  165. package/src/tool-timing.ts +47 -0
  166. package/src/trajectory-contracts.ts +13 -0
  167. package/src/trajectory-host-handle.ts +16 -0
  168. package/src/trajectory.ts +543 -0
  169. package/src/types.ts +374 -0
  170. package/src/utils.ts +192 -0
  171. package/src/validation.ts +910 -0
  172. package/src/workflow-artifacts.ts +35 -0
  173. package/starter/index.ts +35 -0
  174. package/starter/prompts/council.md +16 -0
  175. package/starter/prompts/deep-research.md +16 -0
  176. package/starter/prompts/oracle.md +14 -0
  177. package/starter/prompts/parallel-review.md +22 -0
  178. package/starter/prompts/parallel-scout.md +16 -0
  179. package/starter/prompts/review-loop.md +14 -0
  180. package/starter/prompts/review.md +14 -0
  181. package/starter/prompts/scout.md +14 -0
  182. package/starter/review-loop.ts +73 -0
  183. package/starter/roles/developer.md +6 -0
  184. package/starter/roles/oracle.md +18 -0
  185. package/starter/roles/researcher.md +20 -0
  186. package/starter/roles/reviewer.md +19 -0
  187. package/starter/roles/scout.md +18 -0
  188. package/subagents/README.md +145 -0
  189. package/subagents/index.ts +8 -0
  190. package/subagents/src/contracts.ts +176 -0
  191. package/subagents/src/decode.ts +158 -0
  192. package/subagents/src/index.ts +194 -0
  193. package/subagents/src/manager.ts +1560 -0
  194. package/subagents/src/navigator.ts +488 -0
  195. package/subagents/src/view.ts +376 -0
  196. package/subagents/src/worktree.ts +99 -0
  197. package/trajectory/index.ts +5 -0
  198. package/trajectory/src/assets/favicon.png +0 -0
  199. package/trajectory/src/assets/index.html +772 -0
  200. package/trajectory/src/assets/marked.min.js +6 -0
  201. package/trajectory/src/assets/morphdom.min.js +1 -0
  202. package/trajectory/src/export.ts +85 -0
  203. package/trajectory/src/index.ts +604 -0
  204. package/trajectory/src/server.ts +447 -0
@@ -0,0 +1,356 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
3
+ import { budgetUsage, budgetRelaxed, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
4
+ import { aliasDrift, createLaunchSnapshot, errorCode, errorText, jsonValue, object } from "./utils.js";
5
+ import { LAUNCH_SNAPSHOT_IDENTITY_VERSION, WorkflowError, isExternallyEndedRunState } from "./types.js";
6
+ import { hostSessionContext, withWorkflowFunctions, withoutActiveShells, workflowRunContext } from "./host-runtime.js";
7
+ import { runWorkflow } from "./execution.js";
8
+ import { createWorkflowFailureDiagnostics, formatWorkflowFailureDelivery, formatWorkflowFailureDeliveryFallback, failureDiagnosticsFrom, completionDeliveryFromStore, incompleteRetryPaths, markWorkflowFailureDiagnostics, workflowFailedAt } from "./host-delivery.js";
9
+ function workflowRecoveryGuidance(action, state) {
10
+ if (action === "resume") {
11
+ if (state === "failed")
12
+ return "Failed workflow runs must use workflow_retry({ runId })";
13
+ if (state === "completed")
14
+ return "Completed workflow runs have no recovery action";
15
+ if (state === "stopped")
16
+ return "Stopped workflow runs have no recovery action; launch a new workflow";
17
+ if (state === "interrupted")
18
+ return "Interrupted workflow runs must be resumed from the interactive /workflow picker";
19
+ return `Only budget-exhausted runs can be resumed with workflow_resume; source is ${state}`;
20
+ }
21
+ if (state === "budget_exhausted")
22
+ return "Budget-exhausted workflow runs must use workflow_resume({ runId, budget? })";
23
+ if (state === "completed")
24
+ return "Completed workflow runs have no recovery action";
25
+ if (state === "stopped")
26
+ return "Stopped workflow runs cannot be retried; launch a new workflow";
27
+ if (state === "interrupted")
28
+ return "Interrupted workflow runs must be resumed from the interactive /workflow picker";
29
+ return `Only failed workflow runs can be retried; source is ${state}`;
30
+ }
31
+ /** A snapshot can only be re-executed when it carries the current identity scheme and every role its script references. */
32
+ function assertResumableSnapshot(snapshot, trustedProject) {
33
+ if (snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
34
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
35
+ if (snapshot.roles === undefined)
36
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow role definitions are missing from the launch snapshot");
37
+ if ((snapshot.projectRoles?.length ?? 0) > 0 && !trustedProject)
38
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Cannot restore project roles in an untrusted project");
39
+ const missingRole = snapshot.agentTypes.find((role) => !snapshot.roles?.[role]);
40
+ if (missingRole)
41
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Role definition is missing from the launch snapshot: ${missingRole}`);
42
+ }
43
+ function assertExpectedWorkflowState(expectedState, actualState) {
44
+ if (expectedState !== undefined && expectedState !== actualState)
45
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Workflow run state changed: expected state ${expectedState}, actual state ${actualState}`);
46
+ }
47
+ export function persistedFailure(run, error) { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
48
+ export function createWorkflowRecovery(deps) {
49
+ const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
50
+ const coordinateRunMutation = deps.coordinateRunMutation ?? ((task) => task());
51
+ const budgetDecisionDelivery = (metadata, request) => `Workflow ${metadata.name} budget adjustment ${request.proposalId} for run ${request.runId} requires approval. Consumed usage: ${JSON.stringify(request.consumed)}. Previous limits: ${JSON.stringify(request.previous)}. Proposed limits: ${JSON.stringify(request.proposed)}. Respond with workflow_respond using proposalId ${request.proposalId}.`;
52
+ const appendBudgetDecisionEvent = async (run, request, type) => {
53
+ run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
54
+ await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
55
+ };
56
+ const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal, waitForCompletion = true) => {
57
+ const run = runs.get(runId);
58
+ if (!run)
59
+ return undefined;
60
+ const request = await run.store.answerWorkflowDecision(proposalId, approved);
61
+ if (!request)
62
+ return undefined;
63
+ await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
64
+ const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
65
+ if (!silent)
66
+ deliver(`Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
67
+ return result;
68
+ };
69
+ const refreshPausedRunAliases = async (run, context) => {
70
+ const loaded = await run.store.load();
71
+ const trustedProject = context?.projectTrusted ?? run.projectTrusted();
72
+ const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
73
+ const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: run.abortController.signal, withPreflight: false });
74
+ await run.store.saveSnapshot(snapshot);
75
+ scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
76
+ scheduler.setRunExtensionSettings(run.store.runId, snapshot.settings.extensionSettings);
77
+ run.executor = createAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: activeSnapshotTools(snapshot.tools, "session"), resourceSelectors: currentPolicy.effective, extensionSettings: snapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: resumeRoles(run.store, snapshot, run.store.cwd, trustedProject).definitions, runStore: run.store, providerPause: async () => { deliver(`Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
78
+ run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
79
+ const drift = aliasDrift(previousAliases, currentAliases);
80
+ if (drift.length)
81
+ await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
82
+ };
83
+ const recoveryUi = (context) => {
84
+ const host = object(context) ? context : undefined;
85
+ const ui = host && object(host.ui) ? host.ui : {};
86
+ return { hasUI: host?.hasUI === true, ui };
87
+ };
88
+ const coldResumeRun = async (run, hasUI, ui, trustedProject, context, modeOverride, waitForCompletion = true) => {
89
+ const loaded = await run.store.load();
90
+ const foreground = modeOverride ?? (loaded.run.delivery?.mode === "foreground" || (loaded.run.delivery?.mode === "background" && loaded.run.delivery.toolCallId !== undefined) || (loaded.run.delivery === undefined && loaded.snapshot.launchMode === "foreground"));
91
+ if (loaded.run.activeShells !== undefined || loaded.run.activeShellStartedAt !== undefined || loaded.run.activeShellsByPhase !== undefined)
92
+ await persistRunState(run.store, run.metadata, withoutActiveShells);
93
+ await run.store.validateRetrySource();
94
+ await run.store.validateBorrowedWorktrees();
95
+ assertResumableSnapshot(loaded.snapshot, trustedProject);
96
+ const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
97
+ const controller = new AbortController();
98
+ if (context?.signal?.aborted)
99
+ controller.abort();
100
+ else {
101
+ context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true });
102
+ }
103
+ run.abortController = controller;
104
+ const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: controller.signal, ...(context?.resolvedAliases ? { resolvedAliases: context.resolvedAliases } : {}), ...(context?.blockedAliases ? { blockedAliases: context.blockedAliases } : {}), ...(context?.blockedAliasTargets ? { blockedAliasTargets: context.blockedAliasTargets } : {}), withPreflight: true });
105
+ if (!script)
106
+ throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
107
+ const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
108
+ await run.store.saveSnapshot(persistedSnapshot);
109
+ if (modeOverride !== undefined)
110
+ await persistRunState(run.store, run.metadata, (current) => ({ ...current, delivery: { ...(current.delivery ?? {}), mode: foreground ? "foreground" : "background", state: foreground ? "attached" : "pending" } }));
111
+ scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
112
+ scheduler.setRunExtensionSettings(run.store.runId, snapshot.settings.extensionSettings);
113
+ const roles = resumeRoles(run.store, persistedSnapshot, run.store.cwd, trustedProject);
114
+ run.executor = createAgentExecutor({ cwd: run.store.cwd, model: rootModel, tools: activeSnapshotTools(snapshot.tools, "session"), resourceSelectors: currentPolicy.effective, extensionSettings: snapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: roles.definitions, runStore: run.store, providerPause: async () => { deliver(`Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
115
+ const drift = aliasDrift(previousAliases, currentAliases);
116
+ if (drift.length)
117
+ await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
118
+ const runContext = workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, controller.signal);
119
+ run.executor.setRunContext(runContext);
120
+ await scheduler.cancelRun(run.store.runId);
121
+ await run.lifecycle.resume();
122
+ const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId, roles.capture), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
123
+ run.execution = execution;
124
+ const completion = execution.result.then(async (value) => {
125
+ await scheduler.flush(run.store.runId);
126
+ if (run.budget.hardExhausted)
127
+ throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
128
+ const resultPath = await run.store.saveResult(value);
129
+ const resultBytes = await run.store.resultBytes();
130
+ await run.lifecycle.terminal("completed", "completed");
131
+ await eventPublisher.runCompleted(run.store, run.metadata, resultPath);
132
+ return { value, resultPath, resultBytes };
133
+ }).catch(async (error) => {
134
+ await scheduler.flush(run.store.runId);
135
+ const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
136
+ if (!isExternallyEndedRunState(run.lifecycle.state))
137
+ await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
138
+ const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
139
+ const ended = run.lifecycle.state;
140
+ const state = isExternallyEndedRunState(ended) ? ended : "failed";
141
+ if (state === "failed")
142
+ retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
143
+ await eventPublisher.runFailed(run.store, run.metadata, typed, state);
144
+ run.update?.(workflowToolUpdate(persisted));
145
+ if (state === "failed") {
146
+ const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted);
147
+ markWorkflowFailureDiagnostics(typed, diagnostic);
148
+ }
149
+ throw typed;
150
+ }).finally(() => cleanupTerminalRun(run.store.runId));
151
+ run.completion = completion;
152
+ if (!foreground || !waitForCompletion) {
153
+ void completion.then(async (result) => {
154
+ await deliverTerminal(run.store, async () => (await completionDeliveryFromStore({ mode: "background", name: run.metadata.name, runId: run.store.runId, value: result.value, resultPath: result.resultPath, resultBytes: result.resultBytes, store: run.store, ...(context === undefined ? {} : { context: context.deliveryContext }) })).content);
155
+ }, async (error) => {
156
+ const diagnostic = failureDiagnosticsFrom(error);
157
+ await deliverTerminal(run.store, diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error), true);
158
+ });
159
+ return undefined;
160
+ }
161
+ const markDelivered = () => run.store.updateState((current) => current.delivery?.mode === "foreground" && (current.delivery.state === "attached" || current.delivery.state === "pending") ? { ...current, delivery: { ...current.delivery, state: "delivered" } } : current);
162
+ try {
163
+ const result = await completion;
164
+ await markDelivered();
165
+ const completionResult = await completionDeliveryFromStore({ mode: "foreground", name: run.metadata.name, runId: run.store.runId, value: result.value, resultPath: result.resultPath, resultBytes: result.resultBytes, store: run.store, ...(context === undefined ? {} : { context: context.deliveryContext }) });
166
+ return { ...result, completion: completionResult };
167
+ }
168
+ catch (error) {
169
+ await markDelivered();
170
+ throw error;
171
+ }
172
+ };
173
+ const applyBudgetDecision = async (request, approved, context, signal, waitForCompletion = true) => {
174
+ const run = runs.get(request.runId);
175
+ if (!run)
176
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
177
+ if (!approved)
178
+ return { state: "budget_exhausted", approved: false };
179
+ const nextBudget = validateBudget(request.proposed);
180
+ const nextVersion = request.budgetVersion + 1;
181
+ const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, request.consumed, run.budget.events, { active: false });
182
+ run.budget = runtime;
183
+ await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget)
184
+ next.budget = nextBudget;
185
+ else
186
+ delete next.budget; return next; });
187
+ const { hasUI, ui } = recoveryUi(context);
188
+ const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, request.foreground, waitForCompletion);
189
+ if (completed)
190
+ return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run, completion: completed.completion };
191
+ return { state: "running", approved: true };
192
+ };
193
+ const resumeWorkflowRun = async (runId, rawPatch, context, signal, modeOverride, waitForCompletion = true, expectedState) => {
194
+ const run = runs.get(runId);
195
+ if (!run) {
196
+ const { cwd, sessionId } = hostSessionContext(context);
197
+ if (cwd && sessionId) {
198
+ try {
199
+ const state = (await new RunStore(cwd, sessionId, runId, home).load()).run.state;
200
+ assertExpectedWorkflowState(expectedState, state);
201
+ throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", state));
202
+ }
203
+ catch (error) {
204
+ if (error instanceof WorkflowError)
205
+ throw error;
206
+ }
207
+ }
208
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session`);
209
+ }
210
+ const loaded = await run.store.load();
211
+ assertExpectedWorkflowState(expectedState, loaded.run.state);
212
+ if (loaded.run.state !== "budget_exhausted")
213
+ throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("resume", loaded.run.state));
214
+ const currentBudget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
215
+ const patch = rawPatch === undefined ? {} : validateBudgetPatch(rawPatch);
216
+ const nextBudget = mergeBudget(currentBudget, patch);
217
+ const usage = budgetUsage(loaded.run.usage);
218
+ if (!resumeBudgetAllowed(nextBudget, usage))
219
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Every exhausted hard budget must be raised above retained usage or removed");
220
+ if (budgetRelaxed(currentBudget, nextBudget)) {
221
+ const proposalId = randomUUID();
222
+ const request = { kind: "budget", proposalId, runId, consumed: usage, previous: currentBudget ?? {}, proposed: nextBudget ?? {}, budgetVersion: loaded.run.budgetVersion ?? 1, ...(modeOverride === undefined ? {} : { foreground: modeOverride }) };
223
+ await run.store.requestWorkflowDecision(request);
224
+ await appendBudgetDecisionEvent(run, request, "adjustment_requested");
225
+ deliver(budgetDecisionDelivery(run.metadata, request));
226
+ return { state: "awaiting_approval", proposalId };
227
+ }
228
+ const changed = JSON.stringify(currentBudget ?? {}) !== JSON.stringify(nextBudget ?? {});
229
+ if (changed) {
230
+ const nextVersion = (loaded.run.budgetVersion ?? 1) + 1;
231
+ const runtime = new WorkflowBudgetRuntime(nextBudget, nextVersion, usage, loaded.run.budgetEvents, { active: false });
232
+ run.budget = runtime;
233
+ await persistRunState(run.store, run.metadata, (current) => { const next = { ...current, ...runtime.snapshot(), budgetVersion: nextVersion }; if (nextBudget)
234
+ next.budget = nextBudget;
235
+ else
236
+ delete next.budget; return next; });
237
+ }
238
+ const { hasUI, ui } = recoveryUi(context);
239
+ const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
240
+ if (completed) {
241
+ const persistedRun = structuredClone((await run.store.load()).run);
242
+ if (!jsonValue(persistedRun))
243
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run is not JSON-compatible");
244
+ return { state: "completed", runId, value: completed.value, run: persistedRun, completion: { content: completed.completion.content, inlined: completed.completion.inlined } };
245
+ }
246
+ return { state: "running" };
247
+ };
248
+ const retryReservations = new Set();
249
+ const retryWorkflowRunUnlocked = async (runId, context, signal, expectedState) => {
250
+ if (typeof runId !== "string" || !runId.trim())
251
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
252
+ const host = object(context) ? context : {};
253
+ const { cwd, sessionId } = hostSessionContext(context);
254
+ if (!cwd || !sessionId)
255
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires the current project and Pi session");
256
+ await ensureSessionLease(cwd, sessionId);
257
+ const sourceStore = new RunStore(cwd, sessionId, runId, home);
258
+ let loaded;
259
+ try {
260
+ loaded = await sourceStore.load();
261
+ }
262
+ catch (error) {
263
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run ${runId} in the current project and Pi session: ${errorText(error)}`);
264
+ }
265
+ assertExpectedWorkflowState(expectedState, loaded.run.state);
266
+ if (loaded.run.state !== "failed")
267
+ throw new WorkflowError("RESUME_INCOMPATIBLE", workflowRecoveryGuidance("retry", loaded.run.state));
268
+ // decodeRetry() already guarantees the field shapes; only the semantic constraints remain to check here.
269
+ if (loaded.run.retry && (!loaded.run.retry.sourceRunId || !loaded.run.retry.lineageRootRunId))
270
+ throw new WorkflowError("RESUME_INCOMPATIBLE", "The source retry provenance is incomplete");
271
+ const lineageRootRunId = loaded.run.retry?.lineageRootRunId ?? loaded.run.id;
272
+ if (retryReservations.has(lineageRootRunId))
273
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `An active retry already owns lineage ${lineageRootRunId}`);
274
+ const activeStates = new Set(["queued", "running", "pausing", "paused", "awaiting_input", "interrupted", "budget_exhausted"]);
275
+ for (const candidateId of await listRunIds(cwd, sessionId, home)) {
276
+ if (candidateId === runId)
277
+ continue;
278
+ const candidate = new RunStore(cwd, sessionId, candidateId, home);
279
+ try {
280
+ const candidateRun = (await candidate.load()).run;
281
+ if (activeStates.has(candidateRun.state) && candidateRun.retry?.lineageRootRunId === lineageRootRunId)
282
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `An active retry child already exists for source lineage ${lineageRootRunId}`);
283
+ }
284
+ catch (error) {
285
+ if (error instanceof WorkflowError && error.code === "RESUME_INCOMPATIBLE")
286
+ throw error;
287
+ }
288
+ }
289
+ retryReservations.add(lineageRootRunId);
290
+ let childCreated = false;
291
+ try {
292
+ const trustedProject = projectTrusted(context);
293
+ await sourceStore.validateRetrySource();
294
+ await sourceStore.validateBorrowedWorktrees();
295
+ assertResumableSnapshot(loaded.snapshot, trustedProject);
296
+ const modelRegistry = resumeHostContext(context).modelRegistry;
297
+ const hostModel = object(host.model) && typeof host.model.provider === "string" && typeof host.model.id === "string" ? { provider: host.model.provider, id: host.model.id } : { provider: "", id: "" };
298
+ const rootModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
299
+ const { active, settingsPath, currentPolicy, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot: childBaseSnapshot } = await resolveLaunchPrologue({ snapshot: loaded.snapshot, cwd, trustedProject, rootModel, ...(modelRegistry ? { modelRegistry } : {}), signal: signal ?? new AbortController().signal, withPreflight: true });
300
+ await sourceStore.validateNamedWorktrees();
301
+ for (const name of loaded.run.retry?.namedWorktrees ?? [])
302
+ await sourceStore.resolveNamedWorktree(name);
303
+ const completedPaths = (await sourceStore.replayableOperations()).map(({ path }) => path);
304
+ const incompletePaths = incompleteRetryPaths([...(loaded.run.retry?.incompletePaths ?? []), ...loaded.run.agents.filter((agent) => agent.state !== "completed").map((agent) => operationPath("agent", ...(agent.structuralPath ?? [])))], completedPaths);
305
+ const namedWorktrees = [...new Set([...(loaded.run.retry?.namedWorktrees ?? []), ...(await sourceStore.worktrees()).filter(({ owner }) => owner.startsWith(`${operationPath("worktree", "named")}/`)).map(({ owner }) => decodeURIComponent(owner.split("/").at(-1) ?? owner))])];
306
+ const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
307
+ const childRunId = randomUUID();
308
+ const childStore = new RunStore(cwd, sessionId, childRunId, home);
309
+ const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
310
+ const childInitialBudget = childBudget.snapshot();
311
+ const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
312
+ await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [], agentSessions: [], ...(budget ? { budget } : {}), budgetVersion: loaded.run.budgetVersion ?? 1, ...childInitialBudget }, childBaseSnapshot);
313
+ const model = modelSpec(loaded.snapshot.models[0] ?? "", rootModel);
314
+ const lifecycle = lifecycleFor(childStore, "interrupted", loaded.snapshot.metadata);
315
+ const abortController = new AbortController();
316
+ const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
317
+ const providerPause = async () => { deliver(`Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
318
+ const childRun = { executor: createAgentExecutor({ cwd, model, tools: activeSnapshotTools(loaded.snapshot.tools, active), resourceSelectors: currentPolicy.effective, extensionSettings: childBaseSnapshot.settings.extensionSettings, availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: loaded.snapshot.roles ?? {}, runStore: childStore, providerPause, agentResourcePolicy: frozenResourcePolicy(currentPolicy) }), store: childStore, metadata: loaded.snapshot.metadata, model, lifecycle, budget: childBudget, abortController, projectTrusted: () => projectTrusted(context), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) };
319
+ runs.set(childRunId, childRun);
320
+ childCreated = true;
321
+ return { childRun, parentRunId: loaded.run.id, lineageRootRunId, concurrency: loaded.snapshot.settings.concurrency, ...(childBaseSnapshot.settings.extensionSettings === undefined ? {} : { extensionSettings: childBaseSnapshot.settings.extensionSettings }), hostModel, modelRegistry, currentAliases, blockedAliases, blockedAliasTargets };
322
+ }
323
+ finally {
324
+ if (!childCreated)
325
+ retryReservations.delete(lineageRootRunId);
326
+ }
327
+ };
328
+ const retryWorkflowRun = async (runId, context, signal, modeOverride, expectedState) => {
329
+ const prepared = await coordinateRunMutation(() => retryWorkflowRunUnlocked(runId, context, signal, expectedState));
330
+ let childStarted = false;
331
+ try {
332
+ scheduler.addRun(prepared.childRun.store.runId, prepared.concurrency, () => { prepared.childRun.budget.checkAgentLaunch(); }, prepared.extensionSettings);
333
+ await eventPublisher.runStarted(prepared.childRun.store, prepared.childRun.metadata);
334
+ const { hasUI, ui } = recoveryUi(context);
335
+ const recoveryContext = resumeHostContext(context);
336
+ const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
337
+ const completion = runs.get(prepared.childRun.store.runId)?.completion;
338
+ if (completion) {
339
+ childStarted = true;
340
+ void completion.then(() => { retryReservations.delete(prepared.lineageRootRunId); }, () => { retryReservations.delete(prepared.lineageRootRunId); });
341
+ }
342
+ else if (completed) {
343
+ childStarted = true;
344
+ retryReservations.delete(prepared.lineageRootRunId);
345
+ }
346
+ if (completed)
347
+ return { runId: prepared.childRun.store.runId, parentRunId: prepared.parentRunId, state: "completed", value: completed.value, run: (await prepared.childRun.store.load()).run, completion: completed.completion };
348
+ return { runId: prepared.childRun.store.runId, parentRunId: prepared.parentRunId, state: "running" };
349
+ }
350
+ finally {
351
+ if (!childStarted)
352
+ retryReservations.delete(prepared.lineageRootRunId);
353
+ }
354
+ };
355
+ return { refreshPausedRunAliases, coldResumeRun, applyBudgetDecision, answerBudgetDecision, budgetDecisionDelivery, resumeWorkflowRun, retryWorkflowRun };
356
+ }
@@ -0,0 +1,78 @@
1
+ import { type AgentProviderFailure, type AgentProviderRecovery, WorkflowAgentExecutor } from "./agent-execution.js";
2
+ import { type WorkflowBudgetRuntime } from "./budget.js";
3
+ import { type PersistedRun, type RunStore, type WorktreeReference } from "./persistence.js";
4
+ import { type WorkflowRegistryApi } from "./registry.js";
5
+ import { type AgentRecord, type BudgetEvent, type JsonValue, type ModelSpec, type RunState, type WorkflowBridge, type WorkflowCheckpointState, type WorkflowExecution, type WorkflowExtensionSettings, type WorkflowMetadata, type WorkflowRunContext } from "./types.js";
6
+ export type WorkflowEventSink = {
7
+ emit: (name: string, payload: unknown) => unknown;
8
+ };
9
+ export type WorkflowToolUpdate = {
10
+ content: [{
11
+ type: "text";
12
+ text: string;
13
+ }];
14
+ details: {
15
+ runId: string;
16
+ run: PersistedRun;
17
+ };
18
+ };
19
+ export type WorkflowRunRecord = {
20
+ executor: WorkflowAgentExecutor;
21
+ store: RunStore;
22
+ metadata: WorkflowMetadata;
23
+ model: ModelSpec;
24
+ lifecycle: RunLifecycle;
25
+ budget: WorkflowBudgetRuntime;
26
+ abortController: AbortController;
27
+ projectTrusted: () => boolean;
28
+ providerErrorRecovery?: (failure: AgentProviderFailure) => Promise<AgentProviderRecovery>;
29
+ execution?: WorkflowExecution;
30
+ completion?: Promise<unknown>;
31
+ foreground?: boolean;
32
+ checkpointResolvers: Map<string, (value: boolean) => void>;
33
+ update?: (result: WorkflowToolUpdate) => void;
34
+ };
35
+ /** Drops the transient shell-activity counters a run only holds while its host process is alive. */
36
+ export declare function withoutActiveShells(run: PersistedRun): PersistedRun;
37
+ /** Reads the project cwd and Pi session ID that an extension context exposes, when present. */
38
+ export declare function hostSessionContext(context: unknown): {
39
+ cwd?: string;
40
+ sessionId?: string;
41
+ };
42
+ export declare class RunLifecycle {
43
+ #private;
44
+ private readonly changed?;
45
+ constructor(state?: RunState, changed?: ((state: RunState, previousState: RunState, reason?: string) => void | Promise<void>) | undefined);
46
+ get state(): RunState;
47
+ enter(): Promise<void>;
48
+ leave(): Promise<void>;
49
+ enterAwaitingInput(): Promise<void>;
50
+ resolveAwaitingInput(): Promise<void>;
51
+ pause(): Promise<void>;
52
+ /** Resumes a paused, interrupted, or budget-exhausted run; on a `pausing` run it cancels the pending pause instead. */
53
+ resume(): Promise<void>;
54
+ providerPause(): Promise<void>;
55
+ terminal(state: "completed" | "failed" | "stopped" | "interrupted" | "budget_exhausted", reason?: string): Promise<void>;
56
+ }
57
+ export declare class WorkflowEventPublisher {
58
+ #private;
59
+ private readonly sink;
60
+ constructor(sink: WorkflowEventSink | undefined);
61
+ removeRun(runId: string): void;
62
+ seedBudget(runId: string, events: readonly BudgetEvent[] | undefined): void;
63
+ runStarted(store: RunStore, metadata: WorkflowMetadata): Promise<void>;
64
+ runResumed(store: RunStore, metadata: WorkflowMetadata): Promise<void>;
65
+ runState(store: RunStore, metadata: WorkflowMetadata, previousState: RunState, state: RunState, reason?: string): Promise<void>;
66
+ runCompleted(store: RunStore, metadata: WorkflowMetadata, resultPath: string): Promise<void>;
67
+ runFailed(store: RunStore, metadata: WorkflowMetadata, error: unknown, state: "failed" | "stopped" | "interrupted" | "budget_exhausted"): Promise<void>;
68
+ agentState(store: RunStore, metadata: WorkflowMetadata, previous: AgentRecord | undefined, agent: AgentRecord): Promise<void>;
69
+ agentStates(store: RunStore, metadata: WorkflowMetadata, previous: readonly AgentRecord[], current: readonly AgentRecord[]): Promise<void>;
70
+ phase(store: RunStore, metadata: WorkflowMetadata, previousPhase: string | undefined, phase: string): Promise<void>;
71
+ checkpoint(store: RunStore, metadata: WorkflowMetadata, name: string, state: WorkflowCheckpointState): Promise<void>;
72
+ budget(store: RunStore, metadata: WorkflowMetadata, run: Pick<PersistedRun, "budgetEvents">): Promise<void>;
73
+ worktree(store: RunStore, metadata: WorkflowMetadata, worktree: WorktreeReference): Promise<void>;
74
+ private budgetKey;
75
+ }
76
+ export declare function workflowRunContext(cwd: string, sessionId: string, runId: string, workflow: WorkflowMetadata, args: JsonValue, signal: AbortSignal): Readonly<WorkflowRunContext>;
77
+ export declare function nextNamedOccurrence(counters: Map<string, number>, label: string): string;
78
+ export declare function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, runContext: Readonly<WorkflowRunContext>, registry: WorkflowRegistryApi, settings?: Readonly<WorkflowExtensionSettings>): WorkflowBridge;