@osolmaz/pi-workflows 0.13.0 → 0.13.2

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 (84) hide show
  1. package/README.md +11 -9
  2. package/dist/builtins/autodoc.workflow.d.ts +191 -4
  3. package/dist/builtins/autodoc.workflow.js +156 -30
  4. package/dist/builtins/autodoc.workflow.js.map +1 -1
  5. package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
  6. package/dist/builtins/autoimplement-command-batches.js +29 -31
  7. package/dist/builtins/autoimplement-command-batches.js.map +1 -1
  8. package/dist/builtins/autoimplement.workflow.d.ts +1259 -29
  9. package/dist/builtins/autoimplement.workflow.js +416 -153
  10. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  11. package/dist/builtins/autoplan.workflow.d.ts +6 -0
  12. package/dist/builtins/autoplan.workflow.js +68 -32
  13. package/dist/builtins/autoplan.workflow.js.map +1 -1
  14. package/dist/builtins/catalog.js +5 -5
  15. package/dist/builtins/catalog.js.map +1 -1
  16. package/dist/builtins/change-verification.workflow.d.ts +110 -0
  17. package/dist/builtins/change-verification.workflow.js +860 -0
  18. package/dist/builtins/change-verification.workflow.js.map +1 -0
  19. package/dist/builtins/monitor.workflow.js +4 -3
  20. package/dist/builtins/monitor.workflow.js.map +1 -1
  21. package/dist/builtins/plain-summary.workflow.js +35 -24
  22. package/dist/builtins/plain-summary.workflow.js.map +1 -1
  23. package/dist/builtins/plan-change.workflow.d.ts +361 -6
  24. package/dist/builtins/plan-change.workflow.js +26 -0
  25. package/dist/builtins/plan-change.workflow.js.map +1 -1
  26. package/dist/builtins/sanity-check.workflow.js +19 -22
  27. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  28. package/dist/builtins/workspace-preparation.workflow.d.ts +75 -0
  29. package/dist/builtins/workspace-preparation.workflow.js +498 -0
  30. package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
  31. package/dist/controllers/sqlite.js +16 -51
  32. package/dist/controllers/sqlite.js.map +1 -1
  33. package/dist/extension/decision-channels.js +45 -7
  34. package/dist/extension/decision-channels.js.map +1 -1
  35. package/dist/extension/executor.js +1 -4
  36. package/dist/extension/executor.js.map +1 -1
  37. package/dist/extension/index.js +2 -15
  38. package/dist/extension/index.js.map +1 -1
  39. package/dist/extension/recorder.d.ts +1 -1
  40. package/dist/extension/recorder.js +8 -8
  41. package/dist/extension/recorder.js.map +1 -1
  42. package/dist/state/schema.js +22 -3
  43. package/dist/state/schema.js.map +1 -1
  44. package/dist/viewer/session-reducer.d.ts +0 -1
  45. package/dist/viewer/session-reducer.js +2 -24
  46. package/dist/viewer/session-reducer.js.map +1 -1
  47. package/dist/workflows/engine.js +4 -3
  48. package/dist/workflows/engine.js.map +1 -1
  49. package/dist/workflows/store.d.ts +6 -1
  50. package/dist/workflows/store.js +286 -33
  51. package/dist/workflows/store.js.map +1 -1
  52. package/dist/workflows/types.d.ts +1 -1
  53. package/docs/SQLITE_STATE.md +20 -14
  54. package/docs/WORKFLOW_COMPOSITION.md +8 -0
  55. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +1 -1
  56. package/docs/plans/2026-08-24-change-scoped-verification-plan.md +419 -0
  57. package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
  58. package/docs/session-event-journal.md +2 -3
  59. package/docs/workflows.md +43 -21
  60. package/herdr-plugin.toml +1 -1
  61. package/package.json +1 -1
  62. package/skills/autodoc/SKILL.md +7 -0
  63. package/skills/autoimplement/SKILL.md +4 -0
  64. package/src/builtins/autodoc.workflow.ts +184 -33
  65. package/src/builtins/autoimplement-command-batches.ts +39 -33
  66. package/src/builtins/autoimplement.workflow.ts +483 -175
  67. package/src/builtins/autoplan.workflow.ts +80 -32
  68. package/src/builtins/catalog.ts +5 -5
  69. package/src/builtins/change-verification.workflow.ts +1143 -0
  70. package/src/builtins/monitor.workflow.ts +6 -3
  71. package/src/builtins/plain-summary.workflow.ts +38 -40
  72. package/src/builtins/plan-change.workflow.ts +35 -0
  73. package/src/builtins/sanity-check.workflow.ts +19 -22
  74. package/src/builtins/workspace-preparation.workflow.ts +668 -0
  75. package/src/controllers/sqlite.ts +19 -53
  76. package/src/extension/decision-channels.ts +47 -7
  77. package/src/extension/executor.ts +1 -4
  78. package/src/extension/index.ts +2 -15
  79. package/src/extension/recorder.ts +10 -8
  80. package/src/state/schema.ts +22 -3
  81. package/src/viewer/session-reducer.ts +2 -29
  82. package/src/workflows/engine.ts +4 -3
  83. package/src/workflows/store.ts +397 -43
  84. package/src/workflows/types.ts +0 -1
