@osolmaz/pi-workflows 0.13.2 → 0.13.3

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 (130) hide show
  1. package/README.md +5 -3
  2. package/dist/builtins/autodoc.workflow.d.ts +5 -5
  3. package/dist/builtins/autoimplement.workflow.d.ts +65 -61
  4. package/dist/builtins/autoimplement.workflow.js +47 -8
  5. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  6. package/dist/builtins/autoplan.workflow.d.ts +5 -5
  7. package/dist/builtins/catalog.js +1 -1
  8. package/dist/builtins/change-verification.workflow.d.ts +1 -1
  9. package/dist/builtins/plain-summary.workflow.d.ts +1 -1
  10. package/dist/builtins/plan-approval.workflow.d.ts +1 -1
  11. package/dist/builtins/plan-change.workflow.d.ts +23 -23
  12. package/dist/builtins/sanity-check.workflow.d.ts +3 -3
  13. package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
  14. package/dist/controllers/index.d.ts +2 -2
  15. package/dist/controllers/index.js.map +1 -1
  16. package/dist/controllers/sqlite.js +85 -23
  17. package/dist/controllers/sqlite.js.map +1 -1
  18. package/dist/controllers/types.d.ts +34 -0
  19. package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
  20. package/dist/controllers/workflow-engine-scheduler.js +83 -1
  21. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  22. package/dist/controllers/workflows.d.ts +8 -1
  23. package/dist/controllers/workflows.js +36 -0
  24. package/dist/controllers/workflows.js.map +1 -1
  25. package/dist/extension/controller-host.d.ts +2 -1
  26. package/dist/extension/controller-host.js +1 -1
  27. package/dist/extension/controller-host.js.map +1 -1
  28. package/dist/extension/follow-up-coordinator.d.ts +27 -0
  29. package/dist/extension/follow-up-coordinator.js +131 -0
  30. package/dist/extension/follow-up-coordinator.js.map +1 -0
  31. package/dist/extension/index.d.ts +9 -0
  32. package/dist/extension/index.js +460 -75
  33. package/dist/extension/index.js.map +1 -1
  34. package/dist/extension/session-events.d.ts +2 -3
  35. package/dist/extension/session-events.js +11 -10
  36. package/dist/extension/session-events.js.map +1 -1
  37. package/dist/extension/widget.js +9 -0
  38. package/dist/extension/widget.js.map +1 -1
  39. package/dist/state/database.d.ts +2 -1
  40. package/dist/state/database.js +11 -13
  41. package/dist/state/database.js.map +1 -1
  42. package/dist/state/json.js +6 -1
  43. package/dist/state/json.js.map +1 -1
  44. package/dist/state/mutation.d.ts +1 -1
  45. package/dist/state/mutation.js.map +1 -1
  46. package/dist/state/prune.d.ts +18 -0
  47. package/dist/state/prune.js +373 -0
  48. package/dist/state/prune.js.map +1 -0
  49. package/dist/state/schema.d.ts +1 -1
  50. package/dist/state/schema.js +116 -18
  51. package/dist/state/schema.js.map +1 -1
  52. package/dist/viewer/cli.d.ts +3 -1
  53. package/dist/viewer/cli.js +55 -4
  54. package/dist/viewer/cli.js.map +1 -1
  55. package/dist/viewer/render.js +19 -1
  56. package/dist/viewer/render.js.map +1 -1
  57. package/dist/workflows/composition.d.ts +2 -0
  58. package/dist/workflows/composition.js +15 -0
  59. package/dist/workflows/composition.js.map +1 -1
  60. package/dist/workflows/definition.d.ts +6 -3
  61. package/dist/workflows/definition.js +3 -0
  62. package/dist/workflows/definition.js.map +1 -1
  63. package/dist/workflows/engine.d.ts +6 -1
  64. package/dist/workflows/engine.js +301 -35
  65. package/dist/workflows/engine.js.map +1 -1
  66. package/dist/workflows/human-decision.d.ts +1 -0
  67. package/dist/workflows/human-decision.js +25 -9
  68. package/dist/workflows/human-decision.js.map +1 -1
  69. package/dist/workflows/index.d.ts +3 -1
  70. package/dist/workflows/index.js +2 -0
  71. package/dist/workflows/index.js.map +1 -1
  72. package/dist/workflows/json-patch.d.ts +47 -0
  73. package/dist/workflows/json-patch.js +298 -0
  74. package/dist/workflows/json-patch.js.map +1 -0
  75. package/dist/workflows/schema.js +29 -1
  76. package/dist/workflows/schema.js.map +1 -1
  77. package/dist/workflows/settings.d.ts +128 -0
  78. package/dist/workflows/settings.js +199 -0
  79. package/dist/workflows/settings.js.map +1 -0
  80. package/dist/workflows/store.d.ts +75 -2
  81. package/dist/workflows/store.js +1349 -89
  82. package/dist/workflows/store.js.map +1 -1
  83. package/dist/workflows/tool-input.d.ts +22 -0
  84. package/dist/workflows/tool-input.js +43 -0
  85. package/dist/workflows/tool-input.js.map +1 -1
  86. package/dist/workflows/types.d.ts +40 -5
  87. package/docs/2026-08-25-workflow-follow-ups.md +132 -0
  88. package/docs/2026-08-25-workflow-settings.md +169 -0
  89. package/docs/DEFERRED_TURNS.md +6 -0
  90. package/docs/DESIGN_PHILOSOPHY.md +2 -0
  91. package/docs/SQLITE_STATE.md +31 -18
  92. package/docs/WORKFLOW_COMPOSITION.md +15 -0
  93. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
  94. package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
  95. package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
  96. package/docs/tui-viewer.md +7 -6
  97. package/docs/workflows.md +22 -1
  98. package/examples/workflows/live-settings.workflow.ts +93 -0
  99. package/herdr-plugin.toml +1 -1
  100. package/package.json +1 -1
  101. package/src/builtins/autoimplement.workflow.ts +56 -8
  102. package/src/builtins/catalog.ts +1 -1
  103. package/src/controllers/index.ts +6 -0
  104. package/src/controllers/sqlite.ts +129 -35
  105. package/src/controllers/types.ts +40 -0
  106. package/src/controllers/workflow-engine-scheduler.ts +103 -1
  107. package/src/controllers/workflows.ts +68 -0
  108. package/src/extension/controller-host.ts +6 -1
  109. package/src/extension/follow-up-coordinator.ts +151 -0
  110. package/src/extension/index.ts +538 -88
  111. package/src/extension/session-events.ts +15 -13
  112. package/src/extension/widget.ts +8 -0
  113. package/src/state/database.ts +12 -12
  114. package/src/state/json.ts +6 -1
  115. package/src/state/mutation.ts +4 -1
  116. package/src/state/prune.ts +502 -0
  117. package/src/state/schema.ts +116 -18
  118. package/src/viewer/cli.ts +52 -5
  119. package/src/viewer/render.ts +43 -1
  120. package/src/workflows/composition.ts +19 -0
  121. package/src/workflows/definition.ts +19 -5
  122. package/src/workflows/engine.ts +363 -35
  123. package/src/workflows/human-decision.ts +41 -11
  124. package/src/workflows/index.ts +43 -0
  125. package/src/workflows/json-patch.ts +375 -0
  126. package/src/workflows/schema.ts +31 -1
  127. package/src/workflows/settings.ts +430 -0
  128. package/src/workflows/store.ts +1977 -184
  129. package/src/workflows/tool-input.ts +58 -0
  130. package/src/workflows/types.ts +43 -4
