@bratsos/workflow-engine 0.6.0 → 0.8.0

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 (42) hide show
  1. package/README.md +93 -0
  2. package/dist/{chunk-2MWO6UVR.js → chunk-NANAXHS5.js} +2 -2
  3. package/dist/chunk-NANAXHS5.js.map +1 -0
  4. package/dist/{chunk-DIADEUGZ.js → chunk-TWYPSP7P.js} +99 -7
  5. package/dist/chunk-TWYPSP7P.js.map +1 -0
  6. package/dist/{chunk-HKGZ2WHJ.js → chunk-WWK2SPN7.js} +16 -9
  7. package/dist/chunk-WWK2SPN7.js.map +1 -0
  8. package/dist/{chunk-HOGDFLCG.js → chunk-XPWAEYOO.js} +449 -59
  9. package/dist/chunk-XPWAEYOO.js.map +1 -0
  10. package/dist/{client-llB6XpHS.d.ts → client-YFKVt4p7.d.ts} +10 -21
  11. package/dist/client.d.ts +4 -4
  12. package/dist/client.js +1 -1
  13. package/dist/conventions/index.d.ts +114 -0
  14. package/dist/conventions/index.js +95 -0
  15. package/dist/conventions/index.js.map +1 -0
  16. package/dist/{events-D_P24UaY.d.ts → events-B3XPPu0c.d.ts} +23 -1
  17. package/dist/{index-sGgV8JNu.d.ts → index-CL0KmlyW.d.ts} +10 -1
  18. package/dist/index.d.ts +10 -10
  19. package/dist/index.js +5 -5
  20. package/dist/{interface-DCdddCe0.d.ts → interface-BPz138Hf.d.ts} +110 -2
  21. package/dist/kernel/index.d.ts +6 -6
  22. package/dist/kernel/index.js +2 -2
  23. package/dist/kernel/testing/index.d.ts +3 -3
  24. package/dist/persistence/index.d.ts +2 -2
  25. package/dist/persistence/index.js +2 -2
  26. package/dist/persistence/prisma/index.d.ts +2 -2
  27. package/dist/persistence/prisma/index.js +2 -2
  28. package/dist/{plugins-Oyo_iu0l.d.ts → plugins-zT-aIcEZ.d.ts} +63 -4
  29. package/dist/{ports-ChGnJcn2.d.ts → ports-DUL4hlQr.d.ts} +11 -2
  30. package/dist/{stage-_7BKqqUG.d.ts → stage-WuK0mfrC.d.ts} +81 -1
  31. package/dist/testing/index.d.ts +8 -1
  32. package/dist/testing/index.js +88 -2
  33. package/dist/testing/index.js.map +1 -1
  34. package/package.json +6 -1
  35. package/skills/workflow-engine/SKILL.md +58 -1
  36. package/skills/workflow-engine/migrations/README.md +275 -0
  37. package/skills/workflow-engine/migrations/migrate-0.7-to-0.8.md +528 -0
  38. package/skills/workflow-engine/references/10-annotations.md +479 -0
  39. package/dist/chunk-2MWO6UVR.js.map +0 -1
  40. package/dist/chunk-DIADEUGZ.js.map +0 -1
  41. package/dist/chunk-HKGZ2WHJ.js.map +0 -1
  42. package/dist/chunk-HOGDFLCG.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { StaleVersionError } from './chunk-2MWO6UVR.js';
1
+ import { StaleVersionError } from './chunk-NANAXHS5.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  z.object({
@@ -28,6 +28,16 @@ var IdempotencyInProgressError = class extends Error {
28
28
  this.name = "IdempotencyInProgressError";
29
29
  }
30
30
  };
31
+ var RunNotRunningError = class extends Error {
32
+ constructor(workflowRunId, currentStatus) {
33
+ super(
34
+ `Run ${workflowRunId} is ${currentStatus}, not RUNNING \u2014 transactional write aborted`
35
+ );
36
+ this.workflowRunId = workflowRunId;
37
+ this.currentStatus = currentStatus;
38
+ this.name = "RunNotRunningError";
39
+ }
40
+ };
31
41
 
32
42
  // src/kernel/helpers/load-workflow-context.ts