@@ -7,8 +7,10 @@ import type {
7
7
  WorkflowDefinition,
8
8
  WorkflowDefinitionSnapshot,
9
9
  WorkflowNodeDefinition,
10
+ WorkflowNodeResult,
10
11
  WorkflowNodeSnapshot,
11
12
  WorkflowRunState,
13
+ WorkflowStepRecord,
12
14
  WorkflowSessionBinding,
13
15
  WorkflowSessionCapture,
14
16
  WorkflowSessionEntryRecord,
@@ -58,9 +60,57 @@ type RunContext = {
58
60
  type RunRow = {
59
61
  runId: string;
60
62
  resourceId: string;
61
- stateHash: Buffer;
62
63
  definitionHash: Buffer;
64
+ definitionDigest: Buffer;
65
+ workflowRef: string;
66
+ sourceRef: string;
67
+ workflowSourceHash: Buffer;
68
+ workflowSourcesHash: Buffer | null;
69
+ parentRunId: string | null;
70
+ title: string | null;
63
71
  status: string;
72
+ paused: number;
73
+ statusDetail: string | null;
74
+ inputHash: Buffer;
75
+ humanDecisionHash: Buffer | null;
76
+ finalOutputHash: Buffer | null;
77
+ errorHash: Buffer | null;
78
+ carriedStepCount: number;
79
+ currentNode: string | null;
80
+ currentAttemptId: string | null;
81
+ currentNodeStartedAt: number | null;
82
+ waitingOn: string | null;
83
+ createdAt: number;
84
+ updatedAt: number;
85
+ finishedAt: number | null;
86
+ };
87
+
88
+ type StoredStepMetadata = Pick<WorkflowStepRecord, "action" | "assistantMessage" | "conversation">;
89
+
90
+ type StepRow = {
91
+ stepIndex: number;
92
+ attemptId: string;
93
+ nodeId: string;
94
+ nodeType: WorkflowStepRecord["nodeType"];
95
+ status: string;
96
+ promptHash: Buffer | null;
97
+ outputHash: Buffer | null;
98
+ outputOverrideHash: Buffer | null;
99
+ stepMetadataHash: Buffer | null;
100
+ errorHash: Buffer | null;
101
+ startedAt: number;
102
+ finishedAt: number;
103
+ };
104
+
105
+ type UpdateRow = {
106
+ updateId: string;
107
+ runRevision: number;
108
+ nodeId: string;
109
+ attemptId: string;
110
+ updateType: string;
111
+ updateKey: string;
112
+ dataHash: Buffer;
113
+ recordedAt: number;
64
114
  };
65
115
 
66
116
  type EventRow = {
@@ -228,7 +278,14 @@ export class WorkflowRunStore {
228
278
  const inputHash = this.state.putJson(state.input, now);
229
279
  const workflowSourceHash = this.state.putJson(state.workflowSource ?? source, now);
230
280
  const launchOptionsHash = this.state.putJson({}, now);
231
- const stateHash = this.state.putJson(state, now);
281
+ const workflowSourcesHash =
282
+ state.workflowSources === undefined
283
+ ? null
284
+ : this.state.putJson(state.workflowSources, now);
285
+ const humanDecisionHash =
286
+ state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
287
+ const finalOutputHash =
288
+ state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
232
289
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
233
290
  this.state.connection
234
291
  .prepare(
@@ -236,9 +293,11 @@ export class WorkflowRunStore {
236
293
  run_id, resource_id, project_id, parent_run_id, definition_digest,
237
294
  workflow_ref, workflow_source_hash, launch_options_hash,
238
295
  source_type, source_ref, source_revision, title, status, paused,
239
- status_detail, input_hash, output_hash, error_hash,
296
+ status_detail, input_hash, workflow_sources_hash, human_decision_hash,
297
+ final_output_hash, error_hash, carried_step_count, current_node,
298
+ current_attempt_id, current_node_started_at, waiting_on,
240
299
  created_at, updated_at, finished_at
241
- ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
300
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
242
301
  )
243
302
  .run(
244
303
  state.runId,
@@ -256,8 +315,17 @@ export class WorkflowRunStore {
256
315
  state.paused === true ? 1 : 0,
257
316
  state.statusDetail ?? null,
258
317
  inputHash,
259
- stateHash,
318
+ workflowSourcesHash,
319
+ humanDecisionHash,
320
+ finalOutputHash,
260
321
  errorHash,
322
+ state.carriedStepCount ?? 0,
323
+ state.currentNode ?? null,
324
+ state.currentAttemptId ?? null,
325
+ state.currentNodeStartedAt === undefined
326
+ ? null
327
+ : Date.parse(state.currentNodeStartedAt),
328
+ state.waitingOn ?? null,
261
329
  Date.parse(state.startedAt),
262
330
  now,
263
331
  state.finishedAt === undefined ? null : Date.parse(state.finishedAt),
@@ -393,10 +461,20 @@ export class WorkflowRunStore {
393
461
  this.state.connection
394
462
  .prepare(
395
463
  `INSERT INTO workflow_updates(
396
- update_id, attempt_id, update_seq, update_type, update_key, data_hash, recorded_at
397
- ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
464
+ update_id, attempt_id, update_seq, run_revision,
465
+ update_type, update_key, data_hash, recorded_at
466
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
398
467
  )
399
- .run(updateId, attemptId, nextUpdateSeq, update.type, update.key, dataHash, Date.now());
468
+ .run(
469
+ updateId,
470
+ attemptId,
471
+ nextUpdateSeq,
472
+ revision,
473
+ update.type,
474
+ update.key,
475
+ dataHash,
476
+ Date.now(),
477
+ );
400
478
  insertRunEvent(this.state, run.resourceId, revision, at, event);
401
479
  this.persistRunState(run, state, revision, Date.now());
402
480
  context.revision = revision;
@@ -528,7 +606,7 @@ export class WorkflowRunStore {
528
606
  ...(attemptId !== undefined ? { captureAttemptId: attemptId } : {}),
529
607
  },
530
608
  });
531
- const current = this.readState(run.stateHash);
609
+ const current = this.readRunState(run, this.readDefinition(run.definitionHash));
532
610
  current.traceSeq = revision;
533
611
  current.updatedAt = at;
534
612
  this.persistRunState(run, current, revision, now);
@@ -616,24 +694,9 @@ export class WorkflowRunStore {
616
694
  );
617
695
  expected += 1;
618
696
  }
619
- const now = Date.now();
620
697
  this.state.connection
621
698
  .prepare("UPDATE session_segments SET event_count = ? WHERE segment_id = ?")
622
699
  .run(expected - 1, current.segmentId);
623
- const resourceId = segmentResourceId(current);
624
- const revision = this.requireResourceRevision(resourceId);
625
- this.bumpResource(resourceId, revision, now);
626
- const payloadHash = this.state.putJson({ count: records.length, lastSeq: expected - 1 }, now);
627
- insertGenericEvent(
628
- this.state,
629
- resourceId,
630
- revision + 1,
631
- "session.events_appended",
632
- "session",
633
- current.sessionId,
634
- payloadHash,
635
- now,
636
- );
637
700
  });
638
701
  }
639
702
 
@@ -698,8 +761,8 @@ export class WorkflowRunStore {
698
761
  readRun(runId: string, options: ReadWorkflowRunOptions = {}): LoadedWorkflowRun | null {
699
762
  const row = this.readRunRow(runId);
700
763
  if (row === undefined) return null;
701
- const state = this.readState(row.stateHash);
702
764
  const snapshot = this.readDefinition(row.definitionHash);
765
+ const state = this.readRunState(row, snapshot);
703
766
  const segments = this.segmentRows(runId).map((segment) => this.loadSegment(segment, state));
704
767
  const flat = segments.find((segment) => segment.attemptId === "") ?? emptySegment();
705
768
  return {
@@ -831,13 +894,20 @@ export class WorkflowRunStore {
831
894
  now: number,
832
895
  ): void {
833
896
  this.bumpResource(run.resourceId, expectedRevision - 1, now);
834
- const stateHash = this.state.putJson(state, now);
897
+ const workflowSourcesHash =
898
+ state.workflowSources === undefined ? null : this.state.putJson(state.workflowSources, now);
899
+ const humanDecisionHash =
900
+ state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
901
+ const finalOutputHash =
902
+ state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
835
903
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
836
904
  const update = this.state.connection
837
905
  .prepare(
838
906
  `UPDATE runs
839
- SET title = ?, status = ?, paused = ?, status_detail = ?, output_hash = ?,
840
- error_hash = ?, updated_at = ?, finished_at = ?
907
+ SET title = ?, status = ?, paused = ?, status_detail = ?,
908
+ workflow_sources_hash = ?, human_decision_hash = ?, final_output_hash = ?,
909
+ error_hash = ?, carried_step_count = ?, current_node = ?, current_attempt_id = ?,
910
+ current_node_started_at = ?, waiting_on = ?, updated_at = ?, finished_at = ?
841
911
  WHERE run_id = ?`,
842
912
  )
843
913
  .run(
@@ -845,8 +915,15 @@ export class WorkflowRunStore {
845
915
  state.status,
846
916
  state.paused === true ? 1 : 0,
847
917
  state.statusDetail ?? null,
848
- stateHash,
918
+ workflowSourcesHash,
919
+ humanDecisionHash,
920
+ finalOutputHash,
849
921
  errorHash,
922
+ state.carriedStepCount ?? 0,
923
+ state.currentNode ?? null,
924
+ state.currentAttemptId ?? null,
925
+ state.currentNodeStartedAt === undefined ? null : Date.parse(state.currentNodeStartedAt),
926
+ state.waitingOn ?? null,
850
927
  now,
851
928
  state.finishedAt === undefined ? null : Date.parse(state.finishedAt),
852
929
  state.runId,
@@ -855,7 +932,6 @@ export class WorkflowRunStore {
855
932
  if (state.status !== "running") {
856
933
  this.enqueueRunSettlementEffect(run.resourceId, expectedRevision, state, now);
857
934
  }
858
- run.stateHash = stateHash;
859
935
  }
860
936
 
861
937
  private enqueueRunSettlementEffect(
@@ -931,7 +1007,9 @@ export class WorkflowRunStore {
931
1007
  now: number,
932
1008
  ): void {
933
1009
  for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
934
- const resultHash = this.state.putJson(step, now);
1010
+ const metadata = stepMetadata(step);
1011
+ const stepMetadataHash =
1012
+ Object.keys(metadata).length === 0 ? null : this.state.putJson(metadata, now);
935
1013
  const outputHash = step.output === undefined ? null : this.state.putJson(step.output, now);
936
1014
  const errorHash = step.error === undefined ? null : this.state.putText(step.error, now);
937
1015
  const promptHash = step.prompt === null ? null : this.state.putJson(step.prompt, now);
@@ -944,7 +1022,7 @@ export class WorkflowRunStore {
944
1022
  .prepare(
945
1023
  `INSERT INTO node_attempts(
946
1024
  attempt_id, run_id, node_id, attempt_number, node_type, status,
947
- presentation_hash, output_hash, result_hash, error_hash,
1025
+ prompt_hash, output_hash, step_metadata_hash, error_hash,
948
1026
  started_at, finished_at, created_at, updated_at
949
1027
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
950
1028
  )
@@ -957,7 +1035,7 @@ export class WorkflowRunStore {
957
1035
  outcomeStatus(step.outcome),
958
1036
  promptHash,
959
1037
  outputHash,
960
- resultHash,
1038
+ stepMetadataHash,
961
1039
  errorHash,
962
1040
  Date.parse(step.startedAt),
963
1041
  Date.parse(step.finishedAt),
@@ -968,7 +1046,7 @@ export class WorkflowRunStore {
968
1046
  this.state.connection
969
1047
  .prepare(
970
1048
  `UPDATE node_attempts
971
- SET status = ?, presentation_hash = ?, output_hash = ?, result_hash = ?,
1049
+ SET status = ?, prompt_hash = ?, output_hash = ?, step_metadata_hash = ?,
972
1050
  error_hash = ?, finished_at = ?, updated_at = ?
973
1051
  WHERE attempt_id = ?`,
974
1052
  )
@@ -976,7 +1054,7 @@ export class WorkflowRunStore {
976
1054
  outcomeStatus(step.outcome),
977
1055
  promptHash,
978
1056
  outputHash,
979
- resultHash,
1057
+ stepMetadataHash,
980
1058
  errorHash,
981
1059
  Date.parse(step.finishedAt),
982
1060
  now,
@@ -987,6 +1065,49 @@ export class WorkflowRunStore {
987
1065
  if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
988
1066
  this.ensureAttempt(state, state.currentNode, state.currentAttemptId, now, snapshot);
989
1067
  }
1068
+ this.syncRunSteps(state, now);
1069
+ }
1070
+
1071
+ private syncRunSteps(state: WorkflowRunState, now: number): void {
1072
+ const existingRows = this.state.connection
1073
+ .prepare(
1074
+ `SELECT step_index AS stepIndex, attempt_id AS attemptId
1075
+ FROM run_steps WHERE run_id = ? ORDER BY step_index`,
1076
+ )
1077
+ .all(state.runId)
1078
+ .filter(isRunStepIdentityRow);
1079
+ if (existingRows.length > state.steps.length) {
1080
+ throw new Error(`Workflow run steps cannot shrink: ${state.runId}`);
1081
+ }
1082
+ for (const existing of existingRows) {
1083
+ if (state.steps[existing.stepIndex]?.attemptId !== existing.attemptId) {
1084
+ throw new Error(`Workflow run step history changed at index ${existing.stepIndex}`);
1085
+ }
1086
+ }
1087
+ for (let stepIndex = existingRows.length; stepIndex < state.steps.length; stepIndex += 1) {
1088
+ const step = state.steps[stepIndex];
1089
+ /* istanbul ignore if -- array index follows a checked bound */
1090
+ if (step === undefined) throw new Error("Workflow run step became unavailable");
1091
+ const attempt = this.state.connection
1092
+ .prepare("SELECT output_hash AS outputHash FROM node_attempts WHERE attempt_id = ?")
1093
+ .get(step.attemptId);
1094
+ if (!isAttemptOutputRow(attempt)) {
1095
+ throw new Error(`Workflow node attempt is missing: ${step.attemptId}`);
1096
+ }
1097
+ const carried = stepIndex < (state.carriedStepCount ?? 0);
1098
+ const outputOverrideHash =
1099
+ carried &&
1100
+ (attempt.outputHash === null ||
1101
+ canonicalJson(this.state.readJson(attempt.outputHash)) !== canonicalJson(step.output))
1102
+ ? this.state.putJson(step.output, now)
1103
+ : null;
1104
+ this.state.connection
1105
+ .prepare(
1106
+ `INSERT INTO run_steps(run_id, step_index, attempt_id, output_override_hash)
1107
+ VALUES (?, ?, ?, ?)`,
1108
+ )
1109
+ .run(state.runId, stepIndex, step.attemptId, outputOverrideHash);
1110
+ }
990
1111
  }
991
1112
 
992
1113
  private ensureAttempt(
@@ -1057,8 +1178,17 @@ export class WorkflowRunStore {
1057
1178
  const row = this.state.connection
1058
1179
  .prepare(
1059
1180
  `SELECT r.run_id AS runId, r.resource_id AS resourceId,
1060
- r.output_hash AS stateHash, d.definition_hash AS definitionHash,
1061
- r.status
1181
+ d.definition_hash AS definitionHash, r.definition_digest AS definitionDigest,
1182
+ d.workflow_name AS workflowRef, r.source_ref AS sourceRef,
1183
+ r.workflow_source_hash AS workflowSourceHash,
1184
+ r.workflow_sources_hash AS workflowSourcesHash, r.parent_run_id AS parentRunId,
1185
+ r.title, r.status, r.paused, r.status_detail AS statusDetail,
1186
+ r.input_hash AS inputHash, r.human_decision_hash AS humanDecisionHash,
1187
+ r.final_output_hash AS finalOutputHash, r.error_hash AS errorHash,
1188
+ r.carried_step_count AS carriedStepCount, r.current_node AS currentNode,
1189
+ r.current_attempt_id AS currentAttemptId,
1190
+ r.current_node_started_at AS currentNodeStartedAt, r.waiting_on AS waitingOn,
1191
+ r.created_at AS createdAt, r.updated_at AS updatedAt, r.finished_at AS finishedAt
1062
1192
  FROM runs r
1063
1193
  JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
1064
1194
  WHERE r.run_id = ?`,
@@ -1073,12 +1203,151 @@ export class WorkflowRunStore {
1073
1203
  return row;
1074
1204
  }
1075
1205
 
1076
- private readState(hash: Buffer): WorkflowRunState {
1077
- const value = this.state.readJson(hash);
1078
- if (!isRecord(value) || value.schema !== RUN_STATE_SCHEMA) {
1079
- throw new Error("Workflow run state has an incompatible schema");
1206
+ private readRunState(row: RunRow, snapshot: WorkflowDefinitionSnapshot): WorkflowRunState {
1207
+ const steps = this.readSteps(row.runId);
1208
+ const outputs: Record<string, unknown> = {};
1209
+ const results: Record<string, WorkflowNodeResult> = {};
1210
+ for (const step of steps) {
1211
+ const result = resultForStep(step);
1212
+ results[step.nodeId] = result;
1213
+ if (step.outcome === "ok") outputs[step.nodeId] = step.output;
1214
+ else delete outputs[step.nodeId];
1215
+ const node = snapshot.nodes[step.nodeId];
1216
+ const mountPath =
1217
+ node?.includeTransition === "exit" && node.mountPath !== undefined
1218
+ ? node.mountPath.join("/")
1219
+ : undefined;
1220
+ if (mountPath !== undefined && step.outcome === "ok") {
1221
+ outputs[mountPath] = step.output;
1222
+ results[mountPath] = { ...result, nodeId: mountPath };
1223
+ }
1080
1224
  }
1081
- return value as WorkflowRunState;
1225
+ const revision = this.requireResourceRevision(row.resourceId);
1226
+ return {
1227
+ schema: RUN_STATE_SCHEMA,
1228
+ traceSeq: revision,
1229
+ runId: row.runId,
1230
+ workflowName: row.workflowRef,
1231
+ ...(row.parentRunId === null ? {} : { parentRunId: row.parentRunId }),
1232
+ ...(row.carriedStepCount === 0 ? {} : { carriedStepCount: row.carriedStepCount }),
1233
+ ...(row.title === null ? {} : { runTitle: row.title }),
1234
+ ...(row.sourceRef.startsWith("inline:")
1235
+ ? {}
1236
+ : { workflowSource: this.readJsonAs(row.workflowSourceHash) }),
1237
+ ...(row.workflowSourcesHash === null
1238
+ ? {}
1239
+ : { workflowSources: this.readJsonAs(row.workflowSourcesHash) }),
1240
+ ...(row.workflowSourcesHash !== null || snapshot.composition?.mounts.length
1241
+ ? { definitionDigest: `sha256:${row.definitionDigest.toString("hex")}` }
1242
+ : {}),
1243
+ startedAt: new Date(row.createdAt).toISOString(),
1244
+ ...(row.finishedAt === null ? {} : { finishedAt: new Date(row.finishedAt).toISOString() }),
1245
+ updatedAt: new Date(row.updatedAt).toISOString(),
1246
+ status: row.status as WorkflowRunState["status"],
1247
+ input: this.state.readJson(row.inputHash),
1248
+ outputs,
1249
+ results,
1250
+ steps,
1251
+ ...this.readUpdates(row.runId),
1252
+ ...(row.currentNode === null ? {} : { currentNode: row.currentNode }),
1253
+ ...(row.currentAttemptId === null ? {} : { currentAttemptId: row.currentAttemptId }),
1254
+ ...(row.currentNodeStartedAt === null
1255
+ ? {}
1256
+ : { currentNodeStartedAt: new Date(row.currentNodeStartedAt).toISOString() }),
1257
+ ...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
1258
+ ...(row.humanDecisionHash === null
1259
+ ? {}
1260
+ : { humanDecision: this.readJsonAs(row.humanDecisionHash) }),
1261
+ ...(row.paused === 0 ? {} : { paused: true }),
1262
+ ...(row.waitingOn === null ? {} : { waitingOn: row.waitingOn }),
1263
+ ...(row.finalOutputHash === null
1264
+ ? {}
1265
+ : { finalOutput: this.state.readJson(row.finalOutputHash) }),
1266
+ ...(row.errorHash === null ? {} : { error: this.readText(row.errorHash) }),
1267
+ };
1268
+ }
1269
+
1270
+ private readSteps(runId: string): WorkflowStepRecord[] {
1271
+ const rows = this.state.connection
1272
+ .prepare(
1273
+ `SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
1274
+ a.node_type AS nodeType, a.status, a.prompt_hash AS promptHash,
1275
+ a.output_hash AS outputHash, s.output_override_hash AS outputOverrideHash,
1276
+ a.step_metadata_hash AS stepMetadataHash, a.error_hash AS errorHash,
1277
+ a.started_at AS startedAt, a.finished_at AS finishedAt
1278
+ FROM run_steps s
1279
+ JOIN node_attempts a ON a.attempt_id = s.attempt_id
1280
+ WHERE s.run_id = ? ORDER BY s.step_index`,
1281
+ )
1282
+ .all(runId)
1283
+ .filter(isStepRow);
1284
+ return rows.map((row, index) => {
1285
+ if (row.stepIndex !== index)
1286
+ throw new Error(`Workflow run step sequence has a gap: ${runId}`);
1287
+ const metadata =
1288
+ row.stepMetadataHash === null
1289
+ ? {}
1290
+ : this.readJsonAs<StoredStepMetadata>(row.stepMetadataHash);
1291
+ const prompt = row.promptHash === null ? null : this.readJsonAs<string>(row.promptHash);
1292
+ const outputHash = row.outputOverrideHash ?? row.outputHash;
1293
+ const output = outputHash === null ? null : this.state.readJson(outputHash);
1294
+ const error = row.errorHash === null ? undefined : this.readText(row.errorHash);
1295
+ return {
1296
+ attemptId: row.attemptId,
1297
+ nodeId: row.nodeId,
1298
+ nodeType: row.nodeType,
1299
+ outcome: outcomeForStatus(row.status),
1300
+ startedAt: new Date(row.startedAt).toISOString(),
1301
+ finishedAt: new Date(row.finishedAt).toISOString(),
1302
+ prompt,
1303
+ output,
1304
+ ...(error === undefined ? {} : { error }),
1305
+ ...metadata,
1306
+ };
1307
+ });
1308
+ }
1309
+
1310
+ private readUpdates(runId: string): Pick<WorkflowRunState, "updates"> {
1311
+ const rows = this.state.connection
1312
+ .prepare(
1313
+ `SELECT u.update_id AS updateId, u.run_revision AS runRevision,
1314
+ a.node_id AS nodeId, u.attempt_id AS attemptId,
1315
+ u.update_type AS updateType, u.update_key AS updateKey,
1316
+ u.data_hash AS dataHash, u.recorded_at AS recordedAt
1317
+ FROM workflow_updates u
1318
+ JOIN node_attempts a ON a.attempt_id = u.attempt_id
1319
+ WHERE a.run_id = ? ORDER BY u.run_revision`,
1320
+ )
1321
+ .all(runId)
1322
+ .filter(isUpdateRow);
1323
+ if (rows.length === 0) return {};
1324
+ let updates: WorkflowUpdateRecord[] | undefined;
1325
+ for (const row of rows) {
1326
+ updates = updateProjection(updates, {
1327
+ updateId: row.updateId,
1328
+ seq: row.runRevision,
1329
+ at: new Date(row.recordedAt).toISOString(),
1330
+ runId,
1331
+ nodeId: row.nodeId,
1332
+ attemptId: row.attemptId,
1333
+ type: row.updateType,
1334
+ key: row.updateKey,
1335
+ data: this.readJsonAs<Record<string, unknown>>(row.dataHash),
1336
+ });
1337
+ }
1338
+ return updates === undefined ? {} : { updates };
1339
+ }
1340
+
1341
+ private readJsonAs<T>(hash: Buffer): T {
1342
+ return this.state.readJson(hash) as T;
1343
+ }
1344
+
1345
+ private readText(hash: Buffer): string {
1346
+ const blob = this.state.readBlob(hash);
1347
+ if (blob === undefined || blob.mediaType !== "text/plain") {
1348
+ throw new Error("Text blob is missing or has the wrong media type");
1349
+ }
1350
+ return blob.content.toString("utf8");
1082
1351
  }
1083
1352
 
1084
1353
  private readDefinition(hash: Buffer): WorkflowDefinitionSnapshot {
@@ -1318,12 +1587,13 @@ function insertRunEvent(
1318
1587
  at: string,
1319
1588
  event: WorkflowTraceEventDraft | WorkflowTraceEvent,
1320
1589
  ): void {
1590
+ const payload = compactTracePayload(event.type, event.payload);
1321
1591
  const payloadHash = state.putJson(
1322
1592
  {
1323
1593
  scope: event.scope,
1324
1594
  ...(event.nodeId === undefined ? {} : { nodeId: event.nodeId }),
1325
1595
  ...(event.attemptId === undefined ? {} : { attemptId: event.attemptId }),
1326
- payload: event.payload,
1596
+ payload,
1327
1597
  },
1328
1598
  Date.parse(at),
1329
1599
  );
@@ -1339,6 +1609,28 @@ function insertRunEvent(
1339
1609
  );
1340
1610
  }
1341
1611
 
1612
+ function compactTracePayload(
1613
+ eventType: string,
1614
+ payload: Record<string, unknown>,
1615
+ ): Record<string, unknown> {
1616
+ if (
1617
+ (eventType === "node_finished" || eventType === "include_exited") &&
1618
+ Object.hasOwn(payload, "output")
1619
+ ) {
1620
+ const { output: _output, ...rest } = payload;
1621
+ return { ...rest, outputStored: true };
1622
+ }
1623
+ if (eventType === "run_started" && Object.hasOwn(payload, "input")) {
1624
+ const { input: _input, ...rest } = payload;
1625
+ return { ...rest, inputStored: true };
1626
+ }
1627
+ if (Object.hasOwn(payload, "finalOutput")) {
1628
+ const { finalOutput: _finalOutput, ...rest } = payload;
1629
+ return { ...rest, finalOutputStored: true };
1630
+ }
1631
+ return payload;
1632
+ }
1633
+
1342
1634
  function insertGenericEvent(
1343
1635
  state: StateDatabase,
1344
1636
  resourceId: string,
@@ -1407,6 +1699,43 @@ function outcomeStatus(outcome: string): string {
1407
1699
  }
1408
1700
  }
1409
1701
 
1702
+ function outcomeForStatus(status: string): WorkflowStepRecord["outcome"] {
1703
+ switch (status) {
1704
+ case "completed":
1705
+ return "ok";
1706
+ case "timed_out":
1707
+ return "timed_out";
1708
+ case "cancelled":
1709
+ return "cancelled";
1710
+ case "failed":
1711
+ return "failed";
1712
+ default:
1713
+ throw new Error(`Workflow step has nonterminal status: ${status}`);
1714
+ }
1715
+ }
1716
+
1717
+ function stepMetadata(step: WorkflowStepRecord): StoredStepMetadata {
1718
+ return {
1719
+ ...(step.action === undefined ? {} : { action: step.action }),
1720
+ ...(step.assistantMessage === undefined ? {} : { assistantMessage: step.assistantMessage }),
1721
+ ...(step.conversation === undefined ? {} : { conversation: step.conversation }),
1722
+ };
1723
+ }
1724
+
1725
+ function resultForStep(step: WorkflowStepRecord): WorkflowNodeResult {
1726
+ return {
1727
+ attemptId: step.attemptId,
1728
+ nodeId: step.nodeId,
1729
+ nodeType: step.nodeType,
1730
+ outcome: step.outcome,
1731
+ startedAt: step.startedAt,
1732
+ finishedAt: step.finishedAt,
1733
+ durationMs: Date.parse(step.finishedAt) - Date.parse(step.startedAt),
1734
+ ...(step.outcome === "ok" ? { output: step.output } : {}),
1735
+ ...(step.error === undefined ? {} : { error: step.error }),
1736
+ };
1737
+ }
1738
+
1410
1739
  function traceScope(value: unknown): WorkflowTraceEvent["scope"] {
1411
1740
  return value === "node" || value === "agent" || value === "action" || value === "session"
1412
1741
  ? value
@@ -1438,6 +1767,12 @@ function validateSessionEventRecord(record: WorkflowSessionEventRecord): void {
1438
1767
  ) {
1439
1768
  throw new Error("Session event is missing required envelope fields");
1440
1769
  }
1770
+ if (
1771
+ record.type === "assistant_event" &&
1772
+ ["text_delta", "thinking_delta", "toolcall_delta"].includes(String(record.payload.type))
1773
+ ) {
1774
+ throw new Error("Incremental assistant events are not durable session facts");
1775
+ }
1441
1776
  }
1442
1777
 
1443
1778
  function validateSessionCapture(capture: WorkflowSessionCapture): void {
@@ -1480,6 +1815,25 @@ function isRunIdRow(value: unknown): value is { runId: string } {
1480
1815
  return isRecord(value);
1481
1816
  }
1482
1817
 
1818
+ function isRunStepIdentityRow(value: unknown): value is {
1819
+ stepIndex: number;
1820
+ attemptId: string;
1821
+ } {
1822
+ return isRecord(value);
1823
+ }
1824
+
1825
+ function isAttemptOutputRow(value: unknown): value is { outputHash: Buffer | null } {
1826
+ return isRecord(value) && (value.outputHash === null || Buffer.isBuffer(value.outputHash));
1827
+ }
1828
+
1829
+ function isStepRow(value: unknown): value is StepRow {
1830
+ return isRecord(value);
1831
+ }
1832
+
1833
+ function isUpdateRow(value: unknown): value is UpdateRow {
1834
+ return isRecord(value);
1835
+ }
1836
+
1483
1837
  function isRevisionRow(value: unknown): value is { revision: number } {
1484
1838
  return isRecord(value);
1485
1839
  }
@@ -760,7 +760,6 @@ export type WorkflowSessionEventType =
760
760
  | "assistant_event"
761
761
  | "message_finished"
762
762
  | "tool_execution_started"
763
- | "tool_execution_updated"
764
763
  | "tool_execution_finished";
765
764
 
766
765
  /** One normalized temporal Pi event in `session/events.ndjson`. */