@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
@@ -10,14 +10,14 @@ import {
10
10
  type WorkflowRunView,
11
11
  type WorkflowSessionView,
12
12
  } from "../client/view.js";
13
- import type {
14
- SqliteResourceManagerStore,
15
- WorkflowRunQueueRecord,
16
- WorkflowRunQueueViewRecord,
17
- } from "../resource-managers/sqlite.js";
18
13
  import type { StateDatabase } from "../state/database.js";
19
14
  import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
20
15
  import { WorkflowMessageStore, type WorkflowMessage } from "../state/workflow-messages.js";
16
+ import type {
17
+ WorkflowRunQueueStore,
18
+ WorkflowRunQueueRecord,
19
+ WorkflowRunQueueViewRecord,
20
+ } from "../workflows/queue.js";
21
21
  import type { WorkflowRunDisplayState, WorkflowRunStore } from "../workflows/store.js";
22
22
  import type {
23
23
  WorkflowRunState,
@@ -74,7 +74,7 @@ export class ServerViewStore {
74
74
 
75
75
  constructor(
76
76
  private readonly state: StateDatabase,
77
- private readonly queue: SqliteResourceManagerStore,
77
+ private readonly queue: WorkflowRunQueueStore,
78
78
  private readonly serverState: ServerStateStore,
79
79
  private readonly runs: WorkflowRunStore,
80
80
  private readonly hasLiveRunner: (runId: string) => boolean,
@@ -274,6 +274,7 @@ export class ServerViewStore {
274
274
  schema: RUN_VIEW_SCHEMA,
275
275
  runId,
276
276
  revision,
277
+ runRevision: this.runs.runRevision(runId),
277
278
  display,
278
279
  manifest: manifest(queue, display.status),
279
280
  state: this.projectState(runId, loaded.state, stepPage.items, updatePage.items),
@@ -643,11 +644,7 @@ export class ServerViewStore {
643
644
  }
644
645
  if (message.status === "pending") return message.runId;
645
646
  if (message.status !== "sent") continue;
646
- const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
647
- if (turn === undefined || turn.state === "started") return message.runId;
648
- if (turn.endedAt !== null && Date.parse(turn.endedAt) + TERMINAL_VIEW_RETENTION_MS > now) {
649
- return message.runId;
650
- }
647
+ if (Date.parse(message.updatedAt) + TERMINAL_VIEW_RETENTION_MS > now) return message.runId;
651
648
  }
652
649
  return undefined;
653
650
  }
@@ -686,15 +683,15 @@ export class ServerViewStore {
686
683
  ) {
687
684
  return false;
688
685
  }
689
- const leaf = this.terminalChainLeaf(source.runId);
690
- if (leaf === undefined || leaf.status !== "completed") return false;
686
+ const run = this.state.connection
687
+ .prepare("SELECT status FROM runs WHERE run_id = ?")
688
+ .get(source.runId);
689
+ if (!isObjectRecord(run) || run.status !== "completed") return false;
691
690
  const terminal = this.workflowMessages
692
- .listRun(leaf.runId)
691
+ .listRun(source.runId)
693
692
  .filter((candidate) => candidate.kind === "terminal" && candidate.status === "sent")
694
693
  .at(-1);
695
694
  if (terminal === undefined) return false;
696
- const terminalTurn = this.workflowMessages.latestTurnForMessage(terminal.workflowMessageId);
697
- if (terminalTurn?.state !== "ended") return false;
698
695
  const prior = this.state.connection
699
696
  .prepare(
700
697
  `SELECT follow_up_id AS followUpId, status FROM workflow_follow_ups
@@ -729,24 +726,6 @@ export class ServerViewStore {
729
726
  return reservation === undefined;
730
727
  }
731
728
 
732
- private terminalChainLeaf(runId: string): { runId: string; status: string } | undefined {
733
- const row = this.state.connection
734
- .prepare(
735
- `WITH RECURSIVE chain(run_id, status, depth, created_at) AS (
736
- SELECT run_id, status, 0, created_at FROM runs WHERE run_id = ?
737
- UNION ALL
738
- SELECT child.run_id, child.status, chain.depth + 1, child.created_at
739
- FROM runs child JOIN chain ON child.parent_run_id = chain.run_id
740
- )
741
- SELECT run_id AS runId, status FROM chain
742
- ORDER BY depth DESC, created_at DESC LIMIT 1`,
743
- )
744
- .get(runId);
745
- return isObjectRecord(row) && typeof row.runId === "string" && typeof row.status === "string"
746
- ? { runId: row.runId, status: row.status }
747
- : undefined;
748
- }
749
-
750
729
  private openWorkflowMessage(messages: readonly WorkflowMessage[]): WorkflowMessage | undefined {
751
730
  for (const message of [...messages].reverse()) {
752
731
  if (message.status !== "sent") continue;
@@ -760,7 +739,7 @@ export class ServerViewStore {
760
739
  if (isObjectRecord(request) && request.status === "pending" && request.paused === 0) {
761
740
  return message;
762
741
  }
763
- } else if (message.kind === "terminal" || message.kind === "followUp") {
742
+ } else if (message.kind === "followUp") {
764
743
  const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
765
744
  if (turn === undefined || turn.state === "started") return message;
766
745
  }
@@ -794,7 +773,7 @@ export class ServerViewStore {
794
773
  ambiguous: this.hasAmbiguousEffect(queue.runId),
795
774
  runnerActive: this.hasLiveRunner(queue.runId),
796
775
  originTurnActive: this.hasActivity(queue.runId),
797
- pendingInteraction: this.hasPendingInteraction(queue.runId),
776
+ pendingRequestKind: this.pendingRequestKind(queue.runId),
798
777
  errorMessage: state?.error ?? queue.errorMessage,
799
778
  });
800
779
  }
@@ -805,7 +784,7 @@ export class ServerViewStore {
805
784
  `SELECT t.target_session_id AS targetSessionId FROM workflow_turns t
806
785
  JOIN workflow_messages m ON m.workflow_message_id = t.workflow_message_id
807
786
  WHERE t.run_id = ? AND t.state = 'started'
808
- AND m.kind IN ('step', 'terminal', 'followUp') LIMIT 1`,
787
+ AND m.kind IN ('step', 'followUp') LIMIT 1`,
809
788
  )
810
789
  .get(runId);
811
790
  return (
@@ -815,14 +794,13 @@ export class ServerViewStore {
815
794
  );
816
795
  }
817
796
 
818
- private hasPendingInteraction(runId: string): boolean {
797
+ private pendingRequestKind(runId: string): WorkflowDisplayFacts["pendingRequestKind"] {
819
798
  const row = this.state.connection
820
- .prepare(
821
- `SELECT 1 AS present FROM interactive_requests
822
- WHERE run_id = ? AND status = 'pending' LIMIT 1`,
823
- )
824
- .get(runId);
825
- return row !== undefined;
799
+ .prepare("SELECT kind FROM interactive_requests WHERE run_id = ? AND status = 'pending'")
800
+ .get(runId) as
801
+ | { kind: Exclude<WorkflowDisplayFacts["pendingRequestKind"], null> }
802
+ | undefined;
803
+ return row?.kind ?? null;
826
804
  }
827
805
 
828
806
  private hasAmbiguousEffect(runId: string): boolean {
@@ -857,7 +835,7 @@ export class ServerViewStore {
857
835
  this.workflowActivityRevision(),
858
836
  this.hasLiveRunner(runId),
859
837
  this.hasActivity(runId),
860
- this.hasPendingInteraction(runId),
838
+ this.pendingRequestKind(runId),
861
839
  this.hasAmbiguousEffect(runId),
862
840
  ].join(":");
863
841
  }
@@ -890,35 +868,49 @@ export type WorkflowDisplayFacts = {
890
868
  ambiguous: boolean;
891
869
  runnerActive: boolean;
892
870
  originTurnActive: boolean;
893
- pendingInteraction: boolean;
871
+ pendingRequestKind: "agent" | "assistant" | "checkpoint" | "decision" | null;
894
872
  errorMessage: string | null;
895
873
  };
896
874
 
897
875
  export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisplay {
898
876
  let status: WorkflowDisplayStatus;
899
- let activity: WorkflowDisplay["activity"] = null;
877
+ const activity: WorkflowDisplay["activity"] = facts.runnerActive
878
+ ? "supervised_runner"
879
+ : facts.originTurnActive
880
+ ? "origin_turn"
881
+ : null;
900
882
  let reason: string | null = null;
901
883
 
902
- if (facts.ambiguous) {
903
- status = "ambiguous";
904
- reason = "An external effect needs explicit recovery.";
905
- } else if (facts.runnerActive || facts.originTurnActive) {
906
- status = "running";
907
- activity = facts.runnerActive ? "supervised_runner" : "origin_turn";
908
- } else if (
884
+ if (
909
885
  facts.durableStatus === "completed" ||
910
886
  facts.durableStatus === "failed" ||
911
887
  facts.durableStatus === "timed_out" ||
912
888
  facts.durableStatus === "cancelled"
913
889
  ) {
914
890
  status = facts.durableStatus;
915
- reason = facts.errorMessage;
891
+ reason = facts.ambiguous
892
+ ? "The run is terminal, but an external effect still needs explicit recovery."
893
+ : facts.errorMessage;
894
+ } else if (facts.ambiguous) {
895
+ status = "ambiguous";
896
+ reason = "An external effect needs explicit recovery.";
916
897
  } else if (facts.paused) {
917
898
  status = "paused";
918
899
  reason = "The workflow is durably paused.";
919
- } else if (facts.pendingInteraction || facts.durableStatus === "waiting") {
900
+ } else if (facts.pendingRequestKind !== null || facts.durableStatus === "waiting") {
920
901
  status = "waiting";
921
- reason = "The workflow is waiting for origin-session input.";
902
+ reason =
903
+ facts.pendingRequestKind === "decision"
904
+ ? "The workflow needs a protected human decision."
905
+ : facts.pendingRequestKind === "checkpoint"
906
+ ? "The workflow needs a checkpoint answer."
907
+ : facts.pendingRequestKind === "agent"
908
+ ? "The workflow needs its assigned agent result."
909
+ : facts.pendingRequestKind === "assistant"
910
+ ? "The workflow needs its assigned visible response."
911
+ : "The workflow is waiting.";
912
+ } else if (activity !== null) {
913
+ status = "running";
922
914
  } else if (facts.queueStatus === "parked" || facts.queueStatus === "queued") {
923
915
  status = "queued";
924
916
  reason =
@@ -941,8 +933,12 @@ export function reduceWorkflowDisplay(facts: WorkflowDisplayFacts): WorkflowDisp
941
933
  if (facts.queueStatus === "parked") controls.push("resume");
942
934
  controls.push("cancel");
943
935
  }
944
- if (status === "waiting") controls.push("answer");
945
- if (status === "ambiguous") controls.push("review");
936
+ if (status === "waiting") {
937
+ if (facts.pendingRequestKind === "checkpoint") controls.push("answer");
938
+ if (facts.pendingRequestKind === "decision") controls.push("human-answer");
939
+ if (facts.pendingRequestKind === "agent") controls.push("update", "submit");
940
+ }
941
+ if (facts.ambiguous) controls.push("review");
946
942
  return { status, activity, controls, reason };
947
943
  }
948
944
 
@@ -983,7 +979,7 @@ function projectQueue(
983
979
  rootRunId: run.rootRunId,
984
980
  lineageKind: run.lineageKind,
985
981
  restartNumber: run.restartNumber,
986
- parentTerminalFingerprint: run.parentTerminalFingerprint,
982
+ parentRunRevision: run.parentRunRevision,
987
983
  errorCode: run.errorCode,
988
984
  createdAt: run.createdAt,
989
985
  updatedAt: run.updatedAt,
@@ -18,10 +18,8 @@ import type {
18
18
  AgentStepExecutor,
19
19
  AgentStepRequest,
20
20
  AgentStepSubmission,
21
- ResolvedHumanDecision,
22
21
  WorkflowDefinition,
23
22
  WorkflowMountedSource,
24
- WorkflowRunState,
25
23
  WorkflowSource,
26
24
  } from "../workflows/types.js";
27
25
  import { RpcStepExecutor } from "./rpc-executor.js";
@@ -43,7 +41,6 @@ import {
43
41
  } from "./workflow-runner-store.js";
44
42
 
45
43
  const STARTUP_ENV = "PI_WORKFLOWS_WORKFLOW_RUNNER_LAUNCH";
46
- const PRESENTATION_TIMEOUT_MS = 30_000;
47
44
 
48
45
  type WorkflowRunnerBootstrap = {
49
46
  command: WorkflowRunnerCommand;
@@ -198,7 +195,7 @@ class StdioRunnerTransport implements WorkflowRunnerStoreTransport {
198
195
 
199
196
  class InteractiveExecutor implements AgentStepExecutor {
200
197
  readonly assistantMessageMode = "visible" as const;
201
- readonly preservesDeadlineWhileParked = true;
198
+ readonly preservesActiveTimeBudget = true;
202
199
 
203
200
  constructor(
204
201
  private readonly store: ServerBackedWorkflowStore,
@@ -317,7 +314,7 @@ export function validateAcceptedAssistantSubmission(
317
314
  }
318
315
 
319
316
  export async function executeRunnerRunCommand(
320
- engine: Pick<WorkflowEngine, "run" | "resumeRun" | "continueRun">,
317
+ engine: Pick<WorkflowEngine, "run" | "resumeRun">,
321
318
  workflow: WorkflowDefinition,
322
319
  runId: string,
323
320
  workflowSource: WorkflowSource,
@@ -334,14 +331,6 @@ export async function executeRunnerRunCommand(
334
331
  ? {}
335
332
  : { resumeInteractionAttemptId: command.resumeInteractionAttemptId }),
336
333
  });
337
- case "continue":
338
- return await engine.continueRun(workflow, command.parentRunId, command.input, {
339
- runId,
340
- workflowSource,
341
- ...(command.humanDecision === undefined
342
- ? {}
343
- : { humanDecision: command.humanDecision as ResolvedHumanDecision }),
344
- });
345
334
  }
346
335
  }
347
336
 
@@ -421,11 +410,6 @@ export async function runWorkflowRunner(): Promise<number> {
421
410
  notificationSink: {
422
411
  notify: async (request) => await store.requestNotification(request),
423
412
  },
424
- onRunFinishing: async (_runId, state) => {
425
- if (state.status !== "completed" || workflow.presentationPrompt === undefined) return;
426
- const instructions = await resolvePresentationInstructions(workflow, state);
427
- await store.requestPresentation(instructions);
428
- },
429
413
  });
430
414
  try {
431
415
  const result = await executeRunnerRunCommand(
@@ -448,43 +432,6 @@ export async function runWorkflowRunner(): Promise<number> {
448
432
  }
449
433
  }
450
434
 
451
- async function resolvePresentationInstructions(
452
- workflow: WorkflowDefinition,
453
- state: WorkflowRunState,
454
- ): Promise<string> {
455
- const fallback = "Summarize the completed workflow result for the user in a normal response.";
456
- if (typeof workflow.presentationPrompt === "string") {
457
- return workflow.presentationPrompt.trim() || fallback;
458
- }
459
- if (workflow.presentationPrompt === undefined) return fallback;
460
- const controller = new AbortController();
461
- const timer = setTimeout(
462
- () => controller.abort(new Error("Workflow presentation prompt timed out")),
463
- PRESENTATION_TIMEOUT_MS,
464
- );
465
- timer.unref?.();
466
- const snapshot = structuredClone(state);
467
- try {
468
- const instructions = await Promise.race([
469
- workflow.presentationPrompt({
470
- state: snapshot,
471
- finalOutput: snapshot.finalOutput,
472
- signal: controller.signal,
473
- }),
474
- new Promise<never>((_resolve, reject) => {
475
- controller.signal.addEventListener("abort", () => reject(controller.signal.reason), {
476
- once: true,
477
- });
478
- }),
479
- ]);
480
- return instructions?.trim() || fallback;
481
- } catch {
482
- return fallback;
483
- } finally {
484
- clearTimeout(timer);
485
- }
486
- }
487
-
488
435
  type WorkflowRunnerSources = {
489
436
  root: WorkflowSource;
490
437
  mounted: WorkflowMountedSource[];
@@ -11,12 +11,6 @@ export const WORKFLOW_RUNNER_CONTENT_CHUNK_SCHEMA = "pi-workflows.worker-content
11
11
  export type WorkflowRunnerCommand =
12
12
  | { kind: "start"; input: JsonValue }
13
13
  | { kind: "resume"; resumeInteractionAttemptId?: string }
14
- | {
15
- kind: "continue";
16
- parentRunId: string;
17
- input: JsonValue;
18
- humanDecision?: JsonValue;
19
- }
20
14
  | { kind: "restart"; input: JsonValue };
21
15
 
22
16
  export const WORKFLOW_RUNNER_MESSAGE_KINDS = [
@@ -31,7 +25,6 @@ export const WORKFLOW_RUNNER_MESSAGE_KINDS = [
31
25
  "interaction.accepted",
32
26
  "interaction.rejected",
33
27
  "notification.requested",
34
- "presentation.requested",
35
28
  "effect.reserve",
36
29
  "effect.settle",
37
30
  "runner.progress",
@@ -43,13 +36,12 @@ export const WORKFLOW_RUNNER_STORE_OPERATIONS = [
43
36
  "store.initializeRun",
44
37
  "store.prepareRunResume",
45
38
  "store.readRunState",
46
- "store.writeSnapshot",
39
+ "store.commitTransition",
47
40
  "store.publishUpdate",
48
41
  "store.findSettingsScope",
49
42
  "store.ensureSettingsScope",
50
43
  "store.getSettingsScopeAtChange",
51
- "store.createHumanDecisionRequest",
52
- "store.readResolvedHumanDecision",
44
+ "store.readCheckpoint",
53
45
  "store.reserveEffect",
54
46
  "store.settleEffect",
55
47
  "content.read",
@@ -59,7 +51,6 @@ export const WORKFLOW_RUNNER_STORE_OPERATIONS = [
59
51
  "interaction.accept",
60
52
  "interaction.reject",
61
53
  "notification.request",
62
- "presentation.request",
63
54
  ] as const;
64
55
  export type WorkflowRunnerStoreOperation = (typeof WORKFLOW_RUNNER_STORE_OPERATIONS)[number];
65
56
  const WORKFLOW_RUNNER_CONTROL_OPERATIONS = ["runner.ready", "runner.exiting"] as const;
@@ -142,15 +133,14 @@ export function runnerKindForOperation(
142
133
  if (operation === "store.publishUpdate") return "node.update";
143
134
  if (operation === "store.reserveEffect") return "effect.reserve";
144
135
  if (operation === "store.settleEffect") return "effect.settle";
145
- if (operation === "store.createHumanDecisionRequest" || operation === "interaction.request") {
136
+ if (operation === "interaction.request") {
146
137
  return "interaction.requested";
147
138
  }
148
139
  if (operation === "interaction.accept") return "interaction.accepted";
149
140
  if (operation === "interaction.reject") return "interaction.rejected";
150
141
  if (operation === "notification.request") return "notification.requested";
151
- if (operation === "presentation.request") return "presentation.requested";
152
- if (operation === "store.writeSnapshot") {
153
- const event = payload.event;
142
+ if (operation === "store.commitTransition") {
143
+ const event = isRecord(payload.transition) ? payload.transition.event : undefined;
154
144
  const eventType = isRecord(event) && typeof event.type === "string" ? event.type : "";
155
145
  if (eventType === "node_started") return "node.started";
156
146
  if (eventType === "node_finished") return "node.finished";
@@ -6,9 +6,8 @@ import {
6
6
  type InitializeWorkflowRunOptions,
7
7
  type WorkflowExecutionStore,
8
8
  } from "../workflows/store.js";
9
+ import type { WorkflowTransition } from "../workflows/transitions.js";
9
10
  import type {
10
- HumanDecisionRequest,
11
- ResolvedHumanDecision,
12
11
  WorkflowDefinition,
13
12
  WorkflowEffectRecovery,
14
13
  WorkflowEffectReservation,
@@ -16,7 +15,6 @@ import type {
16
15
  WorkflowNotificationRequest,
17
16
  WorkflowRunState,
18
17
  WorkflowTraceEvent,
19
- WorkflowTraceEventDraft,
20
18
  WorkflowUpdateInput,
21
19
  WorkflowUpdateRecord,
22
20
  } from "../workflows/types.js";
@@ -71,28 +69,26 @@ export class ServerBackedWorkflowStore implements WorkflowExecutionStore {
71
69
  return await this.call<WorkflowRunState | null>("store.readRunState", { runId });
72
70
  }
73
71
 
74
- async writeSnapshot(
72
+ async commitTransition(
75
73
  runId: string,
76
- state: WorkflowRunState,
77
- event: WorkflowTraceEventDraft,
74
+ transition: WorkflowTransition,
78
75
  ): Promise<WorkflowTraceEvent> {
79
76
  return await this.call<WorkflowTraceEvent>(
80
- "store.writeSnapshot",
81
- { runId, state, event },
82
- event.attemptId,
77
+ "store.commitTransition",
78
+ { runId, transition },
79
+ transition.event.attemptId,
83
80
  );
84
81
  }
85
82
 
86
83
  async publishUpdate(
87
84
  runId: string,
88
- state: WorkflowRunState,
89
85
  nodeId: string,
90
86
  attemptId: string,
91
87
  update: WorkflowUpdateInput,
92
88
  ): Promise<{ event: WorkflowTraceEvent; record: WorkflowUpdateRecord }> {
93
89
  return await this.call<{ event: WorkflowTraceEvent; record: WorkflowUpdateRecord }>(
94
90
  "store.publishUpdate",
95
- { runId, state, nodeId, attemptId, update },
91
+ { runId, nodeId, attemptId, update },
96
92
  attemptId,
97
93
  );
98
94
  }
@@ -128,18 +124,11 @@ export class ServerBackedWorkflowStore implements WorkflowExecutionStore {
128
124
  );
129
125
  }
130
126
 
131
- async createHumanDecisionRequest(request: HumanDecisionRequest): Promise<"created" | "adopted"> {
132
- return await this.call<"created" | "adopted">(
133
- "store.createHumanDecisionRequest",
134
- { request },
135
- request.attemptId,
136
- );
137
- }
138
-
139
- async readResolvedHumanDecision(decisionId: string): Promise<ResolvedHumanDecision | null> {
140
- return await this.call<ResolvedHumanDecision | null>("store.readResolvedHumanDecision", {
141
- decisionId,
142
- });
127
+ async readCheckpoint(runId: string, attemptId: string) {
128
+ const value = await this.call<
129
+ import("../workflows/requests.js").WorkflowCheckpointState | null
130
+ >("store.readCheckpoint", { runId, attemptId }, attemptId);
131
+ return value ?? undefined;
143
132
  }
144
133
 
145
134
  async reserveEffect(options: {
@@ -204,10 +193,6 @@ export class ServerBackedWorkflowStore implements WorkflowExecutionStore {
204
193
  );
205
194
  }
206
195
 
207
- async requestPresentation(instructions: string): Promise<void> {
208
- await this.call("presentation.request", { instructions });
209
- }
210
-
211
196
  private async call<T>(
212
197
  operation: WorkflowRunnerStoreOperation,
213
198
  payload: Record<string, unknown>,
@@ -0,0 +1,122 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import type { StateDatabase } from "./database.js";
3
+
4
+ export type AttemptClock = { now(): number; monotonic(): number };
5
+ const realClock: AttemptClock = { now: () => Date.now(), monotonic: () => performance.now() };
6
+
7
+ type Interval = { attemptId: string; intervalNumber: number; elapsedMs: number };
8
+
9
+ /** Records active execution in the existing database. Wall time never determines the budget. */
10
+ export class AttemptTime {
11
+ private readonly anchors = new Map<string, number>();
12
+
13
+ constructor(
14
+ private readonly state: StateDatabase,
15
+ private readonly clock: AttemptClock = realClock,
16
+ ) {}
17
+
18
+ start(attemptId: string): void {
19
+ this.state.transaction(() => {
20
+ const open = this.openIntervals().find((interval) => interval.attemptId === attemptId);
21
+ if (open !== undefined) {
22
+ if (!this.anchors.has(intervalKey(open))) {
23
+ throw new Error(
24
+ "Recover the previous host's active intervals before starting model work",
25
+ );
26
+ }
27
+ return;
28
+ }
29
+ const row = this.state.connection
30
+ .prepare(
31
+ "SELECT COALESCE(MAX(interval_number), 0) + 1 AS next FROM attempt_active_intervals WHERE attempt_id = ?",
32
+ )
33
+ .get(attemptId) as { next: number };
34
+ const now = this.clock.now();
35
+ this.state.connection
36
+ .prepare(
37
+ "INSERT INTO attempt_active_intervals(attempt_id, interval_number, started_at, observed_at) VALUES (?, ?, ?, ?)",
38
+ )
39
+ .run(attemptId, row.next, now, now);
40
+ this.anchors.set(
41
+ intervalKey({ attemptId, intervalNumber: row.next }),
42
+ this.clock.monotonic(),
43
+ );
44
+ });
45
+ }
46
+
47
+ sample(): void {
48
+ const nested = this.state.connection.inTransaction;
49
+ const open = this.openIntervals();
50
+ this.state.transaction(() => {
51
+ for (const interval of open) this.sampleInterval(interval);
52
+ });
53
+ if (!nested) {
54
+ const active = new Set(open.map(intervalKey));
55
+ for (const key of this.anchors.keys()) {
56
+ if (!active.has(key)) this.anchors.delete(key);
57
+ }
58
+ }
59
+ }
60
+
61
+ stop(attemptId: string): void {
62
+ this.state.transaction(() => {
63
+ const interval = this.openIntervals().find((candidate) => candidate.attemptId === attemptId);
64
+ if (interval === undefined) return;
65
+ this.sampleInterval(interval);
66
+ closeAttemptTime(this.state, attemptId);
67
+ });
68
+ }
69
+
70
+ recover(): void {
71
+ // A crashed host can prove only the last saved sample, not time spent while offline.
72
+ this.state.connection
73
+ .prepare("UPDATE attempt_active_intervals SET ended_at = observed_at WHERE ended_at IS NULL")
74
+ .run();
75
+ this.anchors.clear();
76
+ }
77
+
78
+ private sampleInterval(interval: Interval): void {
79
+ const anchor = this.anchors.get(intervalKey(interval));
80
+ if (anchor === undefined) {
81
+ throw new Error("Active interval belongs to a previous host; recover it before sampling");
82
+ }
83
+ const elapsedMs = Math.max(interval.elapsedMs, this.clock.monotonic() - anchor, 0);
84
+ this.state.connection
85
+ .prepare(
86
+ `UPDATE attempt_active_intervals SET elapsed_ms = ?, observed_at = ?
87
+ WHERE attempt_id = ? AND interval_number = ? AND ended_at IS NULL`,
88
+ )
89
+ .run(elapsedMs, this.clock.now(), interval.attemptId, interval.intervalNumber);
90
+ }
91
+
92
+ private openIntervals(): Interval[] {
93
+ return this.state.connection
94
+ .prepare(
95
+ `SELECT attempt_id AS attemptId, interval_number AS intervalNumber, elapsed_ms AS elapsedMs
96
+ FROM attempt_active_intervals WHERE ended_at IS NULL`,
97
+ )
98
+ .all() as Interval[];
99
+ }
100
+ }
101
+
102
+ function intervalKey(interval: Pick<Interval, "attemptId" | "intervalNumber">): string {
103
+ return JSON.stringify([interval.attemptId, interval.intervalNumber]);
104
+ }
105
+
106
+ export function closeRunTime(state: StateDatabase, runId: string): void {
107
+ state.connection
108
+ .prepare(
109
+ `UPDATE attempt_active_intervals SET ended_at = observed_at
110
+ WHERE ended_at IS NULL AND attempt_id IN (SELECT attempt_id FROM node_attempts WHERE run_id = ?)`,
111
+ )
112
+ .run(runId);
113
+ }
114
+
115
+ /** Freeze the last host sample when an attempt settles outside its model turn. */
116
+ export function closeAttemptTime(state: StateDatabase, attemptId: string): void {
117
+ state.connection
118
+ .prepare(
119
+ "UPDATE attempt_active_intervals SET ended_at = observed_at WHERE attempt_id = ? AND ended_at IS NULL",
120
+ )
121
+ .run(attemptId);
122
+ }
package/src/state/json.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type JsonPrimitive = string | number | boolean | null;
2
- export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
2
+ export type JsonValue = JsonPrimitive | JsonValue[] | JsonObject;
3
+ export type JsonObject = { [key: string]: JsonValue };
3
4
 
4
5
  export function canonicalJson(value: unknown): string {
5
6
  return JSON.stringify(normalizeJson(value));