@@ -5,7 +5,7 @@ import { SqliteControllerStore } from "../controllers/index.js";
5
5
  import { canonicalJson } from "../state/json.js";
6
6
  import { compositionMetadata } from "../workflows/composition.js";
7
7
  import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
8
- import { WorkflowEngine } from "../workflows/engine.js";
8
+ import { WorkflowEngine, workflowIdentityMismatch } from "../workflows/engine.js";
9
9
  import { BuiltinWorkflowRevisionChangedError, ClaimLostError, errorMessage, isClaimLostError, TimeoutError, } from "../workflows/errors.js";
10
10
  import { HumanDecisionStore, createHumanDecisionAttemptId, validateHumanDecisionResponse, } from "../workflows/human-decision.js";
11
11
  import { discoverWorkflows, resolveWorkflowRef } from "../workflows/loader.js";
@@ -16,6 +16,7 @@ import { PiDecisionChannel, TelegramDecisionChannel, audienceChannels, createTel
16
16
  import { DeferredTurnCoordinator, } from "./deferred-turn-coordinator.js";
17
17
  import { buildDeferredTurnContent, createDeferredTurnDescriptor, DEFERRED_TURN_MESSAGE_TYPE, deferredTurnMessageDetails, deferredTurnMessageId, deferredTurnSourceEventId, } from "./deferred-turn.js";
18
18
  import { ConversationStepExecutor } from "./executor.js";
19
+ import { FollowUpCoordinator, findSettledPresentationEntries } from "./follow-up-coordinator.js";
19
20
  import { HerdrWorkflowViewer, parseViewerPlacement, PIW_SHORTCUT, PIW_SHORTCUT_HINT, VIEWER_PLACEMENTS, } from "./herdr-viewer.js";
20
21
  import { SessionRecorder } from "./recorder.js";
21
22
  import { recoverAssistantStep, registerWorkflowAgentStepMessageRenderer, WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA, WORKFLOW_AGENT_STEP_MESSAGE_TYPE, } from "./step-message.js";
@@ -122,6 +123,28 @@ export function parseWorkflowArgs(args) {
122
123
  if (trimmed === "status") {
123
124
  return { kind: "status" };
124
125
  }
126
+ if (trimmed.startsWith("change-settings ")) {
127
+ const text = trimmed.slice("change-settings".length).trim();
128
+ try {
129
+ return { kind: "change-settings", patch: JSON.parse(text) };
130
+ }
131
+ catch (error) {
132
+ throw new Error(`change-settings requires a JSON Patch array: ${error instanceof Error ? error.message : String(error)}`);
133
+ }
134
+ }
135
+ if (trimmed.startsWith("queue-follow-up ")) {
136
+ const prompt = trimmed.slice("queue-follow-up".length).trim();
137
+ if (prompt.length === 0)
138
+ throw new Error("queue-follow-up requires a prompt");
139
+ return { kind: "queue-follow-up", prompt };
140
+ }
141
+ if (trimmed.startsWith("remove-follow-up ")) {
142
+ const followUpId = trimmed.slice("remove-follow-up".length).trim();
143
+ if (!/^follow-up-[a-f0-9]{40}$/u.test(followUpId)) {
144
+ throw new Error("remove-follow-up requires one valid follow-up id");
145
+ }
146
+ return { kind: "remove-follow-up", followUpId };
147
+ }
125
148
  if (trimmed.startsWith("status ")) {
126
149
  const runId = trimmed.slice("status".length).trim();
127
150
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(runId)) {
@@ -177,7 +200,7 @@ export function parseWorkflowArgs(args) {
177
200
  }
178
201
  return { kind: "run", ref, input: rest.length > 0 ? { task: rest } : {} };
179
202
  }
180
- function workflowStateSummary(state) {
203
+ function workflowStateSummary(state, controls) {
181
204
  const error = state.error === undefined
182
205
  ? undefined
183
206
  : state.error.length <= MAX_STATUS_ERROR_CHARS
@@ -197,6 +220,32 @@ function workflowStateSummary(state) {
197
220
  ...(state.waitingOn !== undefined ? { waitingOn: state.waitingOn } : {}),
198
221
  ...(state.startedAt !== undefined ? { startedAt: state.startedAt } : {}),
199
222
  ...(state.finishedAt !== undefined ? { finishedAt: state.finishedAt } : {}),
223
+ ...(controls !== undefined
224
+ ? {
225
+ settingsScopes: (controls.settingsScopes ?? []).map((scope) => ({
226
+ scopeId: scope.scopeId,
227
+ mountPath: scope.mountPath,
228
+ invocation: scope.invocation,
229
+ changeNumber: scope.changeNumber,
230
+ settingsHash: scope.settingsHash,
231
+ activeRunId: scope.activeRunId,
232
+ })),
233
+ followUps: controls.followUpQueue === null || controls.followUpQueue === undefined
234
+ ? null
235
+ : {
236
+ presentationState: controls.followUpQueue.presentationState,
237
+ items: controls.followUpQueue.followUps.map((followUp) => ({
238
+ followUpId: followUp.followUpId,
239
+ order: followUp.order,
240
+ state: followUp.state,
241
+ source: followUp.source,
242
+ ...(followUp.sessionEntryId !== undefined
243
+ ? { sessionEntryId: followUp.sessionEntryId }
244
+ : {}),
245
+ })),
246
+ },
247
+ }
248
+ : {}),
200
249
  ...(error !== undefined ? { error } : {}),
201
250
  };
202
251
  }
@@ -314,6 +363,7 @@ export default function piWorkflows(pi) {
314
363
  }, { triggerTurn: true, deliverAs: "followUp" });
315
364
  },
316
365
  }, idle);
