@fieldwangai/agentflow 0.1.135 → 0.1.137

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,14 +164,39 @@ 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
  }
144
171
  return raw;
145
172
  }
146
173
 
174
+ export function normalizeWorkflowExtensions(value = {}) {
175
+ const raw = plainObject(value);
176
+ const out = {};
177
+ for (const [namespace, extension] of Object.entries(raw)) {
178
+ const key = cleanString(namespace, 120).toLowerCase();
179
+ if (!key || !/^[a-z][a-z0-9._-]{0,119}$/.test(key)) continue;
180
+ if (!extension || typeof extension !== "object" || Array.isArray(extension)) continue;
181
+ out[key] = mergeWorkflowGlobalState({}, extension);
182
+ }
183
+ return out;
184
+ }
185
+
186
+ export function materializeWorkflowExtensions(snapshot = {}, runtimeEvents = []) {
187
+ let extensions = normalizeWorkflowExtensions(
188
+ snapshot.extensions || snapshot.workflowExtensions || snapshot.workflow_extensions || {},
189
+ );
190
+ const events = [...(Array.isArray(runtimeEvents) ? runtimeEvents : [])]
191
+ .sort((left, right) => eventTime(left) - eventTime(right));
192
+ for (const event of events) {
193
+ const patch = event?.extensionsPatch || event?.extensions_patch;
194
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) continue;
195
+ extensions = mergeWorkflowGlobalState(extensions, normalizeWorkflowExtensions(patch));
196
+ }
197
+ return extensions;
198
+ }
199
+
147
200
  export function materializeWorkflowProjections(snapshot = {}, runtimeEvents = []) {
148
201
  const base =
149
202
  snapshot.projections ||
@@ -163,7 +216,6 @@ export function materializeWorkflowProjections(snapshot = {}, runtimeEvents = []
163
216
  ? next.timeline
164
217
  .map((item, index) => normalizeWorkflowTimelineProjection(item, index))
165
218
  .filter(Boolean)
166
- .slice(0, 100)
167
219
  : projections.timeline || [],
168
220
  };
169
221
  }
@@ -204,11 +256,22 @@ export function removeWorkflowGlobalStatePath(value, rawPath) {
204
256
  }
205
257
 
206
258
  export function normalizeWorkflowReference(payload = {}) {
207
- const workflow = plainObject(payload.workflow);
208
- 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;
209
267
  const keySeparator = explicitKey.indexOf(":");
210
268
  const keyedNamespace = keySeparator > 0 ? explicitKey.slice(0, keySeparator) : "";
211
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" };
212
275
  const namespace = cleanString(
213
276
  workflow.namespace || workflow.type || payload.workflowNamespace || payload.workflow_namespace || keyedNamespace || "tapd",
214
277
  80,
@@ -234,29 +297,70 @@ export function normalizeWorkflowReference(payload = {}) {
234
297
  }
235
298
 
236
299
  export function normalizeWorkflowReport(payload = {}) {
237
- 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);
238
302
  if (schemaVersion !== WORKFLOW_REPORT_SCHEMA_VERSION) {
239
303
  return { error: `Unsupported workflow report schemaVersion: ${schemaVersion}` };
240
304
  }
241
305
  const workflow = normalizeWorkflowReference(payload);
242
306
  if (workflow.error) return workflow;
243
307
  const rawWorkflow = plainObject(payload.workflow);
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();
311
+ if (!/^[a-z][a-z0-9._-]{0,119}$/.test(source)) {
312
+ return { error: "Invalid workflow report source" };
313
+ }
314
+
315
+ const rawObservation = plainObject(payload.observation);
316
+ const hasObservation = Object.keys(rawObservation).length > 0;
317
+ const observationState = plainObject(rawObservation.state || rawObservation.facts || rawObservation.value);
318
+ if (hasObservation && !Object.keys(observationState).length) {
319
+ return { error: "observation requires state" };
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" };
326
+ const observation = hasObservation ? {
327
+ schema: cleanString(rawObservation.schema || rawObservation.model || "workflow-observation/v1", 160),
328
+ state: mergeWorkflowGlobalState({}, observationState),
329
+ observedAt: cleanString(rawObservation.observedAt || rawObservation.observed_at, 80),
330
+ clientId: cleanString(rawObservation.clientId || rawObservation.client_id || source, 160),
331
+ scope: cleanString(rawObservation.scope || "client", 80).toLowerCase() || "client",
332
+ } : null;
244
333
 
245
334
  const rawAction = plainObject(payload.action);
246
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
+ }
247
339
  const actionKey = cleanString(rawAction.key || rawAction.id || rawAction.actionKey || rawAction.action_key, 240);
248
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" };
249
353
  const action = hasAction ? {
250
354
  ...rawAction,
251
355
  key: actionKey,
252
356
  title: cleanString(rawAction.title || rawAction.label || actionKey, 500),
253
- 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) } : {}),
254
358
  status: normalizeActionStatus(rawAction.status),
