@fieldwangai/agentflow 0.1.136 → 0.1.138

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.
@@ -21,6 +21,14 @@ function cleanString(value, max = 4000) {
21
21
  return String(value ?? "").trim().slice(0, max);
22
22
  }
23
23
 
24
+ function rawString(value) {
25
+ return String(value ?? "").trim();
26
+ }
27
+
28
+ function stringExceeds(value, max) {
29
+ return rawString(value).length > max;
30
+ }
31
+
24
32
  function hasOwn(value, key) {
25
33
  return Boolean(value && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, key));
26
34
  }
@@ -76,6 +84,26 @@ function normalizeActionStatus(value) {
76
84
  return WORKFLOW_ACTION_STATUSES.has(normalized) ? normalized : "pending";
77
85
  }
78
86
 
87
+ function isKnownActionStatus(value) {
88
+ const raw = cleanString(value, 40).toLowerCase();
89
+ if (!raw) return true;
90
+ return WORKFLOW_ACTION_STATUSES.has(raw) || [
91
+ "success", "succeeded", "complete", "completed", "failed", "failure", "canceled", "in_progress", "in-progress",
92
+ ].includes(raw);
93
+ }
94
+
95
+ export function isSafeWorkflowUrl(value, { allowRelative = true } = {}) {
96
+ const raw = rawString(value);
97
+ if (!raw) return true;
98
+ if (allowRelative && raw.startsWith("/") && !raw.startsWith("//")) return true;
99
+ try {
100
+ const parsed = new URL(raw);
101
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
79
107
  function normalizeStringList(value, maxItems = 100) {
80
108
  const list = Array.isArray(value) ? value : value == null || value === "" ? [] : [value];
81
109
  return uniqueValues(list.map((item) => cleanString(item, 240)).filter(Boolean)).slice(0, maxItems);
@@ -136,8 +164,7 @@ function normalizeWorkflowProjectionState(value = {}) {
136
164
  if (Array.isArray(value?.timeline)) {
137
165
  raw.timeline = value.timeline
138
166
  .map((item, index) => normalizeWorkflowTimelineProjection(item, index))
139
- .filter(Boolean)
140
- .slice(0, 100);
167
+ .filter(Boolean);
141
168
  } else {
142
169
  delete raw.timeline;
143
170
  }
@@ -189,7 +216,6 @@ export function materializeWorkflowProjections(snapshot = {}, runtimeEvents = []
189
216
  ? next.timeline
190
217
  .map((item, index) => normalizeWorkflowTimelineProjection(item, index))
191
218
  .filter(Boolean)
192
- .slice(0, 100)
193
219
  : projections.timeline || [],
194
220
  };
195
221
  }
@@ -230,11 +256,22 @@ export function removeWorkflowGlobalStatePath(value, rawPath) {
230
256
  }
231
257
 
232
258
  export function normalizeWorkflowReference(payload = {}) {
233
- const workflow = plainObject(payload.workflow);
234
- const explicitKey = cleanString(workflow.key || payload.workflowKey || payload.workflow_key, 400);
259
+ const workflowInput = payload.workflow;
260
+ const workflow = plainObject(workflowInput);
261
+ const rawExplicitKey = rawString(
262
+ (typeof workflowInput === "string" ? workflowInput : "") || workflow.key || payload.workflowKey || payload.workflow_key,
263
+ );
264
+ if (rawExplicitKey.length > 400) return { error: "Workflow key exceeds 400 characters" };
265
+ if (/[\0\r\n]/.test(rawExplicitKey)) return { error: "Workflow key contains control characters" };
266
+ const explicitKey = rawExplicitKey;
235
267
  const keySeparator = explicitKey.indexOf(":");
236
268
  const keyedNamespace = keySeparator > 0 ? explicitKey.slice(0, keySeparator) : "";
237
269
  const keyedId = keySeparator > 0 ? explicitKey.slice(keySeparator + 1) : explicitKey;
270
+ const rawNamespace = rawString(workflow.namespace || workflow.type || payload.workflowNamespace || payload.workflow_namespace || keyedNamespace || "tapd");
271
+ const rawId = rawString(workflow.id || keyedId || payload.workflowId || payload.workflow_id || payload.tapdId || payload.tapd_id);
272
+ if (rawNamespace.length > 80) return { error: "Workflow namespace exceeds 80 characters" };
273
+ if (rawId.length > 240) return { error: "Workflow id exceeds 240 characters" };
274
+ if (/[\0\r\n]/.test(rawId)) return { error: "Workflow id contains control characters" };
238
275
  const namespace = cleanString(
239
276
  workflow.namespace || workflow.type || payload.workflowNamespace || payload.workflow_namespace || keyedNamespace || "tapd",
240
277
  80,
@@ -260,14 +297,17 @@ export function normalizeWorkflowReference(payload = {}) {
260
297
  }
261
298
 
262
299
  export function normalizeWorkflowReport(payload = {}) {
263
- const schemaVersion = Number(payload.schemaVersion || payload.schema_version || WORKFLOW_REPORT_SCHEMA_VERSION);
300
+ const schemaValue = payload.schemaVersion ?? payload.schema_version ?? WORKFLOW_REPORT_SCHEMA_VERSION;
301
+ const schemaVersion = Number(schemaValue);
264
302
  if (schemaVersion !== WORKFLOW_REPORT_SCHEMA_VERSION) {
265
303
  return { error: `Unsupported workflow report schemaVersion: ${schemaVersion}` };
266
304
  }
267
305
  const workflow = normalizeWorkflowReference(payload);
268
306
  if (workflow.error) return workflow;
269
307
  const rawWorkflow = plainObject(payload.workflow);
270
- const source = cleanString(payload.source || "agentflow-cli", 120).toLowerCase() || "agentflow-cli";
308
+ if (!rawString(payload.source)) return { error: "Workflow report requires source" };
309
+ if (stringExceeds(payload.source, 120)) return { error: "Workflow report source exceeds 120 characters" };
310
+ const source = cleanString(payload.source, 120).toLowerCase();
271
311
  if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
272
312
  return { error: "Invalid workflow report source" };
273
313
  }
@@ -278,29 +318,49 @@ export function normalizeWorkflowReport(payload = {}) {
278
318
  if (hasObservation && !Object.keys(observationState).length) {
279
319
  return { error: "observation requires state" };
280
320
  }
321
+ if (stringExceeds(rawObservation.schema || rawObservation.model, 160)) return { error: "observation.schema exceeds 160 characters" };
322
+ if (stringExceeds(rawObservation.clientId || rawObservation.client_id || source, 160)) return { error: "observation.clientId exceeds 160 characters" };
323
+ if (stringExceeds(rawObservation.scope || "client", 80)) return { error: "observation.scope exceeds 80 characters" };
324
+ const rawObservedAt = rawString(rawObservation.observedAt || rawObservation.observed_at);
325
+ if (rawObservedAt && !Number.isFinite(Date.parse(rawObservedAt))) return { error: "observation.observedAt must be an ISO-compatible date" };
281
326
  const observation = hasObservation ? {
282
327
  schema: cleanString(rawObservation.schema || rawObservation.model || "workflow-observation/v1", 160),
283
328
  state: mergeWorkflowGlobalState({}, observationState),
284
329
  observedAt: cleanString(rawObservation.observedAt || rawObservation.observed_at, 80),
285
- clientId: cleanString(rawObservation.clientId || rawObservation.client_id, 160),
330
+ clientId: cleanString(rawObservation.clientId || rawObservation.client_id || source, 160),
286
331
  scope: cleanString(rawObservation.scope || "client", 80).toLowerCase() || "client",
287
332
  } : null;
288
333
 
289
334
  const rawAction = plainObject(payload.action);
290
335
  const hasAction = Object.keys(rawAction).length > 0;
336
+ if (stringExceeds(rawAction.key || rawAction.id || rawAction.actionKey || rawAction.action_key, 240)) {
337
+ return { error: "Workflow action key exceeds 240 characters" };
338
+ }
291
339
  const actionKey = cleanString(rawAction.key || rawAction.id || rawAction.actionKey || rawAction.action_key, 240);
292
340
  if (hasAction && !actionKey) return { error: "Workflow action requires a stable key" };
341
+ if (hasAction && /[\0\r\n]/.test(actionKey)) return { error: "Workflow action key contains control characters" };
342
+ if (stringExceeds(rawAction.title || rawAction.label || actionKey, 500)) return { error: "Workflow action title exceeds 500 characters" };
343
+ if (stringExceeds(rawAction.detail || rawAction.description || rawAction.message, 4000)) return { error: "Workflow action detail exceeds 4000 characters" };
344
+ if (stringExceeds(rawAction.group || rawAction.stage || rawAction.category, 120)) return { error: "Workflow action group exceeds 120 characters" };
345
+ if (stringExceeds(rawAction.scope, 80)) return { error: "Workflow action scope exceeds 80 characters" };
346
+ if (stringExceeds(rawAction.platform, 80)) return { error: "Workflow action platform exceeds 80 characters" };
347
+ if (stringExceeds(rawAction.issueKey || rawAction.issue_key, 240)) return { error: "Workflow action issueKey exceeds 240 characters" };
348
+ const rawTags = Array.isArray(rawAction.tags) ? rawAction.tags : rawAction.tags == null ? [] : [rawAction.tags];
349
+ if (rawTags.length > 100 || rawTags.some((tag) => stringExceeds(tag, 240))) return { error: "Workflow action tags exceed supported limits" };
350
+ if (hasAction && !isKnownActionStatus(rawAction.status)) return { error: `Invalid workflow action status: ${rawAction.status}` };
351
+ const rawOccurredAt = rawString(rawAction.occurredAt || rawAction.occurred_at || rawAction.completedAt || rawAction.startedAt);
352
+ if (rawOccurredAt && !Number.isFinite(Date.parse(rawOccurredAt))) return { error: "action.occurredAt must be an ISO-compatible date" };
293
353
  const action = hasAction ? {
294
354
  ...rawAction,
295
355
  key: actionKey,
296
356
  title: cleanString(rawAction.title || rawAction.label || actionKey, 500),
297
- detail: cleanString(rawAction.detail || rawAction.description || rawAction.message, 4000),
357
+ ...(rawAction.detail || rawAction.description || rawAction.message ? { detail: cleanString(rawAction.detail || rawAction.description || rawAction.message, 4000) } : {}),
298
358
  status: normalizeActionStatus(rawAction.status),
299
- group: cleanString(rawAction.group || rawAction.stage || rawAction.category, 120),
300
- scope: cleanString(rawAction.scope, 80),
301
- platform: cleanString(rawAction.platform, 80),
302
- issueKey: cleanString(rawAction.issueKey || rawAction.issue_key, 240),
303
- tags: normalizeStringList(rawAction.tags),
359
+ ...(rawAction.group || rawAction.stage || rawAction.category ? { group: cleanString(rawAction.group || rawAction.stage || rawAction.category, 120) } : {}),
360
+ ...(rawAction.scope ? { scope: cleanString(rawAction.scope, 80) } : {}),
361
+ ...(rawAction.platform ? { platform: cleanString(rawAction.platform, 80) } : {}),
362
+ ...(rawAction.issueKey || rawAction.issue_key ? { issueKey: cleanString(rawAction.issueKey || rawAction.issue_key, 240) } : {}),
363
+ ...(rawAction.tags != null ? { tags: normalizeStringList(rawAction.tags) } : {}),
304
364
  occurredAt: cleanString(
305
365
  rawAction.occurredAt ||
306
366
  rawAction.occurred_at ||
@@ -311,6 +371,31 @@ export function normalizeWorkflowReport(payload = {}) {
311
371
  } : null;
312
372
 
313
373
  const rawArtifacts = Array.isArray(payload.artifacts) ? payload.artifacts : [];
374
+ if (rawArtifacts.length > 100) return { error: "Workflow report supports at most 100 artifacts per request" };
375
+ const invalidArtifactShapeIndex = rawArtifacts.findIndex((item) => !item || typeof item !== "object" || Array.isArray(item));
376
+ if (invalidArtifactShapeIndex >= 0) return { error: `artifacts[${invalidArtifactShapeIndex}] must be an object` };
377
+ const missingArtifactTargetIndex = rawArtifacts.findIndex((item) => !rawString(item?.url || item?.href) && !rawString(item?.path));
378
+ if (missingArtifactTargetIndex >= 0) return { error: `artifacts[${missingArtifactTargetIndex}] requires url or path` };
379
+ const oversizedArtifactIndex = rawArtifacts.findIndex((item) => (
380
+ stringExceeds(item?.url || item?.href, 4000) ||
381
+ stringExceeds(item?.path, 4000) ||
382
+ stringExceeds(item?.title || item?.label || item?.name, 500) ||
383
+ stringExceeds(item?.type || item?.kind, 120) ||
384
+ stringExceeds(item?.label, 500) ||
385
+ stringExceeds(item?.status, 80)
386
+ ));
387
+ if (oversizedArtifactIndex >= 0) return { error: `artifacts[${oversizedArtifactIndex}] exceeds supported field limits` };
388
+ const invalidArtifactIndex = rawArtifacts.findIndex((item) => {
389
+ if (!item || typeof item !== "object" || Array.isArray(item)) return false;
390
+ const url = item.url || item.href;
391
+ return Boolean(url) && !isSafeWorkflowUrl(url);
392
+ });
393
+ if (invalidArtifactIndex >= 0) return { error: `artifacts[${invalidArtifactIndex}].url must use http, https, or an absolute application path` };
394
+ const invalidArtifactKeyIndex = rawArtifacts.findIndex((item) => {
395
+ const key = rawString(item?.key || item?.id || item?.artifactKey || item?.artifact_key);
396
+ return key.length > 500 || /[\0\r\n]/.test(key);
397
+ });
398
+ if (invalidArtifactKeyIndex >= 0) return { error: `artifacts[${invalidArtifactKeyIndex}].key is invalid` };
314
399
  const artifacts = rawArtifacts
315
400
  .filter((item) => item && typeof item === "object" && !Array.isArray(item))
316
401
  .map((item, index) => ({
@@ -326,25 +411,81 @@ export function normalizeWorkflowReport(payload = {}) {
326
411
  if (hasProjections && !Array.isArray(rawProjections.timeline)) {
327
412
  return { error: "projections.timeline must be an array" };
328
413
  }
414
+ if (hasProjections && rawProjections.timeline.length > 100) {
415
+ return { error: "projections.timeline supports at most 100 entries" };
416
+ }
329
417
  const invalidTimelineIndex = hasProjections
330
418
  ? rawProjections.timeline.findIndex((item, index) => !normalizeWorkflowTimelineProjection(item, index))
331
419
  : -1;
332
420
  if (invalidTimelineIndex >= 0) {
333
421
  return { error: `projections.timeline[${invalidTimelineIndex}] requires kind and id` };
334
422
  }
335
- const projections = hasProjections ? normalizeWorkflowProjectionState(rawProjections) : null;
423
+ const oversizedTimelineIndex = hasProjections
424
+ ? rawProjections.timeline.findIndex((item) => (
425
+ stringExceeds(item?.kind || item?.type, 80) ||
426
+ stringExceeds(item?.id || item?.key, 240) ||
427
+ stringExceeds(item?.key, 500) ||
428
+ stringExceeds(item?.title || item?.label, 500) ||
429
+ stringExceeds(item?.source || item?.namespace || source, 120) ||
430
+ stringExceeds(item?.date || item?.targetDate || item?.target_date, 80)
431
+ ))
432
+ : -1;
433
+ if (oversizedTimelineIndex >= 0) return { error: `projections.timeline[${oversizedTimelineIndex}] exceeds supported field limits` };
434
+ const invalidTimelineSourceIndex = hasProjections
435
+ ? rawProjections.timeline.findIndex((item) => {
436
+ const itemSource = cleanString(item?.source || item?.namespace || source, 120).toLowerCase();
437
+ return !/^[a-z][a-z0-9._-]{0,119}$/.test(itemSource);
438
+ })
439
+ : -1;
440
+ if (invalidTimelineSourceIndex >= 0) return { error: `projections.timeline[${invalidTimelineSourceIndex}].source is invalid` };
441
+ const invalidTimelineDateIndex = hasProjections
442
+ ? rawProjections.timeline.findIndex((item) => {
443
+ const date = rawString(item?.date || item?.targetDate || item?.target_date);
444
+ return date && !Number.isFinite(Date.parse(date));
445
+ })
446
+ : -1;
447
+ if (invalidTimelineDateIndex >= 0) return { error: `projections.timeline[${invalidTimelineDateIndex}].date must be ISO-compatible` };
448
+ const projections = hasProjections ? normalizeWorkflowProjectionState({
449
+ ...rawProjections,
450
+ timeline: rawProjections.timeline.map((item) => ({
451
+ ...plainObject(item),
452
+ source: cleanString(item?.source || item?.namespace || source, 120).toLowerCase(),
453
+ })),
454
+ }) : null;
336
455
 
337
456
  const rawExtensions = plainObject(payload.extensions);
338
457
  const hasExtensions = Object.keys(rawExtensions).length > 0;
458
+ const invalidExtensionNamespace = Object.keys(rawExtensions).findIndex((namespace) => (
459
+ !/^[a-z][a-z0-9._-]{0,119}$/.test(rawString(namespace).toLowerCase()) ||
460
+ !rawExtensions[namespace] ||
461
+ typeof rawExtensions[namespace] !== "object" ||
462
+ Array.isArray(rawExtensions[namespace])
463
+ ));
464
+ if (invalidExtensionNamespace >= 0) {
465
+ const namespace = Object.keys(rawExtensions)[invalidExtensionNamespace];
466
+ return { error: `extensions[${namespace}] must be a valid namespace object` };
467
+ }
339
468
  const extensions = hasExtensions ? normalizeWorkflowExtensions(rawExtensions) : null;
340
469
  if (hasExtensions && !Object.keys(extensions).length) {
341
470
  return { error: "extensions requires at least one valid namespace object" };
342
471
  }
472
+ if (hasExtensions && Object.keys(extensions).some((namespace) => namespace !== source)) {
473
+ return { error: "extensions may only update the namespace matching report source" };
474
+ }
343
475
 
344
476
  const rawGlobalState = plainObject(payload.globalState || payload.global_state);
345
477
  const hasGlobalState = Object.keys(rawGlobalState).length > 0;
346
478
  const globalStatePatch = plainObject(rawGlobalState.patch);
347
- const globalStateRemove = normalizeStringList(rawGlobalState.remove || rawGlobalState.removePaths || rawGlobalState.remove_paths);
479
+ const rawGlobalStateRemove = rawGlobalState.remove || rawGlobalState.removePaths || rawGlobalState.remove_paths;
480
+ const rawGlobalStateRemoveList = Array.isArray(rawGlobalStateRemove) ? rawGlobalStateRemove : rawGlobalStateRemove == null || rawGlobalStateRemove === "" ? [] : [rawGlobalStateRemove];
481
+ if (rawGlobalStateRemoveList.length > 100 || rawGlobalStateRemoveList.some((item) => stringExceeds(item, 240))) {
482
+ return { error: "globalState.remove exceeds supported limits" };
483
+ }
484
+ const globalStateRemove = normalizeStringList(rawGlobalStateRemove);
485
+ const globalStateOwnerPaths = uniqueValues([
486
+ ...patchLeafPaths(globalStatePatch).filter((path) => path.length).map((path) => path.join(".")),
487
+ ...globalStateRemove,
488
+ ]);
348
489
  const mode = cleanString(rawGlobalState.mode || "merge", 40).toLowerCase() || "merge";
349
490
  if (hasGlobalState && mode !== "merge") return { error: "globalState.mode must be merge" };
350
491
  if (hasGlobalState && !Object.keys(globalStatePatch).length && !globalStateRemove.length) {
@@ -361,9 +502,23 @@ export function normalizeWorkflowReport(payload = {}) {
361
502
  action?.idempotency_key,
362
503
  500,
363
504
  );
505
+ if (stringExceeds(payload.idempotencyKey || payload.idempotency_key || action?.idempotencyKey || action?.idempotency_key, 500)) {
506
+ return { error: "idempotencyKey exceeds 500 characters" };
507
+ }
508
+ if (stringExceeds(payload.expectedRevision || payload.expected_revision, 500)) return { error: "expectedRevision exceeds 500 characters" };
509
+ const rawExpectedVersions = plainObject(payload.expectedVersions || payload.expected_versions);
510
+ const expectedVersions = {};
511
+ for (const [resourceKey, version] of Object.entries(rawExpectedVersions)) {
512
+ const key = rawString(resourceKey);
513
+ if (!key || key.length > 800 || /[\0\r\n]/.test(key)) return { error: "Invalid expectedVersions resource key" };
514
+ const normalizedVersion = version == null || version === "" ? "absent" : rawString(version);
515
+ if (normalizedVersion.length > 160) return { error: `expectedVersions[${key}] exceeds 160 characters` };
516
+ expectedVersions[key] = normalizedVersion;
517
+ }
364
518
  const event = {
365
519
  schemaVersion,
366
520
  type: "workflow-report",
521
+ operation: "report",
367
522
  source,
368
523
  workflow,
369
524
  workflowKey: workflow.key,
@@ -381,9 +536,9 @@ export function normalizeWorkflowReport(payload = {}) {
381
536
  detail: action.detail,
382
537
  status: action.status,
383
538
  scope: action.scope || action.group || "workflow",
384
- platform: action.platform,
385
- issueKey: action.issueKey,
386
- tags: action.tags,
539
+ ...(action.platform ? { platform: action.platform } : {}),
540
+ ...(action.issueKey ? { issueKey: action.issueKey } : {}),
541
+ ...(action.tags ? { tags: action.tags } : {}),
387
542
  ...(action.occurredAt ? { occurredAt: action.occurredAt } : {}),
388
543
  } : {
389
544
  title: cleanString(payload.title || "Workflow 全局状态更新", 500),
@@ -397,9 +552,23 @@ export function normalizeWorkflowReport(payload = {}) {
397
552
  ...(hasGlobalState ? {
398
553
  globalStatePatch,
399
554
  globalStateRemove,
555
+ globalStateOwnerPaths,
400
556
  } : {}),
401
557
  ...(idempotencyKey ? { idempotencyKey } : {}),
402
558
  };
559
+ if (idempotencyKey) {
560
+ event.idempotencyFingerprint = semanticHash({
561
+ workflow,
562
+ source,
563
+ observation,
564
+ action,
565
+ artifacts,
566
+ globalState: hasGlobalState ? { patch: globalStatePatch, remove: globalStateRemove } : null,
567
+ projections,
568
+ extensions,
569
+ });
570
+ event.idempotencyFingerprints = { [idempotencyKey]: event.idempotencyFingerprint };
571
+ }
403
572
  return {
404
573
  schemaVersion,
405
574
  workflow,
@@ -415,6 +584,7 @@ export function normalizeWorkflowReport(payload = {}) {
415
584
  remove: globalStateRemove,
416
585
  } : null,
417
586
  expectedRevision: cleanString(payload.expectedRevision || payload.expected_revision, 500),
587
+ expectedVersions,
418
588
  idempotencyKey,
419
589
  flowId: cleanString(payload.flowId || payload.flow_id || rawWorkflow.flowId || rawWorkflow.flow_id, 240),
420
590
  flowSource: cleanString(payload.flowSource || payload.flow_source || rawWorkflow.flowSource || rawWorkflow.flow_source || "user", 80) || "user",
@@ -422,6 +592,102 @@ export function normalizeWorkflowReport(payload = {}) {
422
592
  };
423
593
  }
424
594
 
595
+ function semanticHash(value) {
596
+ return crypto.createHash("sha256").update(JSON.stringify(stableValue(value))).digest("hex").slice(0, 24);
597
+ }
598
+
599
+ function resourceVersion(value) {
600
+ return `rv:${semanticHash(value)}`;
601
+ }
602
+
603
+ function addObjectResourceVersions(out, prefix, value, path = []) {
604
+ if (value === undefined) return;
605
+ if (path.length) out[`${prefix}:${path.join(".")}`] = resourceVersion(value);
606
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
607
+ for (const [key, child] of Object.entries(value)) {
608
+ if (UNSAFE_OBJECT_KEYS.has(key)) continue;
609
+ addObjectResourceVersions(out, prefix, child, [...path, key]);
610
+ }
611
+ }
612
+
613
+ export function workflowSnapshotResourceVersions(snapshot = {}) {
614
+ const out = {};
615
+ const runtimeEvents = Array.isArray(snapshot.runtimeEvents || snapshot.runtime_events)
616
+ ? (snapshot.runtimeEvents || snapshot.runtime_events)
617
+ : [];
618
+ for (const event of runtimeEvents) {
619
+ const source = cleanString(event?.source || event?.producer || "agentflow", 120).toLowerCase() || "agentflow";
620
+ const actionKey = cleanString(event?.actionModel?.key || event?.action || event?.actionId || event?.stageKey, 240);
621
+ if (actionKey && event?.auxiliary !== true) out[`action:${source}:${actionKey}`] = resourceVersion(event);
622
+ for (const artifact of Array.isArray(event?.artifacts) ? event.artifacts : []) {
623
+ const producer = cleanString(artifact?.producer || source, 120).toLowerCase() || source;
624
+ const key = cleanString(artifact?.key || artifact?.artifactKey || artifact?.artifact_key, 500);
625
+ if (key) out[`artifact:${producer}:${key}`] = resourceVersion(artifact);
626
+ }
627
+ }
628
+ for (const artifact of Array.isArray(snapshot.artifacts) ? snapshot.artifacts : []) {
629
+ const producer = cleanString(artifact?.producer || "legacy", 120).toLowerCase() || "legacy";
630
+ const key = cleanString(artifact?.key || artifact?.artifactKey || artifact?.artifact_key, 500);
631
+ if (key) out[`artifact:${producer}:${key}`] = resourceVersion(artifact);
632
+ }
633
+ for (const projection of Array.isArray(snapshot?.projections?.timeline) ? snapshot.projections.timeline : []) {
634
+ const source = cleanString(projection?.source || "legacy", 120).toLowerCase() || "legacy";
635
+ const kind = cleanString(projection?.kind, 80).toLowerCase();
636
+ const id = cleanString(projection?.id, 240);
637
+ if (kind && id) out[`projection:${source}:${kind}:${id}`] = resourceVersion(projection);
638
+ }
639
+ addObjectResourceVersions(out, "global", plainObject(snapshot.globalState));
640
+ for (const [namespace, value] of Object.entries(plainObject(snapshot.extensions))) {
641
+ addObjectResourceVersions(out, `extension:${namespace}`, value);
642
+ }
643
+ for (const observation of Array.isArray(snapshot.clientObservations) ? snapshot.clientObservations : []) {
644
+ const source = cleanString(observation?.source || "legacy", 120).toLowerCase() || "legacy";
645
+ const clientId = cleanString(observation?.clientId, 160);
646
+ if (clientId) out[`observation:${source}:${clientId}`] = resourceVersion(observation);
647
+ }
648
+ return out;
649
+ }
650
+
651
+ function patchLeafPaths(value, path = []) {
652
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [path];
653
+ const entries = Object.entries(value).filter(([key]) => !UNSAFE_OBJECT_KEYS.has(key));
654
+ if (!entries.length) return [path];
655
+ return entries.flatMap(([key, child]) => patchLeafPaths(child, [...path, key]));
656
+ }
657
+
658
+ export function workflowReportResourceKeys(report = {}, currentSnapshot = {}) {
659
+ const keys = new Set();
660
+ const source = cleanString(report?.event?.source || "agentflow-cli", 120).toLowerCase() || "agentflow-cli";
661
+ if (report.action?.key) keys.add(`action:${source}:${report.action.key}`);
662
+ for (const artifact of Array.isArray(report.artifacts) ? report.artifacts : []) {
663
+ if (artifact?.key) keys.add(`artifact:${source}:${artifact.key}`);
664
+ }
665
+ for (const path of patchLeafPaths(report?.globalState?.patch || {})) {
666
+ if (path.length) keys.add(`global:${path.join(".")}`);
667
+ }
668
+ for (const path of Array.isArray(report?.globalState?.remove) ? report.globalState.remove : []) {
669
+ if (path) keys.add(`global:${path}`);
670
+ }
671
+ for (const [namespace, extension] of Object.entries(plainObject(report.extensions))) {
672
+ for (const path of patchLeafPaths(extension)) {
673
+ if (path.length) keys.add(`extension:${namespace}:${path.join(".")}`);
674
+ }
675
+ }
676
+ if (report.projections) {
677
+ const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
678
+ const incoming = Array.isArray(report.projections.timeline) ? report.projections.timeline : [];
679
+ for (const item of [...current, ...incoming]) {
680
+ const owner = cleanString(item?.source || source, 120).toLowerCase() || source;
681
+ if (owner !== source) continue;
682
+ const kind = cleanString(item?.kind, 80).toLowerCase();
683
+ const id = cleanString(item?.id, 240);
684
+ if (kind && id) keys.add(`projection:${source}:${kind}:${id}`);
685
+ }
686
+ }
687
+ if (report.observation?.clientId) keys.add(`observation:${source}:${report.observation.clientId}`);
688
+ return [...keys].sort();
689
+ }
690
+
425
691
  function displayValue(value) {
426
692
  if (value == null) return "";
427
693
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
@@ -592,17 +858,11 @@ export function mergeWorkflowArtifactLists(left = [], right = [], defaultScope =
592
858
  }
593
859
 
594
860
  export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = [], projections = {}, extensions = {}) {
595
- const events = (Array.isArray(runtimeEvents) ? runtimeEvents : []).map((event) => ({
596
- id: event?.id || "",
597
- action: event?.action || event?.actionId || "",
598
- stageKey: event?.stageKey || event?.stage || "",
599
- status: event?.status || "",
600
- artifacts: event?.artifacts || [],
601
- globalStatePatch: event?.globalStatePatch || {},
602
- globalStateRemove: event?.globalStateRemove || [],
603
- projections: event?.projections || {},
604
- extensionsPatch: event?.extensionsPatch || {},
605
- }));
861
+ const events = (Array.isArray(runtimeEvents) ? runtimeEvents : []).map((event) => {
862
+ const semantic = { ...plainObject(event) };
863
+ for (const key of ["updatedAt", "createdAt", "actor", "rawOutput", "output", "result"]) delete semantic[key];
864
+ return semantic;
865
+ });
606
866
  const hash = crypto
607
867
  .createHash("sha256")
608
868
  .update(JSON.stringify(stableValue({ globalState, artifacts, projections, extensions, events })))