366
+ void followUpCoordinator?.synchronize(ctx, activeRun !== null).catch(() => undefined);
317
367
  }
318
368
  catch {
319
369
  // Delivery retries on the next poll. It never affects workflow execution.
@@ -346,6 +396,8 @@ export default function piWorkflows(pi) {
346
396
  let runGeneration = 0;
347
397
  let presentationAbort = null;
348
398
  let presentationPending = null;
399
+ let followUpStore = null;
400
+ let followUpCoordinator = null;
349
401
  let controllerHost;
350
402
  let controllerContext = null;
351
403
  const branchIntentResolution = (intentId) => {
@@ -377,6 +429,38 @@ export default function piWorkflows(pi) {
377
429
  branchResolution: branchIntentResolution,
378
430
  leaseMs: TURN_INTENT_DELIVERY_LEASE_MS,
379
431
  });
432
+ const ensureFollowUpDelivery = () => {
433
+ if (followUpStore !== null && followUpCoordinator !== null)
434
+ return;
435
+ followUpStore?.close();
436
+ followUpStore = new WorkflowRunStore();
437
+ followUpCoordinator = new FollowUpCoordinator(followUpStore, `follow-up-${runnerId}`, (text) => pi.sendUserMessage(text));
438
+ };
439
+ const settlePresentationFromBranch = (ctx, runId, unavailableReason) => {
440
+ if (followUpStore === null ||
441
+ followUpStore.readFollowUpQueue(runId)?.presentationState !== "pending") {
442
+ return false;
443
+ }
444
+ const settled = findSettledPresentationEntries(ctx.sessionManager.getBranch(), runId);
445
+ if (settled !== undefined) {
446
+ followUpStore.settleFollowUpPresentation({
447
+ runId,
448
+ state: "settled",
449
+ presentationEntryId: settled.presentationEntryId,
450
+ assistantEntryId: settled.assistantEntryId,
451
+ });
452
+ return true;
453
+ }
454
+ if (unavailableReason !== undefined) {
455
+ followUpStore.settleFollowUpPresentation({
456
+ runId,
457
+ state: "unavailable",
458
+ reason: unavailableReason,
459
+ });
460
+ return true;
461
+ }
462
+ return false;
463
+ };
380
464
  // UI updates are best-effort: a captured ctx becomes stale after session
381
465
  // replacement or shutdown, and pi throws on any access (even `ctx.hasUI`).
382
466
  // A workflow finishing right as the session goes away must not crash pi.
@@ -689,6 +773,7 @@ export default function piWorkflows(pi) {
689
773
  return;
690
774
  }
691
775
  if (instructions === undefined || instructions.trim().length === 0) {
776
+ settlePresentationFromBranch(ctx, run.runId, "No result presentation was available");
692
777
  if (run.abortProvenance !== undefined) {
693
778
  makeRunTurnIntentEligible(ctx, run, state.status, "No result presentation was available");
694
779
  }
@@ -699,13 +784,14 @@ export default function piWorkflows(pi) {
699
784
  targetSessionId: originSessionId(ctx, run.runId),
700
785
  resolution: "presentation",
701
786
  send: (turnIntentId) => {
702
- presentationPending = run.generation;
787
+ presentationPending = { generation: run.generation, runId: run.runId };
703
788
  pi.sendMessage({
704
789
  customType: PRESENTATION_MESSAGE_TYPE,
705
790
  content: buildPresentationMessage(instructions, state),
706
791
  display: false,
707
792
  details: {
708
793
  schema: PRESENTATION_MESSAGE_SCHEMA,
794
+ runId: run.runId,
709
795
  ...(turnIntentId === undefined ? {} : { turnIntentId }),
710
796
  },
711
797
  }, {
@@ -716,7 +802,7 @@ export default function piWorkflows(pi) {
716
802
  }, ctx.isIdle() && systemTurnAbort === null);
717
803
  }
718
804
  catch (error) {
719
- if (presentationPending === run.generation) {
805
+ if (presentationPending?.generation === run.generation) {
720
806
  presentationPending = null;
721
807
  }
722
808
  if (error instanceof PresentationSupersededError ||
@@ -728,6 +814,7 @@ export default function piWorkflows(pi) {
728
814
  ? `timed out after ${PRESENTATION_TIMEOUT_MS}ms`
729
815
  : errorMessage(error);
730
816
  notify(ctx, `Could not present workflow result: ${message}`, "warning");
817
+ settlePresentationFromBranch(ctx, run.runId, message);
731
818
  if (run.abortProvenance !== undefined) {
732
819
  makeRunTurnIntentEligible(ctx, run, state.status, message);
733
820
  }
@@ -876,6 +963,7 @@ export default function piWorkflows(pi) {
876
963
  if (options.signal?.aborted) {
877
964
  throw options.signal.reason ?? new Error("Workflow startup aborted");
878
965
  }
966
+ ensureFollowUpDelivery();
879
967
  if (activeRun) {
880
968
  if (!options.quiet) {
881
969
  notify(ctx, `A workflow is already running: ${activeRun.workflowName}. Use /workflow cancel first.`, "error");
@@ -1310,7 +1398,65 @@ export default function piWorkflows(pi) {
1310
1398
  if (controllerHost !== undefined) {
1311
1399
  return controllerHost;
1312
1400
  }
1313
- controllerHost = await PiControllerHost.create({ cwd: ctx.cwd, startChild });
1401
+ controllerHost = await PiControllerHost.create({
1402
+ cwd: ctx.cwd,
1403
+ startChild,
1404
+ workflowControls: {
1405
+ changeSettings: async (request) => {
1406
+ const result = await changeWorkflowSettings(ctx, {
1407
+ runId: request.runId,
1408
+ ...(request.scopeId !== undefined ? { scopeId: request.scopeId } : {}),
1409
+ ...(request.expectedChangeNumber !== undefined
1410
+ ? { expectedChangeNumber: request.expectedChangeNumber }
1411
+ : {}),
1412
+ patch: request.patch,
1413
+ requestId: request.actorRequestKey,
1414
+ actor: "controller",
1415
+ actorId: request.controllerResourceUid,
1416
+ source: "controller-request",
1417
+ });
1418
+ return {
1419
+ runId: request.runId,
1420
+ scopeId: String(result.details.scopeId),
1421
+ changeNumber: Number(result.details.changeNumber),
1422
+ adopted: result.details.adopted === true,
1423
+ };
1424
+ },
1425
+ queueFollowUp: async (request) => {
1426
+ const result = await queueWorkflowFollowUp(ctx, {
1427
+ runId: request.runId,
1428
+ prompt: request.prompt,
1429
+ requestId: request.actorRequestKey,
1430
+ actor: "controller",
1431
+ actorId: request.controllerResourceUid,
1432
+ source: "controller-request",
1433
+ });
1434
+ return {
1435
+ runId: request.runId,
1436
+ followUpId: String(result.details.followUpId),
1437
+ order: Number(result.details.order),
1438
+ state: "queued",
1439
+ adopted: result.details.adopted === true,
1440
+ };
1441
+ },
1442
+ removeFollowUp: async (request) => {
1443
+ const result = await removeWorkflowFollowUp(ctx, {
1444
+ runId: request.runId,
1445
+ followUpId: request.followUpId,
1446
+ actor: "controller",
1447
+ actorId: request.controllerResourceUid,
1448
+ source: "controller-request",
1449
+ });
1450
+ return {
1451
+ runId: request.runId,
1452
+ followUpId: String(result.details.followUpId),
1453
+ order: Number(result.details.order),
1454
+ state: String(result.details.state),
1455
+ adopted: false,
1456
+ };
1457
+ },
1458
+ },
1459
+ });
1314
1460
  controllerHost?.start();
1315
1461
  updateControllerStatus(ctx);
1316
1462
  return controllerHost;
@@ -1574,7 +1720,7 @@ export default function piWorkflows(pi) {
1574
1720
  const { state } = bundle;
1575
1721
  return {
1576
1722
  message: `Workflow ${state.workflowName} is ${state.status} (run ${state.runId}).`,
1577
- details: workflowStateSummary(state),
1723
+ details: workflowStateSummary(state, bundle),
1578
1724
  };
1579
1725
  }
1580
1726
  const state = activeRun?.lastState ?? widgetSource?.state;
@@ -1592,9 +1738,136 @@ export default function piWorkflows(pi) {
1592
1738
  }
1593
1739
  return {
1594
1740
  message: `Workflow ${state.workflowName} is ${state.status} (run ${state.runId}).`,
1595
- details: workflowStateSummary(state),
1741
+ details: workflowStateSummary(state, readWorkflowRun(state.runId) ?? undefined),
1596
1742
  };
1597
1743
  };
1744
+ const resolveMutableWorkflow = async (ctx, requestedRunId) => {
1745
+ const runId = requestedRunId ?? activeRun?.runId ?? lastWaitingRunId ?? undefined;
1746
+ if (runId === undefined) {
1747
+ throw new Error("No running, parked, or waiting workflow is available for changes.");
1748
+ }
1749
+ const loaded = readWorkflowRun(runId);
1750
+ if (loaded === null)
1751
+ throw new Error(`Workflow run not found: ${runId}`);
1752
+ if (originSessionId(ctx, runId) !== ctx.sessionManager.getSessionId()) {
1753
+ throw new Error("Workflow changes must come from the Pi session that owns the run.");
1754
+ }
1755
+ const source = loaded.state.workflowSource;
1756
+ if (source === undefined)
1757
+ throw new Error(`Workflow run ${runId} has no saved source`);
1758
+ const ref = source.kind === "builtin" ? `builtin:${source.id}` : source.path;
1759
+ const resolved = await resolveWorkflowRef(ref, { cwd: ctx.cwd }, builtinWorkflowCatalog);
1760
+ if (workflowIdentityMismatch(loaded.state, resolved.definition, resolved.source)) {
1761
+ throw new Error(`Workflow source changed since run ${runId} started`);
1762
+ }
1763
+ return { runId, workflow: resolved.definition, store: new WorkflowRunStore() };
1764
+ };
1765
+ const changeWorkflowSettings = async (ctx, options) => {
1766
+ const target = await resolveMutableWorkflow(ctx, options.runId);
1767
+ try {
1768
+ const scopes = target.store.listSettingsScopes(target.runId);
1769
+ const scopeId = options.scopeId ??
1770
+ (activeRun?.runId === target.runId ? activeRun.engine.activeSettingsScopeId : undefined) ??
1771
+ (scopes.length === 1 ? scopes[0]?.scopeId : undefined);
1772
+ if (scopeId === undefined) {
1773
+ throw new Error("Specify scopeId because this workflow has more than one settings scope.");
1774
+ }
1775
+ const scope = target.store.getSettingsScope(scopeId);
1776
+ if (scope === undefined || scope.activeRunId !== target.runId) {
1777
+ throw new Error(`Workflow settings scope not found for run ${target.runId}: ${scopeId}`);
1778
+ }
1779
+ const definition = scope.mountPath === ""
1780
+ ? target.workflow.settings
1781
+ : compositionMetadata(target.workflow)?.scopes[scope.mountPath]?.settings;
1782
+ if (definition === undefined) {
1783
+ throw new Error(`Workflow scope ${scopeId} does not declare editable settings.`);
1784
+ }
1785
+ const result = await target.store.changeSettings(definition, {
1786
+ runId: target.runId,
1787
+ scopeId,
1788
+ requestId: options.requestId,
1789
+ ...(options.expectedChangeNumber !== undefined
1790
+ ? { expectedChangeNumber: options.expectedChangeNumber }
1791
+ : {}),
1792
+ actor: {
1793
+ type: options.actor,
1794
+ id: options.actorId ?? ctx.sessionManager.getSessionId(),
1795
+ },
1796
+ source: options.source,
1797
+ patch: options.patch,
1798
+ });
1799
+ renderWidget(ctx);
1800
+ return {
1801
+ message: `Workflow settings changed to number ${result.scope.changeNumber}.`,
1802
+ details: {
1803
+ action: "change-settings",
1804
+ runId: target.runId,
1805
+ scopeId,
1806
+ changeNumber: result.scope.changeNumber,
1807
+ adopted: result.adopted,
1808
+ },
1809
+ };
1810
+ }
1811
+ finally {
1812
+ target.store.close();
1813
+ }
1814
+ };
1815
+ const queueWorkflowFollowUp = async (ctx, options) => {
1816
+ const target = await resolveMutableWorkflow(ctx, options.runId);
1817
+ try {
1818
+ const result = target.store.queueFollowUp({
1819
+ runId: target.runId,
1820
+ requestId: options.requestId,
1821
+ targetSessionId: ctx.sessionManager.getSessionId(),
1822
+ actor: {
1823
+ type: options.actor,
1824
+ id: options.actorId ?? ctx.sessionManager.getSessionId(),
1825
+ },
1826
+ source: options.source,
1827
+ prompt: options.prompt,
1828
+ });
1829
+ return {
1830
+ message: `Queued workflow follow-up ${result.followUp.order}.`,
1831
+ details: {
1832
+ action: "queue-follow-up",
1833
+ runId: target.runId,
1834
+ followUpId: result.followUp.followUpId,
1835
+ order: result.followUp.order,
1836
+ adopted: result.adopted,
1837
+ },
1838
+ };
1839
+ }
1840
+ finally {
1841
+ target.store.close();
1842
+ }
1843
+ };
1844
+ const removeWorkflowFollowUp = async (ctx, options) => {
1845
+ const target = await resolveMutableWorkflow(ctx, options.runId);
1846
+ try {
1847
+ const followUp = target.store.removeFollowUp({
1848
+ runId: target.runId,
1849
+ followUpId: options.followUpId,
1850
+ actor: {
1851
+ type: options.actor,
1852
+ id: options.actorId ?? ctx.sessionManager.getSessionId(),
1853
+ },
1854
+ source: options.source,
1855
+ });
1856
+ return {
1857
+ message: `Removed workflow follow-up ${followUp.order}.`,
1858
+ details: {
1859
+ action: "remove-follow-up",
1860
+ runId: target.runId,
1861
+ followUpId: followUp.followUpId,
1862
+ order: followUp.order,
1863
+ state: followUp.state,
1864
+ },
1865
+ };
1866
+ }
1867
+ finally {
1868
+ target.store.close();
1869
+ }
1870
+ };
1598
1871
  const resolveWaitingWorkflow = async (ctx, requestedRunId, allowOtherSession = false) => {
1599
1872
  let parentRunId = requestedRunId ?? lastWaitingRunId;
1600
1873
  if (parentRunId === null) {
@@ -2116,7 +2389,7 @@ export default function piWorkflows(pi) {
2116
2389
  },
2117
2390
  });
2118
2391
  pi.registerCommand("workflow", {
2119
- description: "Run or manage a workflow: /workflow <name-or-path> [task | --input-json {…}]; also: status, pause, resume, cancel, answer",
2392
+ description: "Run or manage a workflow: /workflow <name-or-path> [task | --input-json {…}]; also: status, pause, resume, cancel, answer, change-settings, queue-follow-up, remove-follow-up",
2120
2393
  getArgumentCompletions: async (prefix) => {
2121
2394
  const discovered = await discoverWorkflows({ cwd: process.cwd() }, builtinWorkflowCatalog);
2122
2395
  const items = [
@@ -2126,6 +2399,9 @@ export default function piWorkflows(pi) {
2126
2399
  { value: "resume", label: "resume" },
2127
2400
  { value: "cancel", label: "cancel" },
2128
2401
  { value: "answer", label: "answer" },
2402
+ { value: "change-settings", label: "change-settings" },
2403
+ { value: "queue-follow-up", label: "queue-follow-up" },
2404
+ { value: "remove-follow-up", label: "remove-follow-up" },
2129
2405
  ].filter((item) => item.value.startsWith(prefix));
2130
2406
  return items.length > 0 ? items : null;
2131
2407
  },
@@ -2167,6 +2443,50 @@ export default function piWorkflows(pi) {
2167
2443
  }
2168
2444
  return;
2169
2445
  }
2446
+ if (parsed.kind === "change-settings") {
2447
+ try {
2448
+ const result = await changeWorkflowSettings(ctx, {
2449
+ patch: parsed.patch,
2450
+ requestId: `interactive-${randomUUID()}`,
2451
+ actor: "human",
2452
+ source: "interactive-command",
2453
+ });
2454
+ notify(ctx, result.message, result.level);
2455
+ }
2456
+ catch (error) {
2457
+ notify(ctx, errorMessage(error), "error");
2458
+ }
2459
+ return;
2460
+ }
2461
+ if (parsed.kind === "queue-follow-up") {
2462
+ try {
2463
+ const result = await queueWorkflowFollowUp(ctx, {
2464
+ prompt: parsed.prompt,
2465
+ requestId: `interactive-${randomUUID()}`,
2466
+ actor: "human",
2467
+ source: "interactive-command",
2468
+ });
2469
+ notify(ctx, result.message, result.level);
2470
+ }
2471
+ catch (error) {
2472
+ notify(ctx, errorMessage(error), "error");
2473
+ }
2474
+ return;
2475
+ }
2476
+ if (parsed.kind === "remove-follow-up") {
2477
+ try {
2478
+ const result = await removeWorkflowFollowUp(ctx, {
2479
+ followUpId: parsed.followUpId,
2480
+ actor: "human",
2481
+ source: "interactive-command",
2482
+ });
2483
+ notify(ctx, result.message, result.level);
2484
+ }
2485
+ catch (error) {
2486
+ notify(ctx, errorMessage(error), "error");
2487
+ }
2488
+ return;
2489
+ }
2170
2490
  if (parsed.kind === "answer") {
2171
2491
  try {
2172
2492
  const result = await answerWorkflowControl(ctx, parsed.input, parsed.runId);
@@ -2316,11 +2636,26 @@ export default function piWorkflows(pi) {
2316
2636
  }
2317
2637
  },
2318
2638
  });
2639
+ let workflowToolTail = Promise.resolve();
2640
+ const withWorkflowToolOrder = async (operation) => {
2641
+ const prior = workflowToolTail;
2642
+ let release;
2643
+ workflowToolTail = new Promise((resolve) => {
2644
+ release = resolve;
2645
+ });
2646
+ await prior;
2647
+ try {
2648
+ return await operation();
2649
+ }
2650
+ finally {
2651
+ release?.();
2652
+ }
2653
+ };
2319
2654
  pi.registerTool({
2320
2655
  name: "workflow",
2321
2656
  label: "Workflow",
2322
2657
  description: [
2323
- "List, start, inspect, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
2658
+ "List, start, inspect, change settings, queue or remove follow-ups, pause, resume, cancel, answer, update, or complete pi-workflows runs.",
2324
2659
  "When the user asks to monitor, watch, poll, or check something repeatedly, start the built-in monitor workflow with input keys task, stopWhen, everyMinutes, and optional maxChecks.",
2325
2660
  "Put the exact goal, authority, limits, and recovery rules in task; Monitor observes first, performs only safe authorized actions, verifies them immediately, and waits only while target work is moving or an external event is pending.",
2326
2661
  "Use update or submit only when a workflow step contract asks for it, and pass the exact step and attempt ids.",
@@ -2328,75 +2663,107 @@ export default function piWorkflows(pi) {
2328
2663
  ].join(" "),
2329
2664
  parameters: WorkflowToolParameters,
2330
2665
  async execute(toolCallId, rawParams, _signal, _onUpdate, ctx) {
2331
- const params = parseWorkflowToolInput(rawParams);
2332
- let control;
2333
- switch (params.action) {
2334
- case "list":
2335
- control = await listWorkflowControl(ctx, params.offset);
2336
- break;
2337
- case "start":
2338
- control = await queueToolLaunch(ctx, params.workflow, params.input ?? {});
2339
- break;
2340
- case "status":
2341
- control = await statusWorkflowControl(ctx, params.runId);
2342
- break;
2343
- case "pause":
2344
- control = pauseWorkflowControl(ctx);
2345
- break;
2346
- case "resume":
2347
- control = resumeWorkflowControl(ctx);
2348
- break;
2349
- case "cancel":
2350
- control = await cancelWorkflowControl(ctx, "agent", params.runId);
2351
- break;
2352
- case "answer": {
2353
- const waiting = await resolveWaitingWorkflow(ctx, params.runId);
2354
- const parent = readWorkflowRun(waiting.parentRunId);
2355
- if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
2356
- throw new Error("This checkpoint requires a verified human answer from Pi UI or a configured decision channel.");
2357
- }
2358
- control = await queueToolLaunch(ctx, waiting.workflowRef, params.input, {
2359
- parentRunId: waiting.parentRunId,
2360
- });
2361
- break;
2362
- }
2363
- case "update": {
2364
- if (!activeRun)
2365
- throw new Error("No workflow step is active.");
2366
- const receipt = await activeRun.engine.publishUpdate(params.step, params.attempt, params.update, toolCallId);
2367
- return {
2368
- content: [
2369
- { type: "text", text: "Workflow update published; the step remains active." },
2370
- ],
2371
- details: { action: "update", ...receipt },
2372
- };
2373
- }
2374
- case "submit": {
2375
- if (!activeRun) {
2376
- if (lastExpiredAttempt?.contract.attemptId === params.attempt &&
2377
- lastExpiredAttempt.contract.nodeId === params.step) {
2378
- throw new Error(`Workflow step ${JSON.stringify(params.step)} attempt ${JSON.stringify(params.attempt)} ${lastExpiredAttempt.reason}; its output is no longer accepted.`);
2666
+ return await withWorkflowToolOrder(async () => {
2667
+ const params = parseWorkflowToolInput(rawParams);
2668
+ let control;
2669
+ switch (params.action) {
2670
+ case "list":
2671
+ control = await listWorkflowControl(ctx, params.offset);
2672
+ break;
2673
+ case "start":
2674
+ control = await queueToolLaunch(ctx, params.workflow, params.input ?? {});
2675
+ break;
2676
+ case "status":
2677
+ control = await statusWorkflowControl(ctx, params.runId);
2678
+ break;
2679
+ case "pause":
2680
+ control = pauseWorkflowControl(ctx);
2681
+ break;
2682
+ case "resume":
2683
+ control = resumeWorkflowControl(ctx);
2684
+ break;
2685
+ case "cancel":
2686
+ control = await cancelWorkflowControl(ctx, "agent", params.runId);
2687
+ break;
2688
+ case "answer": {
2689
+ const waiting = await resolveWaitingWorkflow(ctx, params.runId);
2690
+ const parent = readWorkflowRun(waiting.parentRunId);
2691
+ if (parent !== null && humanDecisionRequest(parent.state.finalOutput) !== null) {
2692
+ throw new Error("This checkpoint requires a verified human answer from Pi UI or a configured decision channel.");
2379
2693
  }
2380
- throw new Error("No workflow step is waiting for output.");
2694
+ control = await queueToolLaunch(ctx, waiting.workflowRef, params.input, {
2695
+ parentRunId: waiting.parentRunId,
2696
+ });
2697
+ break;
2698
+ }
2699
+ case "change-settings":
2700
+ control = await changeWorkflowSettings(ctx, {
2701
+ ...(params.runId !== undefined ? { runId: params.runId } : {}),
2702
+ ...(params.scopeId !== undefined ? { scopeId: params.scopeId } : {}),
2703
+ ...(params.expectedChangeNumber !== undefined
2704
+ ? { expectedChangeNumber: params.expectedChangeNumber }
2705
+ : {}),
2706
+ patch: params.patch,
2707
+ requestId: `tool-${toolCallId}`,
2708
+ actor: "session",
2709
+ source: "workflow-tool",
2710
+ });
2711
+ break;
2712
+ case "queue-follow-up":
2713
+ control = await queueWorkflowFollowUp(ctx, {
2714
+ ...(params.runId !== undefined ? { runId: params.runId } : {}),
2715
+ prompt: params.prompt,
2716
+ requestId: `tool-${toolCallId}`,
2717
+ actor: "session",
2718
+ source: "workflow-tool",
2719
+ });
2720
+ break;
2721
+ case "remove-follow-up":
2722
+ control = await removeWorkflowFollowUp(ctx, {
2723
+ ...(params.runId !== undefined ? { runId: params.runId } : {}),
2724
+ followUpId: params.followUpId,
2725
+ actor: "session",
2726
+ source: "workflow-tool",
2727
+ });
2728
+ break;
2729
+ case "update": {
2730
+ if (!activeRun)
2731
+ throw new Error("No workflow step is active.");
2732
+ const receipt = await activeRun.engine.publishUpdate(params.step, params.attempt, params.update, toolCallId);
2733
+ return {
2734
+ content: [
2735
+ { type: "text", text: "Workflow update published; the step remains active." },
2736
+ ],
2737
+ details: { action: "update", ...receipt },
2738
+ };
2381
2739
  }
2382
- // Flush the conversation into SQLite before accepting, so the
2383
- // attempt range includes the assistant message carrying this call.
2384
- await activeRun.recorder?.record(ctx).catch(() => undefined);
2385
- await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
2386
- const result = await activeRun.executor.submit(params.step, params.attempt, params.output);
2387
- if (!result.accepted) {
2388
- throw new Error(result.message);
2740
+ case "submit": {
2741
+ if (!activeRun) {
2742
+ if (lastExpiredAttempt?.contract.attemptId === params.attempt &&
2743
+ lastExpiredAttempt.contract.nodeId === params.step) {
2744
+ throw new Error(`Workflow step ${JSON.stringify(params.step)} attempt ${JSON.stringify(params.attempt)} ${lastExpiredAttempt.reason}; its output is no longer accepted.`);
2745
+ }
2746
+ throw new Error("No workflow step is waiting for output.");
2747
+ }
2748
+ // Flush the conversation into SQLite before accepting, so the
2749
+ // attempt range includes the assistant message carrying this call.
2750
+ await activeRun.recorder?.record(ctx).catch(() => undefined);
2751
+ await activeRun.recorder?.synchronize(ctx).catch(() => undefined);
2752
+ const result = await activeRun.executor.submit(params.step, params.attempt, params.output);
2753
+ if (!result.accepted) {
2754
+ throw new Error(result.message);
2755
+ }
2756
+ return {
2757
+ content: [{ type: "text", text: result.message }],
2758
+ details: { action: "submit", step: params.step, accepted: true },
2759
+ };
2389
2760
  }
2390
- return {
2391
- content: [{ type: "text", text: result.message }],
2392
- details: { action: "submit", step: params.step, accepted: true },
2393
- };
2394
2761
  }
2395
- }
2396
- return {
2397
- content: [{ type: "text", text: control.message }],
2398
- details: control.details,
2399
- };
2762
+ return {
2763
+ content: [{ type: "text", text: control.message }],
2764
+ details: control.details,
2765
+ };
2766
+ });
2400
2767
  },
2401
2768
  });
2402
2769
  if (herdrEnabled) {
@@ -2419,6 +2786,16 @@ export default function piWorkflows(pi) {
2419
2786
  if (herdrEnabled)
2420
2787
  void refreshHerdrCapability(ctx);
2421
2788
  ensureRunQueueStore(ctx.cwd);
2789
+ ensureFollowUpDelivery();
2790
+ const sessionFollowUpStore = followUpStore;
2791
+ const sessionFollowUpCoordinator = followUpCoordinator;
2792
+ if (sessionFollowUpStore === null || sessionFollowUpCoordinator === null) {
2793
+ throw new Error("Workflow follow-up delivery did not initialize");
2794
+ }
2795
+ for (const runId of sessionFollowUpStore.listPendingPresentations(ctx.sessionManager.getSessionId())) {
2796
+ settlePresentationFromBranch(ctx, runId, "The final workflow response did not settle before the extension restarted");
2797
+ }
2798
+ await sessionFollowUpCoordinator.synchronize(ctx, false).catch(() => undefined);
2422
2799
  try {
2423
2800
  await reloadDecisionChannels(ctx);
2424
2801
  }
@@ -2522,6 +2899,10 @@ export default function piWorkflows(pi) {
2522
2899
  });
2523
2900
  pi.on("agent_settled", async (_event, ctx) => {
2524
2901
  systemTurnAbort = null;
2902
+ if (presentationPending !== null) {
2903
+ settlePresentationFromBranch(ctx, presentationPending.runId, "The final workflow response settled without durable session evidence");
2904
+ presentationPending = null;
2905
+ }
2525
2906
  if (activeRun === null) {
2526
2907
  try {
2527
2908
  const prepared = ensureRunQueueStore(ctx.cwd).findSessionReservation(ctx.sessionManager.getSessionId());
@@ -2538,7 +2919,7 @@ export default function piWorkflows(pi) {
2538
2919
  const run = activeRun;
2539
2920
  if (!run) {
2540
2921
  turnCoordinator.flushNatural(ctx.isIdle() && !sessionClosed && !runHeld());
2541
- presentationPending = null;
2922
+ await followUpCoordinator?.synchronize(ctx, false).catch(() => undefined);
2542
2923
  runSyncPass(ctx);
2543
2924
  return;
2544
2925
  }
@@ -2594,6 +2975,10 @@ export default function piWorkflows(pi) {
2594
2975
  controllerContext = null;
2595
2976
  runQueueStore?.close();
2596
2977
  runQueueStore = null;
2978
+ followUpCoordinator?.clear();
2979
+ followUpCoordinator = null;
2980
+ followUpStore?.close();
2981
+ followUpStore = null;
2597
2982
  presentationPending = null;
2598
2983
  clearWidgetTimer();
2599
2984
  stopWidgetTicker();