@osolmaz/pi-workflows 0.16.4 → 0.16.6

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 (173) hide show
  1. package/README.md +5 -5
  2. package/dist/builtins/autoimplement.workflow.d.ts +5 -1
  3. package/dist/builtins/autoimplement.workflow.js +29 -10
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/client/client.js +3 -1
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/client/protocol.d.ts +1 -1
  8. package/dist/client/protocol.js +18 -1
  9. package/dist/client/protocol.js.map +1 -1
  10. package/dist/client/view.d.ts +7 -5
  11. package/dist/extension/index.d.ts +2 -1
  12. package/dist/extension/index.js +115 -165
  13. package/dist/extension/index.js.map +1 -1
  14. package/dist/extension/recorder.d.ts +1 -1
  15. package/dist/extension/recorder.js +2 -2
  16. package/dist/extension/recorder.js.map +1 -1
  17. package/dist/extension/step-message.d.ts +1 -1
  18. package/dist/extension/step-message.js +4 -4
  19. package/dist/extension/step-message.js.map +1 -1
  20. package/dist/extension/workflow-message-coordinator.d.ts +12 -1
  21. package/dist/extension/workflow-message-coordinator.js +65 -54
  22. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  23. package/dist/resource-managers/index.d.ts +1 -2
  24. package/dist/resource-managers/index.js +1 -2
  25. package/dist/resource-managers/index.js.map +1 -1
  26. package/dist/resource-managers/sqlite.d.ts +2 -295
  27. package/dist/resource-managers/sqlite.js +71 -1528
  28. package/dist/resource-managers/sqlite.js.map +1 -1
  29. package/dist/resource-managers/types.d.ts +2 -7
  30. package/dist/server/resource-runner-entry.js +2 -6
  31. package/dist/server/resource-runner-entry.js.map +1 -1
  32. package/dist/server/rpc-bridge.js +1 -1
  33. package/dist/server/rpc-bridge.js.map +1 -1
  34. package/dist/server/rpc-executor.js +2 -4
  35. package/dist/server/rpc-executor.js.map +1 -1
  36. package/dist/server/server-entry.js +3 -0
  37. package/dist/server/server-entry.js.map +1 -1
  38. package/dist/server/server.d.ts +22 -8
  39. package/dist/server/server.js +498 -543
  40. package/dist/server/server.js.map +1 -1
  41. package/dist/server/state.d.ts +16 -29
  42. package/dist/server/state.js +105 -319
  43. package/dist/server/state.js.map +1 -1
  44. package/dist/server/view.d.ts +4 -5
  45. package/dist/server/view.js +51 -51
  46. package/dist/server/view.js.map +1 -1
  47. package/dist/server/workflow-runner-entry.d.ts +1 -1
  48. package/dist/server/workflow-runner-entry.js +1 -49
  49. package/dist/server/workflow-runner-entry.js.map +1 -1
  50. package/dist/server/workflow-runner-protocol.d.ts +2 -7
  51. package/dist/server/workflow-runner-protocol.js +5 -10
  52. package/dist/server/workflow-runner-protocol.js.map +1 -1
  53. package/dist/server/workflow-runner-store.d.ts +5 -6
  54. package/dist/server/workflow-runner-store.js +7 -14
  55. package/dist/server/workflow-runner-store.js.map +1 -1
  56. package/dist/state/attempt-time.d.ts +21 -0
  57. package/dist/state/attempt-time.js +95 -0
  58. package/dist/state/attempt-time.js.map +1 -0
  59. package/dist/state/json.d.ts +2 -1
  60. package/dist/state/json.js.map +1 -1
  61. package/dist/state/project-store.d.ts +39 -0
  62. package/dist/state/project-store.js +154 -0
  63. package/dist/state/project-store.js.map +1 -0
  64. package/dist/state/prune.d.ts +28 -0
  65. package/dist/state/prune.js +278 -83
  66. package/dist/state/prune.js.map +1 -1
  67. package/dist/state/schema.js +23 -23
  68. package/dist/state/workflow-messages.d.ts +1 -1
  69. package/dist/state/workflow-messages.js +3 -1
  70. package/dist/state/workflow-messages.js.map +1 -1
  71. package/dist/viewer/backup.js +5 -0
  72. package/dist/viewer/backup.js.map +1 -1
  73. package/dist/workflows/diagnostics.d.ts +8 -0
  74. package/dist/workflows/diagnostics.js +61 -0
  75. package/dist/workflows/diagnostics.js.map +1 -0
  76. package/dist/workflows/engine.d.ts +2 -15
  77. package/dist/workflows/engine.js +125 -245
  78. package/dist/workflows/engine.js.map +1 -1
  79. package/dist/workflows/human-decision.d.ts +3 -7
  80. package/dist/workflows/human-decision.js +2 -56
  81. package/dist/workflows/human-decision.js.map +1 -1
  82. package/dist/workflows/index.d.ts +2 -1
  83. package/dist/workflows/index.js +1 -0
  84. package/dist/workflows/index.js.map +1 -1
  85. package/dist/workflows/queue.d.ts +267 -0
  86. package/dist/workflows/queue.js +1248 -0
  87. package/dist/workflows/queue.js.map +1 -0
  88. package/dist/workflows/requests.d.ts +47 -0
  89. package/dist/workflows/requests.js +158 -0
  90. package/dist/workflows/requests.js.map +1 -0
  91. package/dist/workflows/schema.js +2 -4
  92. package/dist/workflows/schema.js.map +1 -1
  93. package/dist/workflows/store.d.ts +11 -16
  94. package/dist/workflows/store.js +174 -169
  95. package/dist/workflows/store.js.map +1 -1
  96. package/dist/workflows/tool-input.d.ts +6 -9
  97. package/dist/workflows/tool-input.js +15 -12
  98. package/dist/workflows/tool-input.js.map +1 -1
  99. package/dist/workflows/transitions.d.ts +38 -0
  100. package/dist/workflows/transitions.js +276 -0
  101. package/dist/workflows/transitions.js.map +1 -0
  102. package/dist/workflows/types.d.ts +9 -42
  103. package/dist/workflows/workflow-message-content.d.ts +7 -1
  104. package/dist/workflows/workflow-message-content.js +22 -4
  105. package/dist/workflows/workflow-message-content.js.map +1 -1
  106. package/docs/2026-08-25-workflow-follow-ups.md +5 -5
  107. package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
  108. package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
  109. package/docs/2026-09-06-durable-execution-plan.md +176 -0
  110. package/docs/DEFERRED_TURNS.md +84 -105
  111. package/docs/HUMAN_DECISIONS.md +17 -19
  112. package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
  113. package/docs/SQLITE_STATE.md +87 -30
  114. package/docs/WORKFLOW_SERVER.md +50 -11
  115. package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
  116. package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
  117. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
  118. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
  119. package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
  120. package/docs/workflows.md +109 -44
  121. package/examples/workflows/autoresearch.workflow.ts +0 -2
  122. package/examples/workflows/branch.workflow.ts +0 -4
  123. package/examples/workflows/echo.workflow.ts +0 -2
  124. package/examples/workflows/live-settings.workflow.ts +0 -2
  125. package/examples/workflows/two-turn.workflow.ts +0 -2
  126. package/herdr-plugin.toml +1 -1
  127. package/package.json +1 -1
  128. package/protocol/fixtures/client-v1.json +1 -1
  129. package/skills/pi-workflows/SKILL.md +8 -7
  130. package/src/builtins/autoimplement.workflow.ts +31 -10
  131. package/src/client/client.ts +3 -1
  132. package/src/client/protocol.ts +21 -1
  133. package/src/client/view.ts +9 -5
  134. package/src/extension/index.ts +117 -189
  135. package/src/extension/recorder.ts +2 -2
  136. package/src/extension/step-message.ts +4 -4
  137. package/src/extension/workflow-message-coordinator.ts +78 -50
  138. package/src/resource-managers/index.ts +1 -9
  139. package/src/resource-managers/sqlite.ts +330 -2655
  140. package/src/resource-managers/types.ts +2 -3
  141. package/src/server/resource-runner-entry.ts +2 -7
  142. package/src/server/rpc-bridge.ts +1 -1
  143. package/src/server/rpc-executor.ts +4 -9
  144. package/src/server/server-entry.ts +3 -0
  145. package/src/server/server.ts +568 -644
  146. package/src/server/state.ts +122 -466
  147. package/src/server/view.ts +56 -60
  148. package/src/server/workflow-runner-entry.ts +2 -55
  149. package/src/server/workflow-runner-protocol.ts +5 -15
  150. package/src/server/workflow-runner-store.ts +12 -27
  151. package/src/state/attempt-time.ts +122 -0
  152. package/src/state/json.ts +2 -1
  153. package/src/state/project-store.ts +218 -0
  154. package/src/state/prune.ts +408 -95
  155. package/src/state/schema.ts +23 -23
  156. package/src/state/workflow-messages.ts +4 -2
  157. package/src/viewer/backup.ts +7 -0
  158. package/src/workflows/diagnostics.ts +78 -0
  159. package/src/workflows/engine.ts +133 -291
  160. package/src/workflows/human-decision.ts +3 -81
  161. package/src/workflows/index.ts +8 -2
  162. package/src/workflows/queue.ts +2091 -0
  163. package/src/workflows/requests.ts +283 -0
  164. package/src/workflows/schema.ts +2 -6
  165. package/src/workflows/store.ts +223 -231
  166. package/src/workflows/tool-input.ts +20 -12
  167. package/src/workflows/transitions.ts +330 -0
  168. package/src/workflows/types.ts +9 -46
  169. package/src/workflows/workflow-message-content.ts +28 -4
  170. package/dist/resource-managers/runtime.d.ts +0 -59
  171. package/dist/resource-managers/runtime.js +0 -423
  172. package/dist/resource-managers/runtime.js.map +0 -1
  173. package/src/resource-managers/runtime.ts +0 -576
