@osolmaz/pi-workflows 0.13.1 → 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 (50) hide show
  1. package/README.md +4 -4
  2. package/dist/builtins/autoimplement.workflow.d.ts +4 -0
  3. package/dist/builtins/autoplan.workflow.d.ts +6 -0
  4. package/dist/builtins/autoplan.workflow.js +68 -32
  5. package/dist/builtins/autoplan.workflow.js.map +1 -1
  6. package/dist/builtins/catalog.js +3 -3
  7. package/dist/builtins/plain-summary.workflow.js +35 -24
  8. package/dist/builtins/plain-summary.workflow.js.map +1 -1
  9. package/dist/builtins/plan-change.workflow.d.ts +2 -0
  10. package/dist/builtins/sanity-check.workflow.js +19 -22
  11. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  12. package/dist/controllers/sqlite.js +16 -51
  13. package/dist/controllers/sqlite.js.map +1 -1
  14. package/dist/extension/decision-channels.js +45 -7
  15. package/dist/extension/decision-channels.js.map +1 -1
  16. package/dist/extension/index.js +2 -1
  17. package/dist/extension/index.js.map +1 -1
  18. package/dist/extension/recorder.d.ts +1 -1
  19. package/dist/extension/recorder.js +8 -8
  20. package/dist/extension/recorder.js.map +1 -1
  21. package/dist/state/schema.js +22 -3
  22. package/dist/state/schema.js.map +1 -1
  23. package/dist/viewer/session-reducer.d.ts +0 -1
  24. package/dist/viewer/session-reducer.js +2 -24
  25. package/dist/viewer/session-reducer.js.map +1 -1
  26. package/dist/workflows/engine.js +4 -3
  27. package/dist/workflows/engine.js.map +1 -1
  28. package/dist/workflows/store.d.ts +6 -1
  29. package/dist/workflows/store.js +286 -33
  30. package/dist/workflows/store.js.map +1 -1
  31. package/dist/workflows/types.d.ts +1 -1
  32. package/docs/SQLITE_STATE.md +20 -14
  33. package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
  34. package/docs/session-event-journal.md +2 -3
  35. package/docs/workflows.md +36 -9
  36. package/herdr-plugin.toml +1 -1
  37. package/package.json +1 -1
  38. package/src/builtins/autoplan.workflow.ts +80 -32
  39. package/src/builtins/catalog.ts +3 -3
  40. package/src/builtins/plain-summary.workflow.ts +38 -40
  41. package/src/builtins/sanity-check.workflow.ts +19 -22
  42. package/src/controllers/sqlite.ts +19 -53
  43. package/src/extension/decision-channels.ts +47 -7
  44. package/src/extension/index.ts +2 -1
  45. package/src/extension/recorder.ts +10 -8
  46. package/src/state/schema.ts +22 -3
  47. package/src/viewer/session-reducer.ts +2 -29
  48. package/src/workflows/engine.ts +4 -3
  49. package/src/workflows/store.ts +397 -43
  50. package/src/workflows/types.ts +0 -1