33
43
  async function loadWorkflowContext(workflowRunId, deps) {
@@ -56,6 +66,76 @@ async function saveStageOutput(runId, workflowType, stageId, output, deps) {
56
66
  return key;
57
67
  }
58
68
 
69
+ // src/kernel/helpers/annotation-buffer.ts
70
+ function normalizeAnnotateArgs(args) {
71
+ const first = args[0];
72
+ if (typeof first === "string") {
73
+ return [
74
+ { key: first, value: args[1], opts: args[2] }
75
+ ];
76
+ }
77
+ if (first !== null && typeof first === "object" && "key" in first && typeof first.key === "string" && !("attributes" in first)) {
78
+ return [
79
+ {
80
+ key: first.key,
81
+ value: args[1],
82
+ opts: args[2]
83
+ }
84
+ ];
85
+ }
86
+ const batch = first;
87
+ const attributes = batch?.attributes ?? {};
88
+ const opts = {
89
+ actor: batch.actor,
90
+ payload: batch.payload,
91
+ idempotencyKey: batch.idempotencyKey,
92
+ emitEvent: batch.emitEvent
93
+ };
94
+ return Object.entries(attributes).map(([key, value]) => ({
95
+ key,
96
+ value,
97
+ opts
98
+ }));
99
+ }
100
+ var AnnotationBuffer = class {
101
+ items = [];
102
+ push(input) {
103
+ this.items.push(input);
104
+ }
105
+ /**
106
+ * Return all buffered items and clear the buffer. Idempotent — calling
107
+ * flush again returns an empty array.
108
+ */
109
+ flush() {
110
+ const out = this.items;
111
+ this.items = [];
112
+ return out;
113
+ }
114
+ size() {
115
+ return this.items.length;
116
+ }
117
+ };
118
+ function createAnnotationBuffer() {
119
+ return new AnnotationBuffer();
120
+ }
121
+
122
+ // src/kernel/helpers/annotation-events.ts
123
+ function buildAnnotationEvents(inputs, now) {
124
+ return inputs.filter((input) => input.emitEvent === true).map((input) => ({
125
+ type: "annotation:created",
126
+ timestamp: now,
127
+ workflowRunId: input.workflowRunId,
128
+ key: input.key,
129
+ value: input.value,
130
+ scope: input.scope,
131
+ scopeId: input.scopeId ?? void 0,
132
+ attempt: input.attempt,
133
+ actorKind: input.actor?.kind,
134
+ actorId: input.actor?.id,
135
+ actorVersion: input.actor?.version
136
+ }));
137
+ }
138
+
59
139
  // src/kernel/helpers/create-storage-shim.ts
60
140
  function createStorageShim(workflowRunId, workflowType, deps) {
61
141
  return {
@@ -78,6 +158,60 @@ function createStorageShim(workflowRunId, workflowType, deps) {
78
158
  };
79
159
  }
80
160
 
161
+ // src/kernel/helpers/legacy-metadata-shim.ts
162
+ function synthesizeLegacyMetadata(run, filters = {}) {
163
+ if (!run.metadata || typeof run.metadata !== "object") return [];
164
+ if (Array.isArray(run.metadata)) return [];
165
+ if (filters.scope !== void 0 && filters.scope !== "run") return [];
166
+ if (filters.scopeId !== void 0 && filters.scopeId !== null) return [];
167
+ if (filters.actorId !== void 0) return [];
168
+ if (filters.actorKind !== void 0) return [];
169
+ if (filters.attempt !== void 0 && filters.attempt !== 0) return [];
170
+ if (filters.since !== void 0 && run.createdAt < filters.since) return [];
171
+ if (filters.until !== void 0 && run.createdAt > filters.until) return [];
172
+ const rows = [];
173
+ for (const [originalKey, value] of Object.entries(
174
+ run.metadata
175
+ )) {
176
+ const key = `legacy.metadata.${originalKey}`;
177
+ if (filters.key !== void 0 && filters.key !== key) continue;
178
+ if (filters.keyPrefix !== void 0 && !key.startsWith(filters.keyPrefix))
179
+ continue;
180
+ rows.push({
181
+ id: `synthesized:${run.id}:${originalKey}`,
182
+ createdAt: run.createdAt,
183
+ workflowRunId: run.id,
184
+ workflowStageRecordId: null,
185
+ attempt: 0,
186
+ scope: "run",
187
+ scopeId: null,
188
+ actorKind: null,
189
+ actorId: null,
190
+ actorVersion: null,
191
+ key,
192
+ value,
193
+ payload: null,
194
+ idempotencyKey: null
195
+ });
196
+ }
197
+ return rows;
198
+ }
199
+ function filterCouldMatchLegacy(filters) {
200
+ if (filters.scope !== void 0 && filters.scope !== "run") return false;
201
+ if (filters.scopeId !== void 0 && filters.scopeId !== null) return false;
202
+ if (filters.actorId !== void 0) return false;
203
+ if (filters.actorKind !== void 0) return false;
204
+ if (filters.attempt !== void 0 && filters.attempt !== 0) return false;
205
+ if (filters.key !== void 0 && !filters.key.startsWith("legacy.metadata."))
206
+ return false;
207
+ if (filters.keyPrefix !== void 0) {
208
+ if (!"legacy.metadata.".startsWith(filters.keyPrefix) && !filters.keyPrefix.startsWith("legacy.metadata.")) {
209
+ return false;
210
+ }
211
+ }
212
+ return true;
213
+ }
214
+
81
215
  // src/kernel/helpers/resolve-execution-group-output.ts
82
216
  function resolveExecutionGroupOutput(workflow, groupIndex, workflowContext) {
83
217
  const stages = workflow.getStagesInExecutionGroup(groupIndex);
@@ -181,6 +315,7 @@ async function handleJobExecute(command, deps) {
181
315
  return record;
182
316
  });
183
317
  const progressEvents = [];
318
+ const annotationBuffer = createAnnotationBuffer();
184
319
  try {
185
320
  const rawInput = resolveStageInput(
186
321
  workflow,
@@ -206,6 +341,27 @@ async function handleJobExecute(command, deps) {
206
341
  }).catch(() => {
207
342
  });
208
343
  };
344
+ const annotateFn = ((...args) => {
345
+ const stageScopeFields = {
346
+ workflowRunId,
347
+ workflowStageRecordId: stageRecord.id,
348
+ attempt: stageRecord.attempt,
349
+ scope: "stage",
350
+ scopeId: stageId
351
+ };
352
+ for (const { key, value, opts } of normalizeAnnotateArgs(args)) {
353
+ if (value === void 0 || value === null) continue;
354
+ annotationBuffer.push({
355
+ ...stageScopeFields,
356
+ actor: opts?.actor,
357
+ key,
358
+ value,
359
+ payload: opts?.payload,
360
+ idempotencyKey: opts?.idempotencyKey,
361
+ emitEvent: opts?.emitEvent
362
+ });
363
+ }
364
+ });
209
365
  const context = {
210
366
  workflowRunId,
211
367
  stageId,
@@ -228,6 +384,7 @@ async function handleJobExecute(command, deps) {
228
384
  },
229
385
  onLog: logFn,
230
386
  log: logFn,
387
+ annotate: annotateFn,
231
388
  storage: createStorageShim(workflowRunId, workflowRun.workflowType, deps),
232
389
  workflowContext
233
390
  };
@@ -246,30 +403,54 @@ async function handleJobExecute(command, deps) {
246
403
  const nextPollAt = new Date(
247
404
  pollConfig.nextPollAt?.getTime() ?? deps.clock.now().getTime() + (pollConfig.pollInterval || 6e4)
248
405
  );
249
- await deps.persistence.withTransaction(async (tx) => {
250
- await tx.updateStage(stageRecord.id, {
251
- status: "SUSPENDED",
252
- suspendedState: state,
253
- nextPollAt,
254
- pollInterval: pollConfig.pollInterval,
255
- maxWaitUntil: pollConfig.maxWaitTime ? new Date(deps.clock.now().getTime() + pollConfig.maxWaitTime) : void 0,
256
- metrics
406
+ const bufferedAnnotations = annotationBuffer.flush();
407
+ try {
408
+ await deps.persistence.withTransaction(async (tx) => {
409
+ const currentStatus = await tx.getRunStatus(workflowRunId);
410
+ if (currentStatus !== "RUNNING") {
411
+ throw new RunNotRunningError(
412
+ workflowRunId,
413
+ currentStatus ?? "DELETED"
414
+ );
415
+ }
416
+ await tx.updateStage(stageRecord.id, {
417
+ status: "SUSPENDED",
418
+ suspendedState: state,
419
+ nextPollAt,
420
+ pollInterval: pollConfig.pollInterval,
421
+ maxWaitUntil: pollConfig.maxWaitTime ? new Date(deps.clock.now().getTime() + pollConfig.maxWaitTime) : void 0,
422
+ metrics
423
+ });
424
+ if (bufferedAnnotations.length > 0) {
425
+ await tx.appendAnnotations(bufferedAnnotations);
426
+ }
427
+ const suspendedEvent = {
428
+ type: "stage:suspended",
429
+ timestamp: deps.clock.now(),
430
+ workflowRunId,
431
+ stageId,
432
+ stageName: stageDef.name,
433
+ nextPollAt
434
+ };
435
+ await tx.appendOutboxEvents(
436
+ toOutboxEvents(workflowRunId, causationId, [
437
+ ...progressEvents,
438
+ suspendedEvent,
439
+ ...buildAnnotationEvents(bufferedAnnotations, deps.clock.now())
440
+ ])
441
+ );
257
442
  });
258
- const suspendedEvent = {
259
- type: "stage:suspended",
260
- timestamp: deps.clock.now(),
261
- workflowRunId,
262
- stageId,
263
- stageName: stageDef.name,
264
- nextPollAt
265
- };
266
- await tx.appendOutboxEvents(
267
- toOutboxEvents(workflowRunId, causationId, [
268
- ...progressEvents,
269
- suspendedEvent
270
- ])
271
- );
272
- });
443
+ } catch (txError) {
444
+ if (txError instanceof RunNotRunningError) {
445
+ return {
446
+ outcome: "failed",
447
+ ghost: true,
448
+ error: txError.message,
449
+ _events: []
450
+ };
451
+ }
452
+ throw txError;
453
+ }
273
454
  return { outcome: "suspended", nextPollAt, _events: [] };
274
455
  } else {
275
456
  const duration = deps.clock.now().getTime() - startTime;
@@ -287,33 +468,57 @@ async function handleJobExecute(command, deps) {
287
468
  result.artifacts,
288
469
  deps
289
470
  ) : void 0;
290
- await deps.persistence.withTransaction(async (tx) => {
291
- await tx.updateStage(stageRecord.id, {
292
- status: "COMPLETED",
293
- completedAt: deps.clock.now(),
294
- duration,
295
- outputData: {
296
- _artifactKey: outputKey,
297
- ...artifactKeys ? { _artifactKeys: artifactKeys } : {}
298
- },
299
- metrics: result.metrics,
300
- embeddingInfo: result.embeddings
471
+ const bufferedAnnotations = annotationBuffer.flush();
472
+ try {
473
+ await deps.persistence.withTransaction(async (tx) => {
474
+ const currentStatus = await tx.getRunStatus(workflowRunId);
475
+ if (currentStatus !== "RUNNING") {
476
+ throw new RunNotRunningError(
477
+ workflowRunId,
478
+ currentStatus ?? "DELETED"
479
+ );
480
+ }
481
+ await tx.updateStage(stageRecord.id, {
482
+ status: "COMPLETED",
483
+ completedAt: deps.clock.now(),
484
+ duration,
485
+ outputData: {
486
+ _artifactKey: outputKey,
487
+ ...artifactKeys ? { _artifactKeys: artifactKeys } : {}
488
+ },
489
+ metrics: result.metrics,
490
+ embeddingInfo: result.embeddings
491
+ });
492
+ if (bufferedAnnotations.length > 0) {
493
+ await tx.appendAnnotations(bufferedAnnotations);
494
+ }
495
+ const completedEvent = {
496
+ type: "stage:completed",
497
+ timestamp: deps.clock.now(),
498
+ workflowRunId,
499
+ stageId,
500
+ stageName: stageDef.name,
501
+ duration
502
+ };
503
+ await tx.appendOutboxEvents(
504
+ toOutboxEvents(workflowRunId, causationId, [
505
+ ...progressEvents,
506
+ completedEvent,
507
+ ...buildAnnotationEvents(bufferedAnnotations, deps.clock.now())
508
+ ])
509
+ );
301
510
  });
302
- const completedEvent = {
303
- type: "stage:completed",
304
- timestamp: deps.clock.now(),
305
- workflowRunId,
306
- stageId,
307
- stageName: stageDef.name,
308
- duration
309
- };
310
- await tx.appendOutboxEvents(
311
- toOutboxEvents(workflowRunId, causationId, [
312
- ...progressEvents,
313
- completedEvent
314
- ])
315
- );
316
- });
511
+ } catch (txError) {
512
+ if (txError instanceof RunNotRunningError) {
513
+ return {
514
+ outcome: "failed",
515
+ ghost: true,
516
+ error: txError.message,
517
+ _events: []
518
+ };
519
+ }
520
+ throw txError;
521
+ }
317
522
  return {
318
523
  outcome: "completed",
319
524
  output: result.output,
@@ -323,14 +528,25 @@ async function handleJobExecute(command, deps) {
323
528
  } catch (error) {
324
529
  const errorMessage = error instanceof Error ? error.message : String(error);
325
530
  const duration = deps.clock.now().getTime() - startTime;
531
+ const bufferedAnnotations = annotationBuffer.flush();
326
532
  try {
327
533
  await deps.persistence.withTransaction(async (tx) => {
534
+ const currentStatus = await tx.getRunStatus(workflowRunId);
535
+ if (currentStatus !== "RUNNING") {
536
+ throw new RunNotRunningError(
537
+ workflowRunId,
538
+ currentStatus ?? "DELETED"
539
+ );
540
+ }
328
541
  await tx.updateStage(stageRecord.id, {
329
542
  status: "FAILED",
330
543
  completedAt: deps.clock.now(),
331
544
  duration,
332
545
  errorMessage
333
546
  });
547
+ if (bufferedAnnotations.length > 0) {
548
+ await tx.appendAnnotations(bufferedAnnotations);
549
+ }
334
550
  const failedEvent = {
335
551
  type: "stage:failed",
336
552
  timestamp: deps.clock.now(),
@@ -342,11 +558,20 @@ async function handleJobExecute(command, deps) {
342
558
  await tx.appendOutboxEvents(
343
559
  toOutboxEvents(workflowRunId, causationId, [
344
560
  ...progressEvents,
345
- failedEvent
561
+ failedEvent,
562
+ ...buildAnnotationEvents(bufferedAnnotations, deps.clock.now())
346
563
  ])
347
564
  );
348
565
  });
349
- } catch {
566
+ } catch (txError) {
567
+ if (txError instanceof RunNotRunningError) {
568
+ return {
569
+ outcome: "failed",
570
+ ghost: true,
571
+ error: txError.message,
572
+ _events: []
573
+ };
574
+ }
350
575
  throw error;
351
576
  }
352
577
  await deps.persistence.createLog({
@@ -614,6 +839,31 @@ async function handleRunCreate(command, deps) {
614
839
  priority,
615
840
  metadata: command.metadata
616
841
  });
842
+ const annotationEvents = [];
843
+ if (command.annotations && command.annotations.length > 0) {
844
+ const annotationInputs = [];
845
+ for (const entry of command.annotations) {
846
+ for (const [key, value] of Object.entries(entry.attributes)) {
847
+ if (value === void 0 || value === null) continue;
848
+ annotationInputs.push({
849
+ workflowRunId: run.id,
850
+ scope: "run",
851
+ actor: entry.actor,
852
+ key,
853
+ value,
854
+ payload: entry.payload,
855
+ idempotencyKey: entry.idempotencyKey,
856
+ emitEvent: entry.emitEvent
857
+ });
858
+ }
859
+ }
860
+ if (annotationInputs.length > 0) {
861
+ await deps.persistence.appendAnnotations(annotationInputs);
862
+ annotationEvents.push(
863
+ ...buildAnnotationEvents(annotationInputs, deps.clock.now())
864
+ );
865
+ }
866
+ }
617
867
  return {
618
868
  workflowRunId: run.id,
619
869
  status: "PENDING",
@@ -623,7 +873,8 @@ async function handleRunCreate(command, deps) {
623
873
  timestamp: deps.clock.now(),
624
874
  workflowRunId: run.id,
625
875
  workflowId: command.workflowId
626
- }
876
+ },
877
+ ...annotationEvents
627
878
  ]
628
879
  };
629
880
  }
@@ -703,6 +954,11 @@ async function handleRunRerunFrom(command, deps) {
703
954
  (s) => s.executionGroup >= targetGroup
704
955
  );
705
956
  const deletedStageIds = stagesToDelete.map((s) => s.stageId);
957
+ const priorMaxAttempt = stagesToDelete.reduce(
958
+ (max, s) => s.attempt > max ? s.attempt : max,
959
+ 0
960
+ );
961
+ const newAttempt = priorMaxAttempt + 1;
706
962
  for (const stage of stagesToDelete) {
707
963
  const prefix = `workflow-v2/${run.workflowType}/${workflowRunId}/${stage.stageId}/`;
708
964
  const keys = await deps.blobStore.list(prefix).catch(() => []);
@@ -731,6 +987,7 @@ async function handleRunRerunFrom(command, deps) {
731
987
  stageName: stage.name,
732
988
  stageNumber: workflow.getStageIndex(stage.id) + 1,
733
989
  executionGroup: targetGroup,
990
+ attempt: newAttempt,
734
991
  status: "PENDING",
735
992
  config: run.config?.[stage.id] || {}
736
993
  });
@@ -775,6 +1032,11 @@ async function claimRunTransition(run, deps) {
775
1032
  }
776
1033
  async function enqueueExecutionGroup(run, workflow, groupIndex, deps) {
777
1034
  const stages = workflow.getStagesInExecutionGroup(groupIndex);
1035
+ const existingStages = await deps.persistence.getStagesByRun(run.id);
1036
+ const currentAttempt = existingStages.reduce(
1037
+ (max, s) => s.attempt > max ? s.attempt : max,
1038
+ 0
1039
+ );
778
1040
  const stagesToEnqueue = [];
779
1041
  for (const stage of stages) {
780
1042
  const record = await deps.persistence.upsertStage({
@@ -786,6 +1048,7 @@ async function enqueueExecutionGroup(run, workflow, groupIndex, deps) {
786
1048
  stageName: stage.name,
787
1049
  stageNumber: workflow.getStageIndex(stage.id) + 1,
788
1050
  executionGroup: groupIndex,
1051
+ attempt: currentAttempt,
789
1052
  status: "PENDING",
790
1053
  config: run.config?.[stage.id] || {}
791
1054
  },
@@ -932,6 +1195,10 @@ async function markStageCancelled(stageId, deps) {
932
1195
  async function withClaimedRun(workflowRunId, expectedVersion, deps, fn) {
933
1196
  try {
934
1197
  const value = await deps.persistence.withTransaction(async (tx) => {
1198
+ const runStatus = await tx.getRunStatus(workflowRunId);
1199
+ if (runStatus !== "RUNNING") {
1200
+ throw new RunNotRunningError(workflowRunId, runStatus ?? "DELETED");
1201
+ }
935
1202
  await tx.updateRun(workflowRunId, {
936
1203
  expectedVersion
937
1204
  });
@@ -942,6 +1209,9 @@ async function withClaimedRun(workflowRunId, expectedVersion, deps, fn) {
942
1209
  if (error instanceof StaleVersionError) {
943
1210
  return { status: "stale" };
944
1211
  }
1212
+ if (error instanceof RunNotRunningError) {
1213
+ return { status: "cancelled", runStatus: error.currentStatus };
1214
+ }
945
1215
  throw error;
946
1216
  }
947
1217
  }
@@ -1027,6 +1297,28 @@ async function handleStagePollSuspended(command, deps) {
1027
1297
  }).catch(() => {
1028
1298
  });
1029
1299
  };
1300
+ const annotationBuffer = createAnnotationBuffer();
1301
+ const annotateFn = ((...args) => {
1302
+ const stageScopeFields = {
1303
+ workflowRunId: stageRecord.workflowRunId,
1304
+ workflowStageRecordId: stageRecord.id,
1305
+ attempt: stageRecord.attempt,
1306
+ scope: "stage",
1307
+ scopeId: stageRecord.stageId
1308
+ };
1309
+ for (const { key, value, opts } of normalizeAnnotateArgs(args)) {
1310
+ if (value === void 0 || value === null) continue;
1311
+ annotationBuffer.push({
1312
+ ...stageScopeFields,
1313
+ actor: opts?.actor,
1314
+ key,
1315
+ value,
1316
+ payload: opts?.payload,
1317
+ idempotencyKey: opts?.idempotencyKey,
1318
+ emitEvent: opts?.emitEvent
1319
+ });
1320
+ }
1321
+ });
1030
1322
  const checkContext = {
1031
1323
  workflowRunId: run.id,
1032
1324
  stageId: stageRecord.stageId,
@@ -1034,6 +1326,7 @@ async function handleStagePollSuspended(command, deps) {
1034
1326
  config: stageRecord.config || {},
1035
1327
  log: logFn,
1036
1328
  onLog: logFn,
1329
+ annotate: annotateFn,
1037
1330
  storage
1038
1331
  };
1039
1332
  try {
@@ -1042,6 +1335,7 @@ async function handleStagePollSuspended(command, deps) {
1042
1335
  checkContext
1043
1336
  );
1044
1337
  if (checkResult.error) {
1338
+ const bufferedAnnotations = annotationBuffer.flush();
1045
1339
  const claimResult = await withClaimedRun(
1046
1340
  stageRecord.workflowRunId,
1047
1341
  run.version,
@@ -1057,6 +1351,9 @@ async function handleStagePollSuspended(command, deps) {
1057
1351
  status: "FAILED",
1058
1352
  completedAt: deps.clock.now()
1059
1353
  });
1354
+ if (bufferedAnnotations.length > 0) {
1355
+ await tx.appendAnnotations(bufferedAnnotations);
1356
+ }
1060
1357
  await tx.appendOutboxEvents(
1061
1358
  toOutboxEvents2(stageRecord.workflowRunId, [
1062
1359
  {
@@ -1072,7 +1369,8 @@ async function handleStagePollSuspended(command, deps) {
1072
1369
  timestamp: deps.clock.now(),
1073
1370
  workflowRunId: stageRecord.workflowRunId,
1074
1371
  error: checkResult.error
1075
- }
1372
+ },
1373
+ ...buildAnnotationEvents(bufferedAnnotations, deps.clock.now())
1076
1374
  ])
1077
1375
  );
1078
1376
  }
@@ -1086,6 +1384,10 @@ async function handleStagePollSuspended(command, deps) {
1086
1384
  }
1087
1385
  continue;
1088
1386
  }
1387
+ if (claimResult.status === "cancelled") {
1388
+ await markStageCancelled(stageRecord.id, deps);
1389
+ continue;
1390
+ }
1089
1391
  failed++;
1090
1392
  } else if (checkResult.ready) {
1091
1393
  let outputRef;
@@ -1105,6 +1407,7 @@ async function handleStagePollSuspended(command, deps) {
1105
1407
  outputRef = { _artifactKey: outputKey };
1106
1408
  }
1107
1409
  const duration = deps.clock.now().getTime() - (stageRecord.startedAt?.getTime() ?? deps.clock.now().getTime());
1410
+ const bufferedAnnotations = annotationBuffer.flush();
1108
1411
  const claimResult = await withClaimedRun(
1109
1412
  stageRecord.workflowRunId,
1110
1413
  run.version,
@@ -1119,6 +1422,9 @@ async function handleStagePollSuspended(command, deps) {
1119
1422
  metrics: checkResult.metrics,
1120
1423
  embeddingInfo: checkResult.embeddings
1121
1424
  });
1425
+ if (bufferedAnnotations.length > 0) {
1426
+ await tx.appendAnnotations(bufferedAnnotations);
1427
+ }
1122
1428
  await tx.appendOutboxEvents(
1123
1429
  toOutboxEvents2(stageRecord.workflowRunId, [
1124
1430
  {
@@ -1128,7 +1434,8 @@ async function handleStagePollSuspended(command, deps) {
1128
1434
  stageId: stageRecord.stageId,
1129
1435
  stageName: stageRecord.stageName,
1130
1436
  duration
1131
- }
1437
+ },
1438
+ ...buildAnnotationEvents(bufferedAnnotations, deps.clock.now())
1132
1439
  ])
1133
1440
  );
1134
1441
  }
@@ -1142,11 +1449,16 @@ async function handleStagePollSuspended(command, deps) {
1142
1449
  }
1143
1450
  continue;
1144
1451
  }
1452
+ if (claimResult.status === "cancelled") {
1453
+ await markStageCancelled(stageRecord.id, deps);
1454
+ continue;
1455
+ }
1145
1456
  resumed++;
1146
1457
  resumedWorkflowRunIds.add(stageRecord.workflowRunId);
1147
1458
  } else {
1148
1459
  const pollInterval = checkResult.nextCheckIn ?? stageRecord.pollInterval ?? 6e4;
1149
1460
  const nextPollAt = new Date(deps.clock.now().getTime() + pollInterval);
1461
+ const bufferedAnnotations = annotationBuffer.flush();
1150
1462
  const claimResult = await withClaimedRun(
1151
1463
  stageRecord.workflowRunId,
1152
1464
  run.version,
@@ -1155,6 +1467,9 @@ async function handleStagePollSuspended(command, deps) {
1155
1467
  await tx.updateStage(stageRecord.id, {
1156
1468
  nextPollAt
1157
1469
  });
1470
+ if (bufferedAnnotations.length > 0) {
1471
+ await tx.appendAnnotations(bufferedAnnotations);
1472
+ }
1158
1473
  }
1159
1474
  );
1160
1475
  if (claimResult.status === "stale") {
@@ -1166,9 +1481,14 @@ async function handleStagePollSuspended(command, deps) {
1166
1481
  }
1167
1482
  continue;
1168
1483
  }
1484
+ if (claimResult.status === "cancelled") {
1485
+ await markStageCancelled(stageRecord.id, deps);
1486
+ continue;
1487
+ }
1169
1488
  }
1170
1489
  } catch (error) {
1171
1490
  const errorMessage = error instanceof Error ? error.message : String(error);
1491
+ const bufferedAnnotations = annotationBuffer.flush();
1172
1492
  const claimResult = await withClaimedRun(
1173
1493
  stageRecord.workflowRunId,
1174
1494
  run.version,
@@ -1184,6 +1504,9 @@ async function handleStagePollSuspended(command, deps) {
1184
1504
  status: "FAILED",
1185
1505
  completedAt: deps.clock.now()
1186
1506
  });
1507
+ if (bufferedAnnotations.length > 0) {
1508
+ await tx.appendAnnotations(bufferedAnnotations);
1509
+ }
1187
1510
  await tx.appendOutboxEvents(
1188
1511
  toOutboxEvents2(stageRecord.workflowRunId, [
1189
1512
  {
@@ -1199,7 +1522,8 @@ async function handleStagePollSuspended(command, deps) {
1199
1522
  timestamp: deps.clock.now(),
1200
1523
  workflowRunId: stageRecord.workflowRunId,
1201
1524
  error: errorMessage
1202
- }
1525
+ },
1526
+ ...buildAnnotationEvents(bufferedAnnotations, deps.clock.now())
1203
1527
  ])
1204
1528
  );
1205
1529
  }
@@ -1213,6 +1537,10 @@ async function handleStagePollSuspended(command, deps) {
1213
1537
  }
1214
1538
  continue;
1215
1539
  }
1540
+ if (claimResult.status === "cancelled") {
1541
+ await markStageCancelled(stageRecord.id, deps);
1542
+ continue;
1543
+ }
1216
1544
  failed++;
1217
1545
  }
1218
1546
  }
@@ -1407,7 +1735,69 @@ function createKernel(config) {
1407
1735
  throw error;
1408
1736
  }
1409
1737
  }
1410
- return { dispatch };
1738
+ const annotations = {
1739
+ async attach(workflowRunId, input) {
1740
+ const scope = input.scope ?? "run";
1741
+ const inputs = [];
1742
+ for (const [key, value] of Object.entries(input.attributes)) {
1743
+ if (value === void 0 || value === null) continue;
1744
+ inputs.push({
1745
+ workflowRunId,
1746
+ workflowStageRecordId: input.workflowStageRecordId ?? null,
1747
+ attempt: input.attempt,
1748
+ scope,
1749
+ scopeId: input.scopeId ?? null,
1750
+ actor: input.actor,
1751
+ key,
1752
+ value,
1753
+ payload: input.payload,
1754
+ idempotencyKey: input.idempotencyKey,
1755
+ emitEvent: input.emitEvent
1756
+ });
1757
+ }
1758
+ if (inputs.length === 0) return;
1759
+ const causationId = input.idempotencyKey ?? crypto.randomUUID();
1760
+ await persistence.withTransaction(async (tx) => {
1761
+ await tx.appendAnnotations(inputs);
1762
+ const events = buildAnnotationEvents(inputs, clock.now());
1763
+ if (events.length > 0) {
1764
+ await tx.appendOutboxEvents(
1765
+ events.map((event) => ({
1766
+ workflowRunId: event.workflowRunId,
1767
+ eventType: event.type,
1768
+ payload: event,
1769
+ causationId,
1770
+ occurredAt: event.timestamp
1771
+ }))
1772
+ );
1773
+ }
1774
+ });
1775
+ },
1776
+ async list(workflowRunId, filters) {
1777
+ const persisted = await persistence.listAnnotations(
1778
+ workflowRunId,
1779
+ filters
1780
+ );
1781
+ if (!filterCouldMatchLegacy(filters ?? {})) return persisted;
1782
+ const migrationCheck = await persistence.listAnnotations(workflowRunId, {
1783
+ keyPrefix: "legacy.metadata.",
1784
+ limit: 1
1785
+ });
1786
+ if (migrationCheck.length > 0) return persisted;
1787
+ const run = await persistence.getRun(workflowRunId);
1788
+ if (!run) return persisted;
1789
+ const synthesized = synthesizeLegacyMetadata(run, filters);
1790
+ if (synthesized.length === 0) return persisted;
1791
+ const merged = [...persisted, ...synthesized].sort((a, b) => {
1792
+ const cmp = a.createdAt.getTime() - b.createdAt.getTime();
1793
+ if (cmp !== 0) return cmp;
1794
+ return a.id.localeCompare(b.id);
1795
+ });
1796
+ const limit = filters?.limit ?? 1e3;
1797
+ return merged.slice(0, limit);
1798
+ }
1799
+ };
1800
+ return { dispatch, annotations };
1411
1801
  }
1412
1802
 
1413
1803
  // src/kernel/plugins.ts
@@ -1437,5 +1827,5 @@ function createPluginRunner(config) {
1437
1827
  }
1438
1828
 
1439
1829
  export { IdempotencyInProgressError, createKernel, createPluginRunner, definePlugin, loadWorkflowContext, saveStageOutput };
1440
- //# sourceMappingURL=chunk-HOGDFLCG.js.map
1441
- //# sourceMappingURL=chunk-HOGDFLCG.js.map
1830
+ //# sourceMappingURL=chunk-XPWAEYOO.js.map
1831
+ //# sourceMappingURL=chunk-XPWAEYOO.js.map