255
- group: cleanString(rawAction.group || rawAction.stage || rawAction.category, 120),
256
- scope: cleanString(rawAction.scope, 80),
257
- platform: cleanString(rawAction.platform, 80),
258
- issueKey: cleanString(rawAction.issueKey || rawAction.issue_key, 240),
259
- 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) } : {}),
260
364
  occurredAt: cleanString(
261
365
  rawAction.occurredAt ||
262
366
  rawAction.occurred_at ||
@@ -267,9 +371,37 @@ export function normalizeWorkflowReport(payload = {}) {
267
371
  } : null;
268
372
 
269
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` };
270
399
  const artifacts = rawArtifacts
271
400
  .filter((item) => item && typeof item === "object" && !Array.isArray(item))
272
- .map((item, index) => normalizeWorkflowArtifact(item, index, action ? "action" : "global"));
401
+ .map((item, index) => ({
402
+ ...normalizeWorkflowArtifact(item, index, action ? "action" : "global"),
403
+ producer: source,
404
+ }));
273
405
 
274
406
  const rawProjections = plainObject(payload.projections || payload.workflowProjections || payload.workflow_projections);
275
407
  const hasProjections = Object.keys(rawProjections).length > 0 || hasOwn(payload, "projections");
@@ -279,25 +411,88 @@ export function normalizeWorkflowReport(payload = {}) {
279
411
  if (hasProjections && !Array.isArray(rawProjections.timeline)) {
280
412
  return { error: "projections.timeline must be an array" };
281
413
  }
414
+ if (hasProjections && rawProjections.timeline.length > 100) {
415
+ return { error: "projections.timeline supports at most 100 entries" };
416
+ }
282
417
  const invalidTimelineIndex = hasProjections
283
418
  ? rawProjections.timeline.findIndex((item, index) => !normalizeWorkflowTimelineProjection(item, index))
284
419
  : -1;
285
420
  if (invalidTimelineIndex >= 0) {
286
421
  return { error: `projections.timeline[${invalidTimelineIndex}] requires kind and id` };
287
422
  }
288
- 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;
455
+
456
+ const rawExtensions = plainObject(payload.extensions);
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
+ }
468
+ const extensions = hasExtensions ? normalizeWorkflowExtensions(rawExtensions) : null;
469
+ if (hasExtensions && !Object.keys(extensions).length) {
470
+ return { error: "extensions requires at least one valid namespace object" };
471
+ }
472
+ if (hasExtensions && Object.keys(extensions).some((namespace) => namespace !== source)) {
473
+ return { error: "extensions may only update the namespace matching report source" };
474
+ }
289
475
 
290
476
  const rawGlobalState = plainObject(payload.globalState || payload.global_state);
291
477
  const hasGlobalState = Object.keys(rawGlobalState).length > 0;
292
478
  const globalStatePatch = plainObject(rawGlobalState.patch);
293
- 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
+ ]);
294
489
  const mode = cleanString(rawGlobalState.mode || "merge", 40).toLowerCase() || "merge";
295
490
  if (hasGlobalState && mode !== "merge") return { error: "globalState.mode must be merge" };
296
491
  if (hasGlobalState && !Object.keys(globalStatePatch).length && !globalStateRemove.length) {
297
492
  return { error: "globalState requires patch or remove" };
298
493
  }
299
- if (!action && !artifacts.length && !hasGlobalState && !hasProjections) {
300
- return { error: "Workflow report requires action, artifacts, globalState, or projections" };
494
+ if (!action && !artifacts.length && !hasGlobalState && !hasProjections && !hasExtensions && !hasObservation) {
495
+ return { error: "Workflow report requires observation, action, artifacts, globalState, projections, or extensions" };
301
496
  }
302
497
 
303
498
  const idempotencyKey = cleanString(
@@ -307,10 +502,24 @@ export function normalizeWorkflowReport(payload = {}) {
307
502
  action?.idempotency_key,
308
503
  500,
309
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
+ }
310
518
  const event = {
311
519
  schemaVersion,
312
520
  type: "workflow-report",
313
- source: cleanString(payload.source || "agentflow-cli", 120) || "agentflow-cli",
521
+ operation: "report",
522
+ source,
314
523
  workflow,
315
524
  workflowKey: workflow.key,
316
525
  aggregateByStage: Boolean(action),
@@ -327,9 +536,9 @@ export function normalizeWorkflowReport(payload = {}) {
327
536
  detail: action.detail,
328
537
  status: action.status,
329
538
  scope: action.scope || action.group || "workflow",
330
- platform: action.platform,
331
- issueKey: action.issueKey,
332
- tags: action.tags,
539
+ ...(action.platform ? { platform: action.platform } : {}),
540
+ ...(action.issueKey ? { issueKey: action.issueKey } : {}),
541
+ ...(action.tags ? { tags: action.tags } : {}),
333
542
  ...(action.occurredAt ? { occurredAt: action.occurredAt } : {}),
334
543
  } : {
335
544
  title: cleanString(payload.title || "Workflow 全局状态更新", 500),
@@ -339,24 +548,43 @@ export function normalizeWorkflowReport(payload = {}) {
339
548
  }),
340
549
  artifacts,
341
550
  ...(hasProjections ? { projections } : {}),
551
+ ...(hasExtensions ? { extensionsPatch: extensions } : {}),
342
552
  ...(hasGlobalState ? {
343
553
  globalStatePatch,
344
554
  globalStateRemove,
555
+ globalStateOwnerPaths,
345
556
  } : {}),
346
557
  ...(idempotencyKey ? { idempotencyKey } : {}),
347
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
+ }
348
572
  return {
349
573
  schemaVersion,
350
574
  workflow,
351
575
  action,
352
576
  artifacts,
353
577
  projections,
578
+ extensions,
579
+ observation,
580
+ hasRuntimeUpdate: Boolean(action || artifacts.length || hasGlobalState || hasProjections || hasExtensions),
354
581
  globalState: hasGlobalState ? {
355
582
  mode: "merge",
356
583
  patch: globalStatePatch,
357
584
  remove: globalStateRemove,
358
585
  } : null,
359
586
  expectedRevision: cleanString(payload.expectedRevision || payload.expected_revision, 500),
587
+ expectedVersions,
360
588
  idempotencyKey,
361
589
  flowId: cleanString(payload.flowId || payload.flow_id || rawWorkflow.flowId || rawWorkflow.flow_id, 240),
362
590
  flowSource: cleanString(payload.flowSource || payload.flow_source || rawWorkflow.flowSource || rawWorkflow.flow_source || "user", 80) || "user",
@@ -364,6 +592,102 @@ export function normalizeWorkflowReport(payload = {}) {
364
592
  };
365
593
  }
366
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
+
367
691
  function displayValue(value) {
368
692
  if (value == null) return "";
369
693
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
@@ -491,20 +815,22 @@ export function mergeWorkflowArtifactLists(left = [], right = [], defaultScope =
491
815
  };
492
816
  const artifactAliases = (artifact) => {
493
817
  const aliases = [];
818
+ const producer = cleanString(artifact?.producer || artifact?.reportSource || artifact?.report_source, 120).toLowerCase();
819
+ const ownedAlias = (alias) => producer ? `producer:${producer}:${alias}` : alias;
494
820
  const explicitKey = cleanString(
495
821
  artifact?.key || artifact?.artifactKey || artifact?.artifact_key,
496
822
  500,
497
823
  );
498
- if (explicitKey) aliases.push(`key:${explicitKey}`);
824
+ if (explicitKey) aliases.push(ownedAlias(`key:${explicitKey}`));
499
825
  const url = normalizedUrl(
500
826
  artifact?.canonicalUrl
501
827
  || artifact?.canonical_url
502
828
  || artifact?.href
503
829
  || artifact?.url,
504
830
  );
505
- if (url) aliases.push(`url:${url}`);
831
+ if (url) aliases.push(ownedAlias(`url:${url}`));
506
832
  const artifactPath = cleanString(artifact?.path, 4000);
507
- if (artifactPath) aliases.push(`path:${artifactPath}`);
833
+ if (artifactPath) aliases.push(ownedAlias(`path:${artifactPath}`));
508
834
  return aliases;
509
835
  };
510
836
  const add = (artifact) => {
@@ -531,20 +857,15 @@ export function mergeWorkflowArtifactLists(left = [], right = [], defaultScope =
531
857
  return out;
532
858
  }
533
859
 
534
- export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = [], projections = {}) {
535
- const events = (Array.isArray(runtimeEvents) ? runtimeEvents : []).map((event) => ({
536
- id: event?.id || "",
537
- action: event?.action || event?.actionId || "",
538
- stageKey: event?.stageKey || event?.stage || "",
539
- status: event?.status || "",
540
- artifacts: event?.artifacts || [],
541
- globalStatePatch: event?.globalStatePatch || {},
542
- globalStateRemove: event?.globalStateRemove || [],
543
- projections: event?.projections || {},
544
- }));
860
+ export function workflowRuntimeRevision(globalState = {}, artifacts = [], runtimeEvents = [], projections = {}, extensions = {}) {
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
+ });
545
866
  const hash = crypto
546
867
  .createHash("sha256")
547
- .update(JSON.stringify(stableValue({ globalState, artifacts, projections, events })))
868
+ .update(JSON.stringify(stableValue({ globalState, artifacts, projections, extensions, events })))
548
869
  .digest("hex")
549
870
  .slice(0, 24);
550
871
  return `runtime:${hash}`;