@@ -103,17 +103,25 @@ export class WorkflowRunStore {
103
103
  const inputHash = this.state.putJson(state.input, now);
104
104
  const workflowSourceHash = this.state.putJson(state.workflowSource ?? source, now);
105
105
  const launchOptionsHash = this.state.putJson({}, now);
106
- const stateHash = this.state.putJson(state, now);
106
+ const workflowSourcesHash = state.workflowSources === undefined
107
+ ? null
108
+ : this.state.putJson(state.workflowSources, now);
109
+ const humanDecisionHash = state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
110
+ const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
107
111
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
108
112
  this.state.connection
109
113
  .prepare(`INSERT INTO runs(
110
114
  run_id, resource_id, project_id, parent_run_id, definition_digest,
111
115
  workflow_ref, workflow_source_hash, launch_options_hash,
112
116
  source_type, source_ref, source_revision, title, status, paused,
113
- status_detail, input_hash, output_hash, error_hash,
117
+ status_detail, input_hash, workflow_sources_hash, human_decision_hash,
118
+ final_output_hash, error_hash, carried_step_count, current_node,
119
+ current_attempt_id, current_node_started_at, waiting_on,
114
120
  created_at, updated_at, finished_at
115
- ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
116
- .run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName, workflowSourceHash, launchOptionsHash, source.type, source.ref, source.revision, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, stateHash, errorHash, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
121
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
122
+ .run(state.runId, resourceId, state.parentRunId ?? null, definitionDigest, state.workflowName, workflowSourceHash, launchOptionsHash, source.type, source.ref, source.revision, state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, inputHash, workflowSourcesHash, humanDecisionHash, finalOutputHash, errorHash, state.carriedStepCount ?? 0, state.currentNode ?? null, state.currentAttemptId ?? null, state.currentNodeStartedAt === undefined
123
+ ? null
124
+ : Date.parse(state.currentNodeStartedAt), state.waitingOn ?? null, Date.parse(state.startedAt), now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt));
117
125
  insertRunEvent(this.state, resourceId, 1, at, {
118
126
  scope: "run",
119
127
  type: "run_created",
@@ -230,9 +238,10 @@ export class WorkflowRunStore {
230
238
  const nextUpdateSeq = this.nextUpdateSequence(attemptId);
231
239
  this.state.connection
232
240
  .prepare(`INSERT INTO workflow_updates(
233
- update_id, attempt_id, update_seq, update_type, update_key, data_hash, recorded_at
234
- ) VALUES (?, ?, ?, ?, ?, ?, ?)`)
235
- .run(updateId, attemptId, nextUpdateSeq, update.type, update.key, dataHash, Date.now());
241
+ update_id, attempt_id, update_seq, run_revision,
242
+ update_type, update_key, data_hash, recorded_at
243
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
244
+ .run(updateId, attemptId, nextUpdateSeq, revision, update.type, update.key, dataHash, Date.now());
236
245
  insertRunEvent(this.state, run.resourceId, revision, at, event);
237
246
  this.persistRunState(run, state, revision, Date.now());
238
247
  context.revision = revision;
@@ -329,7 +338,7 @@ export class WorkflowRunStore {
329
338
  ...(attemptId !== undefined ? { captureAttemptId: attemptId } : {}),
330
339
  },
331
340
  });
332
- const current = this.readState(run.stateHash);
341
+ const current = this.readRunState(run, this.readDefinition(run.definitionHash));
333
342
  current.traceSeq = revision;
334
343
  current.updatedAt = at;
335
344
  this.persistRunState(run, current, revision, now);
@@ -385,15 +394,9 @@ export class WorkflowRunStore {
385
394
  .run(current.segmentId, record.seq, record.type, record.nodeId, record.attemptId, record.turnId ?? null, record.messageId ?? null, record.toolCallId ?? null, payloadHash, Date.parse(record.at));
386
395
  expected += 1;
387
396
  }
388
- const now = Date.now();
389
397
  this.state.connection
390
398
  .prepare("UPDATE session_segments SET event_count = ? WHERE segment_id = ?")
391
399
  .run(expected - 1, current.segmentId);
392
- const resourceId = segmentResourceId(current);
393
- const revision = this.requireResourceRevision(resourceId);
394
- this.bumpResource(resourceId, revision, now);
395
- const payloadHash = this.state.putJson({ count: records.length, lastSeq: expected - 1 }, now);
396
- insertGenericEvent(this.state, resourceId, revision + 1, "session.events_appended", "session", current.sessionId, payloadHash, now);
397
400
  });
398
401
  }
399
402
  async writeSessionCapture(runId, capture, attemptId) {
@@ -432,8 +435,8 @@ export class WorkflowRunStore {
432
435
  const row = this.readRunRow(runId);
433
436
  if (row === undefined)
434
437
  return null;
435
- const state = this.readState(row.stateHash);
436
438
  const snapshot = this.readDefinition(row.definitionHash);
439
+ const state = this.readRunState(row, snapshot);
437
440
  const segments = this.segmentRows(runId).map((segment) => this.loadSegment(segment, state));
438
441
  const flat = segments.find((segment) => segment.attemptId === "") ?? emptySegment();
439
442
  return {
@@ -551,20 +554,23 @@ export class WorkflowRunStore {
551
554
  }
552
555
  persistRunState(run, state, expectedRevision, now) {
553
556
  this.bumpResource(run.resourceId, expectedRevision - 1, now);
554
- const stateHash = this.state.putJson(state, now);
557
+ const workflowSourcesHash = state.workflowSources === undefined ? null : this.state.putJson(state.workflowSources, now);
558
+ const humanDecisionHash = state.humanDecision === undefined ? null : this.state.putJson(state.humanDecision, now);
559
+ const finalOutputHash = state.finalOutput === undefined ? null : this.state.putJson(state.finalOutput, now);
555
560
  const errorHash = state.error === undefined ? null : this.state.putText(state.error, now);
556
561
  const update = this.state.connection
557
562
  .prepare(`UPDATE runs
558
- SET title = ?, status = ?, paused = ?, status_detail = ?, output_hash = ?,
559
- error_hash = ?, updated_at = ?, finished_at = ?
563
+ SET title = ?, status = ?, paused = ?, status_detail = ?,
564
+ workflow_sources_hash = ?, human_decision_hash = ?, final_output_hash = ?,
565
+ error_hash = ?, carried_step_count = ?, current_node = ?, current_attempt_id = ?,
566
+ current_node_started_at = ?, waiting_on = ?, updated_at = ?, finished_at = ?
560
567
  WHERE run_id = ?`)
561
- .run(state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, stateHash, errorHash, now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt), state.runId);
568
+ .run(state.runTitle ?? null, state.status, state.paused === true ? 1 : 0, state.statusDetail ?? null, workflowSourcesHash, humanDecisionHash, finalOutputHash, errorHash, state.carriedStepCount ?? 0, state.currentNode ?? null, state.currentAttemptId ?? null, state.currentNodeStartedAt === undefined ? null : Date.parse(state.currentNodeStartedAt), state.waitingOn ?? null, now, state.finishedAt === undefined ? null : Date.parse(state.finishedAt), state.runId);
562
569
  if (update.changes !== 1)
563
570
  throw new Error(`Workflow run is missing: ${state.runId}`);
564
571
  if (state.status !== "running") {
565
572
  this.enqueueRunSettlementEffect(run.resourceId, expectedRevision, state, now);
566
573
  }
567
- run.stateHash = stateHash;
568
574
  }
569
575
  enqueueRunSettlementEffect(runResourceId, sourceRevision, state, now) {
570
576
  if (this.state.connection.prepare("SELECT 1 FROM run_queue WHERE run_id = ?").get(state.runId) ===
@@ -602,7 +608,8 @@ export class WorkflowRunStore {
602
608
  }
603
609
  syncNodeAttempts(state, snapshot, now) {
604
610
  for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
605
- const resultHash = this.state.putJson(step, now);
611
+ const metadata = stepMetadata(step);
612
+ const stepMetadataHash = Object.keys(metadata).length === 0 ? null : this.state.putJson(metadata, now);
606
613
  const outputHash = step.output === undefined ? null : this.state.putJson(step.output, now);
607
614
  const errorHash = step.error === undefined ? null : this.state.putText(step.error, now);
608
615
  const promptHash = step.prompt === null ? null : this.state.putJson(step.prompt, now);
@@ -614,23 +621,61 @@ export class WorkflowRunStore {
614
621
  this.state.connection
615
622
  .prepare(`INSERT INTO node_attempts(
616
623
  attempt_id, run_id, node_id, attempt_number, node_type, status,
617
- presentation_hash, output_hash, result_hash, error_hash,
624
+ prompt_hash, output_hash, step_metadata_hash, error_hash,
618
625
  started_at, finished_at, created_at, updated_at
619
626
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
620
- .run(step.attemptId, state.runId, step.nodeId, attemptNumber, step.nodeType, outcomeStatus(step.outcome), promptHash, outputHash, resultHash, errorHash, Date.parse(step.startedAt), Date.parse(step.finishedAt), Date.parse(step.startedAt), now);
627
+ .run(step.attemptId, state.runId, step.nodeId, attemptNumber, step.nodeType, outcomeStatus(step.outcome), promptHash, outputHash, stepMetadataHash, errorHash, Date.parse(step.startedAt), Date.parse(step.finishedAt), Date.parse(step.startedAt), now);
621
628
  }
622
629
  else {
623
630
  this.state.connection
624
631
  .prepare(`UPDATE node_attempts
625
- SET status = ?, presentation_hash = ?, output_hash = ?, result_hash = ?,
632
+ SET status = ?, prompt_hash = ?, output_hash = ?, step_metadata_hash = ?,
626
633
  error_hash = ?, finished_at = ?, updated_at = ?
627
634
  WHERE attempt_id = ?`)
628
- .run(outcomeStatus(step.outcome), promptHash, outputHash, resultHash, errorHash, Date.parse(step.finishedAt), now, step.attemptId);
635
+ .run(outcomeStatus(step.outcome), promptHash, outputHash, stepMetadataHash, errorHash, Date.parse(step.finishedAt), now, step.attemptId);
629
636
  }
630
637
  }
631
638
  if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
632
639
  this.ensureAttempt(state, state.currentNode, state.currentAttemptId, now, snapshot);
633
640
  }