@@ -13,13 +13,12 @@ const inputSchema = Type.Unknown({
13
13
  });
14
14
  const runIdSchema = Type.String({
15
15
  description:
16
- "Run id; required for restart and optional for status, cancel, answer, settings, and follow-ups",
16
+ "Run id; required for restart and optional for status, cancel, settings, and follow-ups",
17
17
  });
18
- const stepSchema = Type.String({
19
- description: "Workflow step id; required when action is update or submit",
20
- });
21
- const attemptSchema = Type.String({
22
- description: "Workflow attempt id; required when action is update or submit",
18
+ const requestIdSchema = Type.String({
19
+ minLength: 1,
20
+ description:
21
+ "Exact durable request id from the step contract or checkpoint; required for answer, update, and submit",
23
22
  });
24
23
  const updateSchema = Type.Object(
25
24
  {
@@ -45,7 +44,18 @@ export const WorkflowActionSchemas = {
45
44
  },
46
45
  noExtraProperties,
47
46
  ),
48
- restart: Type.Object({ action: Type.Literal("restart"), runId: runIdSchema }, noExtraProperties),
47
+ restart: Type.Object(
48
+ {
49
+ action: Type.Literal("restart"),
50
+ runId: runIdSchema,
51
+ expectedRevision: Type.Integer({
52
+ minimum: 0,
53
+ description:
54
+ "Use runRevision from the terminal run view, not its presentation revision. Restart only when the user explicitly requests a fresh run.",
55
+ }),
56
+ },
57
+ noExtraProperties,
58
+ ),
49
59
  "change-settings": Type.Object(
50
60
  {
51
61
  action: Type.Literal("change-settings"),
@@ -85,16 +95,15 @@ export const WorkflowActionSchemas = {
85
95
  answer: Type.Object(
86
96
  {
87
97
  action: Type.Literal("answer"),
98
+ requestId: requestIdSchema,
88
99
  input: inputSchema,
89
- runId: Type.Optional(runIdSchema),
90
100
  },
91
101
  noExtraProperties,
92
102
  ),
93
103
  update: Type.Object(
94
104
  {
95
105
  action: Type.Literal("update"),
96
- step: stepSchema,
97
- attempt: attemptSchema,
106
+ requestId: requestIdSchema,
98
107
  update: updateSchema,
99
108
  },
100
109
  noExtraProperties,
@@ -102,8 +111,7 @@ export const WorkflowActionSchemas = {
102
111
  submit: Type.Object(
103
112
  {
104
113
  action: Type.Literal("submit"),
105
- step: stepSchema,
106
- attempt: attemptSchema,
114
+ requestId: requestIdSchema,
107
115
  output: outputSchema,
108
116
  },
109
117
  noExtraProperties,
@@ -0,0 +1,330 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import type { JsonValue } from "../state/json.js";
3
+ import { resolveNext, resolveNextForOutcome } from "./graph.js";
4
+ import type {
5
+ WorkflowDefinitionSnapshot,
6
+ WorkflowRunState,
7
+ WorkflowStepRecord,
8
+ WorkflowTraceEventDraft,
9
+ } from "./types.js";
10
+
11
+ /** Worker proposals carry one change, never a replacement run projection. */
12
+ export type WorkflowTransition = {
13
+ event: WorkflowTraceEventDraft;
14
+ } & (
15
+ | { kind: "startAttempt"; startedAt: string }
16
+ | { kind: "finishAttempt"; step: WorkflowStepRecord }
17
+ | { kind: "setTimeout"; timeoutMs: number | null }
18
+ | { kind: "resume"; attemptId?: string }
19
+ | { kind: "pause" }
20
+ | { kind: "resumeInteraction" }
21
+ | {
22
+ kind: "waitForInput";
23
+ requestId: string;
24
+ requestKind: "checkpoint" | "decision";
25
+ contract: JsonValue;
26
+ }
27
+ | {
28
+ kind: "finish";
29
+ status: "completed" | "failed" | "timed_out" | "cancelled";
30
+ error?: string;
31
+ finalOutput?: unknown;
32
+ }
33
+ | { kind: "record" }
34
+ );
35
+
36
+ /** Translate engine-local state into the narrow proposal for this event. */
37
+ export function executionTransition(
38
+ state: WorkflowRunState,
39
+ event: WorkflowTraceEventDraft,
40
+ ): WorkflowTransition {
41
+ switch (event.type) {
42
+ case "node_started":
43
+ if (state.currentNodeStartedAt === undefined)
44
+ throw new Error("Attempt start time is missing");
45
+ return { kind: "startAttempt", startedAt: state.currentNodeStartedAt, event };
46
+ case "node_finished":
47
+ case "node_failed": {
48
+ const step = state.steps.at(-1);
49
+ if (step === undefined || step.attemptId !== event.attemptId) {
50
+ throw new Error("Finished attempt is missing from engine state");
51
+ }
52
+ return { kind: "finishAttempt", step, event };
53
+ }
54
+ case "node_timeout_set":
55
+ return { kind: "setTimeout", timeoutMs: state.currentNodeTimeoutMs ?? null, event };
56
+ case "run_resumed":
57
+ return {
58
+ kind: "resume",
59
+ ...(state.currentAttemptId === undefined ? {} : { attemptId: state.currentAttemptId }),
60
+ event,
61
+ };
62
+ case "run_paused":
63
+ return { kind: "pause", event };
64
+ case "interaction_validation_started":
65
+ case "interaction_accepted":
66
+ return { kind: "resumeInteraction", event };
67
+ case "run_completed":
68
+ case "run_failed":
69
+ case "run_interrupted":
70
+ case "run_timed_out":
71
+ case "run_cancelled":
72
+ if (state.status === "running" || state.status === "waiting")
73
+ throw new Error("Terminal transition has unfinished execution");
74
+ return {
75
+ kind: "finish",
76
+ status: state.status,
77
+ ...(state.error === undefined ? {} : { error: state.error }),
78
+ ...(state.finalOutput === undefined ? {} : { finalOutput: state.finalOutput }),
79
+ event,
80
+ };
81
+ default:
82
+ return { kind: "record", event };
83
+ }
84
+ }
85
+
86
+ const recordEvents = new Set([
87
+ "run_started",
88
+ "include_entered",
89
+ "include_exited",
90
+ "settings_route_retried",
91
+ "agent_session_required",
92
+ "agent_prompt_sent",
93
+ "interaction_resume_prepared",
94
+ ]);
95
+
96
+ /** Apply a validated transition to a projection read by the durable owner. */
97
+ export function applyExecutionTransition(
98
+ current: WorkflowRunState,
99
+ definition: WorkflowDefinitionSnapshot,
100
+ transition: WorkflowTransition,
101
+ now: string,
102
+ ): WorkflowRunState {
103
+ if (
104
+ current.status !== "running" &&
105
+ !(current.status === "waiting" && transition.kind === "resumeInteraction")
106
+ ) {
107
+ throw new Error(`Run ${current.runId} cannot execute with status ${current.status}`);
108
+ }
109
+ const state = structuredClone(current);
110
+ const event = transition.event;
111
+ if (typeof event !== "object" || event === null || typeof event.type !== "string") {
112
+ throw new Error("Execution transition event is invalid");
113
+ }
114
+ switch (transition.kind) {
115
+ case "startAttempt": {
116
+ requireEvent(event, "node_started");
117
+ const nodeId = requireIdentity(event.nodeId, "node");
118
+ const attemptId = requireIdentity(event.attemptId, "attempt");
119
+ const node = definition.nodes[nodeId];
120
+ if (node === undefined || node.nodeType !== event.payload.nodeType) {
121
+ throw new Error("Attempt node does not match the workflow definition");
122
+ }
123
+ if (state.steps.some((step) => step.attemptId === attemptId)) {
124
+ throw new Error("A completed attempt cannot start again");
125
+ }
126
+ if (state.currentAttemptId !== undefined) {
127
+ throw new Error("Another attempt is already active");
128
+ }
129
+ if (nextNode(definition, state) !== nodeId) {
130
+ throw new Error("Attempt does not follow the accepted graph route");
131
+ }
132
+ if (!Number.isFinite(Date.parse(transition.startedAt)))
133
+ throw new Error("Invalid attempt time");
134
+ state.currentNode = nodeId;
135
+ state.currentAttemptId = attemptId;
136
+ state.currentNodeStartedAt = transition.startedAt;
137
+ setSettings(state, event.payload);
138
+ if (node.statusDetail !== undefined) state.statusDetail = node.statusDetail;
139
+ break;
140
+ }
141
+ case "finishAttempt": {
142
+ const step = transition.step;
143
+ requireEvent(event, step.outcome === "ok" ? "node_finished" : "node_failed");
144
+ requireAttempt(state, event);
145
+ if (
146
+ step.attemptId !== event.attemptId ||
147
+ step.nodeId !== event.nodeId ||
148
+ step.nodeType !== definition.nodes[step.nodeId]?.nodeType ||
149
+ step.startedAt !== state.currentNodeStartedAt ||
150
+ step.outcome !== event.payload.outcome ||
151
+ !isDeepStrictEqual(step.output, event.payload.output ?? null)
152
+ ) {
153
+ throw new Error("Attempt result does not match the active request");
154
+ }
155
+ if (state.steps.some((prior) => prior.attemptId === step.attemptId)) {
156
+ throw new Error("Attempt result was already committed");
157
+ }
158
+ state.steps.push(structuredClone(step));
159
+ state.results[step.nodeId] = {
160
+ ...step,
161
+ durationMs: Date.parse(step.finishedAt) - Date.parse(step.startedAt),
162
+ };
163
+ if (step.outcome === "ok") state.outputs[step.nodeId] = step.output;
164
+ clearAttempt(state);
165
+ break;
166
+ }
167
+ case "setTimeout":
168
+ requireEvent(event, "node_timeout_set");
169
+ requireAttempt(state, event);
170
+ if (
171
+ transition.timeoutMs !== null &&
172
+ (!Number.isFinite(transition.timeoutMs) || transition.timeoutMs <= 0)
173
+ ) {
174
+ throw new Error("Invalid attempt timeout");
175
+ }
176
+ if (state.currentNodeElapsedMs !== undefined)
177
+ throw new Error("Attempt timeout was already configured");
178
+ state.currentNodeTimeoutMs = transition.timeoutMs;
179
+ state.currentNodeElapsedMs = 0;
180
+ break;
181
+ case "resume":
182
+ requireEvent(event, "run_resumed");
183
+ if (transition.attemptId !== undefined && transition.attemptId !== state.currentAttemptId) {
184
+ throw new Error("Resume does not name the active attempt");
185
+ }
186
+ if (transition.attemptId === undefined) clearAttempt(state);
187
+ delete state.paused;
188
+ break;
189
+ case "waitForInput":
190
+ requireEvent(event, "checkpoint_requested");
191
+ requireAttempt(state, event);
192
+ if (definition.nodes[state.currentNode ?? ""]?.nodeType !== "checkpoint") {
193
+ throw new Error("Only a checkpoint can request checkpoint input");
194
+ }
195
+ state.status = "waiting";
196
+ state.waitingOn = requireIdentity(event.nodeId, "node");
197
+ state.statusDetail =
198
+ transition.requestKind === "decision"
199
+ ? "waiting for a human decision"
200
+ : "waiting for checkpoint input";
201
+ delete state.finishedAt;
202
+ break;
203
+ case "resumeInteraction":
204
+ if (
205
+ event.type !== "interaction_validation_started" &&
206
+ event.type !== "interaction_accepted"
207
+ ) {
208
+ throw new Error("Invalid interaction resume event");
209
+ }
210
+ if (state.waitingOn !== event.nodeId || state.currentAttemptId !== event.attemptId) {
211
+ throw new Error("Interaction does not match the waiting attempt");
212
+ }
213
+ state.status = "running";
214
+ state.currentNode = requireIdentity(event.nodeId, "node");
215
+ delete state.waitingOn;
216
+ delete state.finishedAt;
217
+ delete state.statusDetail;
218
+ break;
219
+ case "pause":
220
+ requireEvent(event, "run_paused");
221
+ state.paused = true;
222
+ break;
223
+ case "finish":
224
+ if (
225
+ !["completed", "failed", "timed_out", "cancelled"].includes(transition.status) ||
226
+ (event.type !== `run_${transition.status}` &&
227
+ !(event.type === "run_interrupted" && transition.status === "failed"))
228
+ ) {
229
+ throw new Error("Run outcome does not match the transition");
230
+ }
231
+ if (transition.status === "completed" && state.currentAttemptId !== undefined) {
232
+ throw new Error("A run with an unfinished attempt cannot complete");
233
+ }
234
+ if (transition.status === "completed") {
235
+ const last = state.steps.at(-1);
236
+ if (
237
+ last?.outcome !== "ok" ||
238
+ nextNode(definition, state) !== null ||
239
+ !isDeepStrictEqual(transition.finalOutput, last.output)
240
+ ) {
241
+ throw new Error("Completion does not match the accepted graph result");
242
+ }
243
+ }
244
+ state.status = transition.status;
245
+ state.finishedAt = now;
246
+ if (transition.error !== undefined) state.error = transition.error;
247
+ if (transition.finalOutput !== undefined) state.finalOutput = transition.finalOutput;
248
+ clearAttempt(state);
249
+ break;
250
+ case "record":
251
+ if (!recordEvents.has(event.type)) throw new Error(`Unknown execution event: ${event.type}`);
252
+ if (event.type === "agent_prompt_sent" || event.type === "agent_session_required") {
253
+ requireAttempt(state, event);
254
+ }
255
+ if (event.type === "agent_session_required")
256
+ state.statusDetail = "waiting for origin Pi session";
257
+ if (event.type === "settings_route_retried") {
258
+ const payload = event.payload;
259
+ if (typeof payload.scopeId !== "string" || typeof payload.changeNumber !== "number") {
260
+ throw new Error("Settings retry binding is invalid");
261
+ }
262
+ state.currentSettingsScopeId = payload.scopeId;
263
+ state.currentSettingsChangeNumber = payload.changeNumber;
264
+ if (typeof payload.settingsHash !== "string")
265
+ throw new Error("Settings retry hash is missing");
266
+ state.currentSettingsHash = payload.settingsHash;
267
+ }
268
+ break;
269
+ default:
270
+ throw new Error("Unknown execution transition");
271
+ }
272
+ return state;
273
+ }
274
+
275
+ function nextNode(definition: WorkflowDefinitionSnapshot, state: WorkflowRunState): string | null {
276
+ const last = state.steps.at(-1);
277
+ if (last === undefined) return definition.startAt;
278
+ const result = state.results[last.nodeId];
279
+ if (result === undefined) throw new Error("Accepted graph result is missing");
280
+ return result.outcome === "ok"
281
+ ? resolveNext(definition.edges, last.nodeId, result.output, result)
282
+ : resolveNextForOutcome(definition.edges, last.nodeId, result);
283
+ }
284
+
285
+ function requireEvent(event: WorkflowTraceEventDraft, type: string): void {
286
+ if (event.type !== type) throw new Error(`Transition requires ${type}`);
287
+ }
288
+
289
+ function requireIdentity(value: string | undefined, name: string): string {
290
+ if (typeof value !== "string" || value.length === 0) throw new Error(`Missing ${name} identity`);
291
+ return value;
292
+ }
293
+
294
+ function requireAttempt(state: WorkflowRunState, event: WorkflowTraceEventDraft): void {
295
+ if (
296
+ state.currentNode !== requireIdentity(event.nodeId, "node") ||
297
+ state.currentAttemptId !== requireIdentity(event.attemptId, "attempt")
298
+ ) {
299
+ throw new Error("Execution transition does not match the active attempt");
300
+ }
301
+ }
302
+
303
+ function clearAttempt(state: WorkflowRunState): void {
304
+ delete state.currentNode;
305
+ delete state.currentAttemptId;
306
+ delete state.currentNodeStartedAt;
307
+ delete state.currentNodeTimeoutMs;
308
+ delete state.currentNodeElapsedMs;
309
+ delete state.currentSettingsScopeId;
310
+ delete state.currentSettingsChangeNumber;
311
+ delete state.currentSettingsHash;
312
+ delete state.statusDetail;
313
+ }
314
+
315
+ function setSettings(state: WorkflowRunState, payload: WorkflowTraceEventDraft["payload"]): void {
316
+ delete state.currentSettingsScopeId;
317
+ delete state.currentSettingsChangeNumber;
318
+ delete state.currentSettingsHash;
319
+ if (typeof payload.settingsScopeId === "string") {
320
+ if (
321
+ typeof payload.settingsChangeNumber !== "number" ||
322
+ typeof payload.settingsHash !== "string"
323
+ ) {
324
+ throw new Error("Attempt settings binding is invalid");
325
+ }
326
+ state.currentSettingsScopeId = payload.settingsScopeId;
327
+ state.currentSettingsChangeNumber = payload.settingsChangeNumber;
328
+ state.currentSettingsHash = payload.settingsHash;
329
+ }
330
+ }
@@ -448,20 +448,9 @@ export type HumanDecisionCancellationRecord = {
448
448
  reason: "cancelled" | "expired";
449
449
  };
450
450
 
451
- export type HumanDecisionContinuationRecord = {
452
- schema: "pi-workflows.human-decision-continuation.v1";
453
- decisionId: string;
454
- requestDigest: string;
455
- provenance: "human" | "timeout";
456
- parentRunId: string;
457
- runId: string;
458
- createdAt: string;
459
- };
460
-
461
451
  /**
462
- * A pause point. The run terminates with status `waiting` so a human (or an
463
- * external trigger) can decide how to continue. The optional `run` callback
464
- * produces the checkpoint's output before the run pauses.
452
+ * A same-run input request. The optional `run` callback produces its prompt.
453
+ * The accepted answer becomes the node output; requesting input does not finish the node.
465
454
  */
466
455
  export type CheckpointNodeDefinition = WorkflowNodeCommon & {
467
456
  nodeType: "checkpoint";
@@ -478,15 +467,6 @@ export type WorkflowNodeDefinition =
478
467
  | ActionNodeDefinition
479
468
  | CheckpointNodeDefinition;
480
469
 
481
- export type WorkflowPresentationContext = {
482
- /** Final persisted state of the workflow run. */
483
- state: WorkflowRunState;
484
- /** Convenience alias for `state.finalOutput`. */
485
- finalOutput: unknown;
486
- /** Aborted if a new run starts, the session closes, or prompt generation times out. */
487
- signal: AbortSignal;
488
- };
489
-
490
470
  /** Runtime parser that also carries its normalized TypeScript result type. */
491
471
  export type WorkflowValueParser<T> = (value: unknown) => MaybePromise<T>;
492
472
 
@@ -577,14 +557,6 @@ export type WorkflowDefinition<
577
557
  title?:
578
558
  | string
579
559
  | ((context: { input: TInput; workflowName: string }) => MaybePromise<string | undefined>);
580
- /**
581
- * Optional instructions for a normal assistant response after the run ends.
582
- * The Pi extension resolves this only after the final state is persisted;
583
- * the engine and durable store remain presentation-agnostic.
584
- */
585
- presentationPrompt?:
586
- | string
587
- | ((context: WorkflowPresentationContext) => MaybePromise<string | undefined>);
588
560
  startAt: string;
589
561
  nodes: Record<string, WorkflowNodeDefinition>;
590
562
  includes?: TIncludes;
@@ -686,14 +658,8 @@ export type WorkflowRunState = {
686
658
  traceSeq: number;
687
659
  runId: string;
688
660
  workflowName: string;
689
- /** Set on continuation runs: the checkpointed run this one carries forward. */
661
+ /** Set on explicit fresh restarts. Checkpoint answers keep the same run. */
690
662
  parentRunId?: string;
691
- /**
692
- * Steps carried from the parent at continuation start. Steps beyond this
693
- * count were recorded by this run itself; resume uses it to tell a
694
- * carried checkpoint from this run's own.
695
- */
696
- carriedStepCount?: number;
697
663
  runTitle?: string;
698
664
  /** Stable built-in identity or immutable file source used by this run. */
699
665
  workflowSource?: WorkflowSource;
@@ -718,12 +684,13 @@ export type WorkflowRunState = {
718
684
  currentAttemptId?: string;
719
685
  currentNodeStartedAt?: string;
720
686
  /** Durable active-execution deadline for the current attempt. Null disables the deadline. */
721
- currentNodeDeadlineAt?: string | null;
687
+ currentNodeTimeoutMs?: number | null;
688
+ currentNodeElapsedMs?: number;
722
689
  currentSettingsScopeId?: string;
723
690
  currentSettingsChangeNumber?: number;
724
691
  currentSettingsHash?: string;
725
692
  statusDetail?: string;
726
- /** Redacted verified-human receipt carried by a continuation run. */
693
+ /** Redacted receipt for the latest accepted human decision in this run. */
727
694
  humanDecision?: HumanDecisionReceipt;
728
695
  /** True while the run is held at a step boundary by a pause request. */
729
696
  paused?: boolean;
@@ -878,6 +845,7 @@ export type AssistantMessageReceipt = {
878
845
 
879
846
  /** The step contract handed to the executor alongside the prompt. */
880
847
  export type AgentStepContract = {
848
+ requestId: string;
881
849
  runId: string;
882
850
  workflowName: string;
883
851
  nodeId: string;
@@ -931,8 +899,8 @@ export interface AgentStepExecutor {
931
899
  * leave the run claimable for an origin session. Omission is unsupported.
932
900
  */
933
901
  readonly assistantMessageMode?: "visible" | "park" | "unsupported";
934
- /** True when the executor can park outside this process and resume the same attempt. */
935
- readonly preservesDeadlineWhileParked?: boolean;
902
+ /** True when the host records active time while this executor parks and resumes an attempt. */
903
+ readonly preservesActiveTimeBudget?: boolean;
936
904
  runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission>;
937
905
  }
938
906
 
@@ -968,11 +936,6 @@ export type WorkflowEngineOptions = {
968
936
  * Awaited after `run_started` is persisted, before any node executes.
969
937
  */
970
938
  onRunStarted?: (runId: string, state: WorkflowRunState) => MaybePromise<void>;
971
- /**
972
- * Awaited before the terminal snapshot is persisted. This is where a
973
- * session recorder stops and drains. Errors are swallowed: finishing the run wins.
974
- */
975
- onRunFinishing?: (runId: string, state: WorkflowRunState) => MaybePromise<void>;
976
939
  /** Default per-node timeout. Defaults to 15 minutes. */
977
940
  defaultNodeTimeoutMs?: number;
978
941
  /** Guard against unbounded graph loops. Defaults to 100 executed steps. */
@@ -9,7 +9,7 @@ import type { AgentStepContract } from "./types.js";
9
9
  export const WORKFLOW_STEP_MESSAGE_TYPE = "pi-workflows-step";
10
10
  export const WORKFLOW_DECISION_MESSAGE_TYPE = "pi-workflows-interaction";
11
11
  export const WORKFLOW_NOTIFICATION_MESSAGE_TYPE = "pi-workflows-notification";
12
- export const WORKFLOW_TERMINAL_MESSAGE_TYPE = "pi-workflows-presentation";
12
+ export const WORKFLOW_TERMINAL_MESSAGE_TYPE = "pi-workflows-terminal";
13
13
  export const WORKFLOW_FOLLOW_UP_MESSAGE_TYPE = "pi-workflows-follow-up";
14
14
 
15
15
  export function stepWorkflowMessageContent(options: {
@@ -20,6 +20,8 @@ export function stepWorkflowMessageContent(options: {
20
20
  }): WorkflowMessageContent {
21
21
  const outer = requireRecord(options.contract, "Stored workflow step contract");
22
22
  const agent = requireAgentContract(outer.contract);
23
+ if (agent.requestId !== options.requestId)
24
+ throw new Error("Workflow message request identity conflicts with its contract");
23
25
  const details: Record<string, JsonValue> = {
24
26
  schema: "pi-workflows.agent-step-message.v1",
25
27
  kind: "step",
@@ -61,6 +63,27 @@ export function decisionWorkflowMessageContent(options: {
61
63
  };
62
64
  }
63
65
 
66
+ export function checkpointWorkflowMessageContent(options: {
67
+ workflowMessageId: string;
68
+ requestId: string;
69
+ runId: string;
70
+ contract: JsonValue;
71
+ }): WorkflowMessageContent {
72
+ return {
73
+ schema: WORKFLOW_MESSAGE_CONTENT_SCHEMA,
74
+ customType: WORKFLOW_DECISION_MESSAGE_TYPE,
75
+ content: `${JSON.stringify(options.contract, null, 2)}\n\nAnswer checkpoint ${options.requestId} with the workflow answer action.`,
76
+ display: true,
77
+ details: {
78
+ workflowMessageId: options.workflowMessageId,
79
+ requestId: options.requestId,
80
+ runId: options.runId,
81
+ kind: "checkpoint",
82
+ },
83
+ triggerTurn: false,
84
+ };
85
+ }
86
+
64
87
  export function notificationWorkflowMessageContent(options: {
65
88
  workflowMessageId: string;
66
89
  notificationId: string;
@@ -93,14 +116,14 @@ export function terminalWorkflowMessageContent(options: {
93
116
  schema: WORKFLOW_MESSAGE_CONTENT_SCHEMA,
94
117
  customType: WORKFLOW_TERMINAL_MESSAGE_TYPE,
95
118
  content: options.content,
96
- display: false,
119
+ display: true,
97
120
  details: {
98
121
  workflowMessageId: options.workflowMessageId,
99
122
  runId: options.runId,
100
123
  kind: "terminal",
101
124
  terminal: options.details,
102
125
  },
103
- triggerTurn: true,
126
+ triggerTurn: false,
104
127
  };
105
128
  }
106
129
 
@@ -128,6 +151,7 @@ export function followUpWorkflowMessageContent(options: {
128
151
  function requireAgentContract(value: unknown): AgentStepContract {
129
152
  if (
130
153
  !isRecord(value) ||
154
+ typeof value.requestId !== "string" ||
131
155
  typeof value.runId !== "string" ||
132
156
  typeof value.workflowName !== "string" ||
133
157
  typeof value.nodeId !== "string" ||
@@ -159,7 +183,7 @@ function decisionPrompt(contract: Record<string, unknown>): string {
159
183
  typeof presentation.summary === "string" ? presentation.summary : "",
160
184
  ...blocks,
161
185
  choices.length === 0 ? "" : `Choices:\n${choices.join("\n")}`,
162
- "A human must answer this protected decision with `/workflow answer`.",
186
+ `A human must answer this protected decision with \`/workflow answer ${String(contract.decisionId)} <response>\`.`,
163
187
  ]
164
188
  .filter(Boolean)
165
189
  .join("\n\n");
@@ -1,59 +0,0 @@
1
- import type { ResourceManagerStore } from "./store.js";
2
- import type { AnyResourceManagerDefinition, ResourceManagerDefinition, ManagedResource, ManagedResourceRef } from "./types.js";
3
- import { ResourceManagerWorkflowCoordinator, type ResourceManagerWorkflowScheduler } from "./workflows.js";
4
- export type ResourceManagerRuntimeOptions = {
5
- store: ResourceManagerStore;
6
- resourceManagers: AnyResourceManagerDefinition[];
7
- workflowScheduler?: ResourceManagerWorkflowScheduler;
8
- maxConcurrent?: number;
9
- resourceManagerConcurrency?: Record<string, number>;
10
- defaultTimeoutMs?: number;
11
- leaseMs?: number;
12
- pollIntervalMs?: number;
13
- baseBackoffMs?: number;
14
- maxBackoffMs?: number;
15
- jitterRatio?: number;
16
- now?: () => Date;
17
- random?: () => number;
18
- };
19
- export declare class ResourceManagerRuntime {
20
- readonly store: ResourceManagerStore;
21
- private readonly ownerId;
22
- private readonly definitions;
23
- private readonly workflowCoordinator;
24
- private readonly maxConcurrent;
25
- private readonly resourceManagerConcurrency;
26
- private readonly defaultTimeoutMs;
27
- private readonly leaseMs;
28
- private readonly pollIntervalMs;
29
- private readonly baseBackoffMs;
30
- private readonly maxBackoffMs;
31
- private readonly jitterRatio;
32
- private readonly now;
33
- private readonly random;
34
- private readonly activeByResourceManager;
35
- private readonly activeReconciles;
36
- private runAbort;
37
- private runnerTasks;
38
- constructor(options: ResourceManagerRuntimeOptions);
39
- putResource<TSpec, TStatus>(definition: ResourceManagerDefinition<TSpec, TStatus>, key: string, spec: TSpec): ManagedResource<TSpec, TStatus>;
40
- putResourceByName(resourceManager: string, key: string, spec: unknown): ManagedResource;
41
- requestDeletion(ref: ManagedResourceRef): ManagedResource;
42
- enqueue(ref: ManagedResourceRef, afterMs?: number): void;
43
- start(): void;
44
- stop(): Promise<void>;
45
- runOne(): Promise<boolean>;
46
- runUntilIdle(maxReconciles?: number): Promise<number>;
47
- get workflowRequests(): ResourceManagerWorkflowCoordinator;
48
- private runnerLoop;
49
- private claimNext;
50
- private eligibleResourceManagers;
51
- private processClaim;
52
- private reconcileClaim;
53
- private callReconciler;
54
- private finishSuccessfulClaim;
55
- private finishFailedClaim;
56
- private backoffMs;
57
- private readyQueueExists;
58
- private recordEvent;
59
- }