641
+ this.syncRunSteps(state, now);
642
+ }
643
+ syncRunSteps(state, now) {
644
+ const existingRows = this.state.connection
645
+ .prepare(`SELECT step_index AS stepIndex, attempt_id AS attemptId
646
+ FROM run_steps WHERE run_id = ? ORDER BY step_index`)
647
+ .all(state.runId)
648
+ .filter(isRunStepIdentityRow);
649
+ if (existingRows.length > state.steps.length) {
650
+ throw new Error(`Workflow run steps cannot shrink: ${state.runId}`);
651
+ }
652
+ for (const existing of existingRows) {
653
+ if (state.steps[existing.stepIndex]?.attemptId !== existing.attemptId) {
654
+ throw new Error(`Workflow run step history changed at index ${existing.stepIndex}`);
655
+ }
656
+ }
657
+ for (let stepIndex = existingRows.length; stepIndex < state.steps.length; stepIndex += 1) {
658
+ const step = state.steps[stepIndex];
659
+ /* istanbul ignore if -- array index follows a checked bound */
660
+ if (step === undefined)
661
+ throw new Error("Workflow run step became unavailable");
662
+ const attempt = this.state.connection
663
+ .prepare("SELECT output_hash AS outputHash FROM node_attempts WHERE attempt_id = ?")
664
+ .get(step.attemptId);
665
+ if (!isAttemptOutputRow(attempt)) {
666
+ throw new Error(`Workflow node attempt is missing: ${step.attemptId}`);
667
+ }
668
+ const carried = stepIndex < (state.carriedStepCount ?? 0);
669
+ const outputOverrideHash = carried &&
670
+ (attempt.outputHash === null ||
671
+ canonicalJson(this.state.readJson(attempt.outputHash)) !== canonicalJson(step.output))
672
+ ? this.state.putJson(step.output, now)
673
+ : null;
674
+ this.state.connection
675
+ .prepare(`INSERT INTO run_steps(run_id, step_index, attempt_id, output_override_hash)
676
+ VALUES (?, ?, ?, ?)`)
677
+ .run(state.runId, stepIndex, step.attemptId, outputOverrideHash);
678
+ }
634
679
  }
635
680
  ensureAttempt(state, nodeId, attemptId, now, snapshot) {
636
681
  const existing = this.state.connection
@@ -675,8 +720,17 @@ export class WorkflowRunStore {
675
720
  readRunRow(runId) {
676
721
  const row = this.state.connection
677
722
  .prepare(`SELECT r.run_id AS runId, r.resource_id AS resourceId,
678
- r.output_hash AS stateHash, d.definition_hash AS definitionHash,
679
- r.status
723
+ d.definition_hash AS definitionHash, r.definition_digest AS definitionDigest,
724
+ d.workflow_name AS workflowRef, r.source_ref AS sourceRef,
725
+ r.workflow_source_hash AS workflowSourceHash,
726
+ r.workflow_sources_hash AS workflowSourcesHash, r.parent_run_id AS parentRunId,
727
+ r.title, r.status, r.paused, r.status_detail AS statusDetail,
728
+ r.input_hash AS inputHash, r.human_decision_hash AS humanDecisionHash,
729
+ r.final_output_hash AS finalOutputHash, r.error_hash AS errorHash,
730
+ r.carried_step_count AS carriedStepCount, r.current_node AS currentNode,
731
+ r.current_attempt_id AS currentAttemptId,
732
+ r.current_node_started_at AS currentNodeStartedAt, r.waiting_on AS waitingOn,
733
+ r.created_at AS createdAt, r.updated_at AS updatedAt, r.finished_at AS finishedAt
680
734
  FROM runs r
681
735
  JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
682
736
  WHERE r.run_id = ?`)
@@ -689,12 +743,144 @@ export class WorkflowRunStore {
689
743
  throw new Error(`Workflow run is missing: ${runId}`);
690
744
  return row;
691
745
  }
692
- readState(hash) {
693
- const value = this.state.readJson(hash);
694
- if (!isRecord(value) || value.schema !== RUN_STATE_SCHEMA) {
695
- throw new Error("Workflow run state has an incompatible schema");
746
+ readRunState(row, snapshot) {
747
+ const steps = this.readSteps(row.runId);
748
+ const outputs = {};
749
+ const results = {};
750
+ for (const step of steps) {
751
+ const result = resultForStep(step);
752
+ results[step.nodeId] = result;
753
+ if (step.outcome === "ok")
754
+ outputs[step.nodeId] = step.output;
755
+ else
756
+ delete outputs[step.nodeId];
757
+ const node = snapshot.nodes[step.nodeId];
758
+ const mountPath = node?.includeTransition === "exit" && node.mountPath !== undefined
759
+ ? node.mountPath.join("/")
760
+ : undefined;
761
+ if (mountPath !== undefined && step.outcome === "ok") {
762
+ outputs[mountPath] = step.output;
763
+ results[mountPath] = { ...result, nodeId: mountPath };
764
+ }
696
765
  }
697
- return value;
766
+ const revision = this.requireResourceRevision(row.resourceId);
767
+ return {
768
+ schema: RUN_STATE_SCHEMA,
769
+ traceSeq: revision,
770
+ runId: row.runId,
771
+ workflowName: row.workflowRef,
772
+ ...(row.parentRunId === null ? {} : { parentRunId: row.parentRunId }),
773
+ ...(row.carriedStepCount === 0 ? {} : { carriedStepCount: row.carriedStepCount }),
774
+ ...(row.title === null ? {} : { runTitle: row.title }),
775
+ ...(row.sourceRef.startsWith("inline:")
776
+ ? {}
777
+ : { workflowSource: this.readJsonAs(row.workflowSourceHash) }),
778
+ ...(row.workflowSourcesHash === null
779
+ ? {}
780
+ : { workflowSources: this.readJsonAs(row.workflowSourcesHash) }),
781
+ ...(row.workflowSourcesHash !== null || snapshot.composition?.mounts.length
782
+ ? { definitionDigest: `sha256:${row.definitionDigest.toString("hex")}` }
783
+ : {}),
784
+ startedAt: new Date(row.createdAt).toISOString(),
785
+ ...(row.finishedAt === null ? {} : { finishedAt: new Date(row.finishedAt).toISOString() }),
786
+ updatedAt: new Date(row.updatedAt).toISOString(),
787
+ status: row.status,
788
+ input: this.state.readJson(row.inputHash),
789
+ outputs,
790
+ results,
791
+ steps,
792
+ ...this.readUpdates(row.runId),
793
+ ...(row.currentNode === null ? {} : { currentNode: row.currentNode }),
794
+ ...(row.currentAttemptId === null ? {} : { currentAttemptId: row.currentAttemptId }),
795
+ ...(row.currentNodeStartedAt === null
796
+ ? {}
797
+ : { currentNodeStartedAt: new Date(row.currentNodeStartedAt).toISOString() }),
798
+ ...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
799
+ ...(row.humanDecisionHash === null
800
+ ? {}
801
+ : { humanDecision: this.readJsonAs(row.humanDecisionHash) }),
802
+ ...(row.paused === 0 ? {} : { paused: true }),
803
+ ...(row.waitingOn === null ? {} : { waitingOn: row.waitingOn }),
804
+ ...(row.finalOutputHash === null
805
+ ? {}
806
+ : { finalOutput: this.state.readJson(row.finalOutputHash) }),
807
+ ...(row.errorHash === null ? {} : { error: this.readText(row.errorHash) }),
808
+ };
809
+ }
810
+ readSteps(runId) {
811
+ const rows = this.state.connection
812
+ .prepare(`SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
813
+ a.node_type AS nodeType, a.status, a.prompt_hash AS promptHash,
814
+ a.output_hash AS outputHash, s.output_override_hash AS outputOverrideHash,
815
+ a.step_metadata_hash AS stepMetadataHash, a.error_hash AS errorHash,
816
+ a.started_at AS startedAt, a.finished_at AS finishedAt
817
+ FROM run_steps s
818
+ JOIN node_attempts a ON a.attempt_id = s.attempt_id
819
+ WHERE s.run_id = ? ORDER BY s.step_index`)
820
+ .all(runId)
821
+ .filter(isStepRow);
822
+ return rows.map((row, index) => {
823
+ if (row.stepIndex !== index)
824
+ throw new Error(`Workflow run step sequence has a gap: ${runId}`);
825
+ const metadata = row.stepMetadataHash === null
826
+ ? {}
827
+ : this.readJsonAs(row.stepMetadataHash);
828
+ const prompt = row.promptHash === null ? null : this.readJsonAs(row.promptHash);
829
+ const outputHash = row.outputOverrideHash ?? row.outputHash;
830
+ const output = outputHash === null ? null : this.state.readJson(outputHash);
831
+ const error = row.errorHash === null ? undefined : this.readText(row.errorHash);
832
+ return {
833
+ attemptId: row.attemptId,
834
+ nodeId: row.nodeId,
835
+ nodeType: row.nodeType,
836
+ outcome: outcomeForStatus(row.status),
837
+ startedAt: new Date(row.startedAt).toISOString(),
838
+ finishedAt: new Date(row.finishedAt).toISOString(),
839
+ prompt,
840
+ output,
841
+ ...(error === undefined ? {} : { error }),
842
+ ...metadata,
843
+ };
844
+ });
845
+ }
846
+ readUpdates(runId) {
847
+ const rows = this.state.connection
848
+ .prepare(`SELECT u.update_id AS updateId, u.run_revision AS runRevision,
849
+ a.node_id AS nodeId, u.attempt_id AS attemptId,
850
+ u.update_type AS updateType, u.update_key AS updateKey,
851
+ u.data_hash AS dataHash, u.recorded_at AS recordedAt
852
+ FROM workflow_updates u
853
+ JOIN node_attempts a ON a.attempt_id = u.attempt_id
854
+ WHERE a.run_id = ? ORDER BY u.run_revision`)
855
+ .all(runId)
856
+ .filter(isUpdateRow);
857
+ if (rows.length === 0)
858
+ return {};
859
+ let updates;
860
+ for (const row of rows) {
861
+ updates = updateProjection(updates, {
862
+ updateId: row.updateId,
863
+ seq: row.runRevision,
864
+ at: new Date(row.recordedAt).toISOString(),
865
+ runId,
866
+ nodeId: row.nodeId,
867
+ attemptId: row.attemptId,
868
+ type: row.updateType,
869
+ key: row.updateKey,
870
+ data: this.readJsonAs(row.dataHash),
871
+ });
872
+ }
873
+ return updates === undefined ? {} : { updates };
874
+ }
875
+ readJsonAs(hash) {
876
+ return this.state.readJson(hash);
877
+ }
878
+ readText(hash) {
879
+ const blob = this.state.readBlob(hash);
880
+ if (blob === undefined || blob.mediaType !== "text/plain") {
881
+ throw new Error("Text blob is missing or has the wrong media type");
882
+ }
883
+ return blob.content.toString("utf8");
698
884
  }
699
885
  readDefinition(hash) {
700
886
  const value = this.state.readJson(hash);
@@ -892,14 +1078,31 @@ export function readLastTraceEvent(runId, options = {}) {
892
1078
  }
893
1079
  }
894
1080
  function insertRunEvent(state, resourceId, revision, at, event) {
1081
+ const payload = compactTracePayload(event.type, event.payload);
895
1082
  const payloadHash = state.putJson({
896
1083
  scope: event.scope,
897
1084
  ...(event.nodeId === undefined ? {} : { nodeId: event.nodeId }),
898
1085
  ...(event.attemptId === undefined ? {} : { attemptId: event.attemptId }),
899
- payload: event.payload,
1086
+ payload,
900
1087
  }, Date.parse(at));
901
1088
  insertGenericEvent(state, resourceId, revision, event.type, "system", null, payloadHash, Date.parse(at));
902
1089
  }
1090
+ function compactTracePayload(eventType, payload) {
1091
+ if ((eventType === "node_finished" || eventType === "include_exited") &&
1092
+ Object.hasOwn(payload, "output")) {
1093
+ const { output: _output, ...rest } = payload;
1094
+ return { ...rest, outputStored: true };
1095
+ }
1096
+ if (eventType === "run_started" && Object.hasOwn(payload, "input")) {
1097
+ const { input: _input, ...rest } = payload;
1098
+ return { ...rest, inputStored: true };
1099
+ }
1100
+ if (Object.hasOwn(payload, "finalOutput")) {
1101
+ const { finalOutput: _finalOutput, ...rest } = payload;
1102
+ return { ...rest, finalOutputStored: true };
1103
+ }
1104
+ return payload;
1105
+ }
903
1106
  function insertGenericEvent(state, resourceId, revision, eventType, actorType, actorId, payloadHash, now) {
904
1107
  state.connection
905
1108
  .prepare(`INSERT INTO events(
@@ -941,6 +1144,40 @@ function outcomeStatus(outcome) {
941
1144
  return "failed";
942
1145
  }
943
1146
  }
1147
+ function outcomeForStatus(status) {
1148
+ switch (status) {
1149
+ case "completed":
1150
+ return "ok";
1151
+ case "timed_out":
1152
+ return "timed_out";
1153
+ case "cancelled":
1154
+ return "cancelled";
1155
+ case "failed":
1156
+ return "failed";
1157
+ default:
1158
+ throw new Error(`Workflow step has nonterminal status: ${status}`);
1159
+ }
1160
+ }
1161
+ function stepMetadata(step) {
1162
+ return {
1163
+ ...(step.action === undefined ? {} : { action: step.action }),
1164
+ ...(step.assistantMessage === undefined ? {} : { assistantMessage: step.assistantMessage }),
1165
+ ...(step.conversation === undefined ? {} : { conversation: step.conversation }),
1166
+ };
1167
+ }
1168
+ function resultForStep(step) {
1169
+ return {
1170
+ attemptId: step.attemptId,
1171
+ nodeId: step.nodeId,
1172
+ nodeType: step.nodeType,
1173
+ outcome: step.outcome,
1174
+ startedAt: step.startedAt,
1175
+ finishedAt: step.finishedAt,
1176
+ durationMs: Date.parse(step.finishedAt) - Date.parse(step.startedAt),
1177
+ ...(step.outcome === "ok" ? { output: step.output } : {}),
1178
+ ...(step.error === undefined ? {} : { error: step.error }),
1179
+ };
1180
+ }
944
1181
  function traceScope(value) {
945
1182
  return value === "node" || value === "agent" || value === "action" || value === "session"
946
1183
  ? value
@@ -968,6 +1205,10 @@ function validateSessionEventRecord(record) {
968
1205
  Array.isArray(record.payload)) {
969
1206
  throw new Error("Session event is missing required envelope fields");
970
1207
  }
1208
+ if (record.type === "assistant_event" &&
1209
+ ["text_delta", "thinking_delta", "toolcall_delta"].includes(String(record.payload.type))) {
1210
+ throw new Error("Incremental assistant events are not durable session facts");
1211
+ }
971
1212
  }
972
1213
  function validateSessionCapture(capture) {
973
1214
  if (capture.schema !== SESSION_CAPTURE_SCHEMA ||
@@ -1002,6 +1243,18 @@ function isRunRow(value) {
1002
1243
  function isRunIdRow(value) {
1003
1244
  return isRecord(value);
1004
1245
  }
1246
+ function isRunStepIdentityRow(value) {
1247
+ return isRecord(value);
1248
+ }
1249
+ function isAttemptOutputRow(value) {
1250
+ return isRecord(value) && (value.outputHash === null || Buffer.isBuffer(value.outputHash));
1251
+ }
1252
+ function isStepRow(value) {
1253
+ return isRecord(value);
1254
+ }
1255
+ function isUpdateRow(value) {
1256
+ return isRecord(value);
1257
+ }
1005
1258
  function isRevisionRow(value) {
1006
1259
  return isRecord(value);
1007
1260
  }