@oomol-lab/open-flow 0.1.0-beta.77 → 0.1.0-beta.78

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 (58) hide show
  1. package/README.md +2 -0
  2. package/dist/browser/{ContentIcon-VZQO8VZU.js → ContentIcon-CMn0c-kl.js} +93 -3
  3. package/dist/browser/{FlowCanvas-BNGDMaWr.js → FlowCanvas-BQGBcfoL.js} +1675 -591
  4. package/dist/browser/{createResourceDialog-BmtakfVg.js → createResourceDialog-DCGxfKFM.js} +3 -3
  5. package/dist/browser/{dialog-BcFBfRB1.js → dialog-WzR4CRFH.js} +11 -81
  6. package/dist/browser/{exportSvg-CG8hEUNk.js → exportSvg-CYwyYiuH.js} +2 -2
  7. package/dist/browser/{flowChanges-BGo_4Ypo.js → flowChanges-Bz4dbDN_.js} +1204 -387
  8. package/dist/browser/{flowWorkspace-BFHGev7s.js → flowWorkspace-BI6L6a4r.js} +557 -597
  9. package/dist/browser/licenses.md +1 -1
  10. package/dist/browser/preview.css +512 -462
  11. package/dist/browser/preview.js +8457 -6732
  12. package/dist/browser/{tooltip-BAWSUexx.js → tooltip-BDcVTNpu.js} +65 -2
  13. package/dist/browser/ui.css +5 -24
  14. package/dist/browser/workbench.css +512 -462
  15. package/dist/browser/workbench.d.ts +2 -3
  16. package/dist/browser/workbench.js +65 -690
  17. package/dist/browser/{workbenchSelect-CjK-xdm3.js → workbenchSelect-DtAd0zc4.js} +2 -2
  18. package/dist/command/index.js +25 -25
  19. package/dist/common/authoring.d.ts +3 -3
  20. package/dist/common/authoringExamples.d.ts +3 -3
  21. package/dist/common/authoringSchema.d.ts +14 -22
  22. package/dist/common/control-api-conformance.js +11 -18
  23. package/dist/common/control-api.d.ts +5 -1
  24. package/dist/common/control-api.js +11 -18
  25. package/dist/common/control-requests.d.ts +4 -3
  26. package/dist/common/control-requests.js +26 -37
  27. package/dist/common/flow-authoring.js +6 -14
  28. package/dist/common/flow-change.d.ts +1 -1
  29. package/dist/common/flow-change.js +6 -14
  30. package/dist/common/flow-encoding.js +5 -13
  31. package/dist/common/flow-semantics.js +15 -21
  32. package/dist/common/flow-state.d.ts +16 -39
  33. package/dist/common/flow-state.js +47 -105
  34. package/dist/common/mcp.d.ts +3 -3
  35. package/dist/common/mcp.js +11 -18
  36. package/dist/common/run-events.js +19 -9
  37. package/dist/common/runtime-contract.js +6 -12
  38. package/dist/common/runtime-nodejs-compat.js +11 -12
  39. package/dist/common/scheduler.d.ts +2 -1
  40. package/dist/common/scheduler.js +56 -50
  41. package/dist/common/state-conformance.d.ts +3 -3
  42. package/package.json +1 -1
  43. package/src/control/common/api.ts +20 -3
  44. package/src/control/common/authoring.ts +6 -5
  45. package/src/control/common/authoringExamples.ts +6 -5
  46. package/src/control/common/authoringSchema.ts +7 -7
  47. package/src/control/common/requests.ts +7 -1
  48. package/src/execution/common/events.ts +22 -10
  49. package/src/execution/common/node-execution.ts +1 -1
  50. package/src/execution/common/scheduler.ts +28 -14
  51. package/src/flow/common/change.ts +1 -1
  52. package/src/flow/common/changeSchema.ts +4 -4
  53. package/src/flow/common/errorHandling.ts +1 -1
  54. package/src/flow/common/semantics.ts +12 -6
  55. package/src/runtime/common/nodejsGuest.generated.json +11 -12
  56. package/src/runtime/common/runtime.ts +6 -7
  57. package/src/state/common/conformance.ts +16 -21
  58. package/src/state/common/state.ts +44 -72
@@ -33,7 +33,7 @@ export type SchedulerEvent = {
33
33
  readonly inputs: Readonly<Record<string, JsonValue>>;
34
34
  readonly jobId: string;
35
35
  readonly nodeId: string;
36
- readonly nodeKind: 'agent' | 'approval' | 'condition' | 'connector' | 'decision' | 'javascript' | 'openapi' | 'http-request' | 'dedupe' | 'llm' | 'value' | 'wait' | 'outputs' | 'workflow' | 'webhook-response';
36
+ readonly nodeKind: 'agent' | 'approval' | 'condition' | 'connector' | 'decision' | 'javascript' | 'openapi' | 'http-request' | 'skip-duplicates' | 'llm' | 'value' | 'wait' | 'outputs' | 'workflow' | 'webhook-response';
37
37
  readonly nodeTitle?: string;
38
38
  readonly runId: string;
39
39
  readonly type: 'node.started';
@@ -167,6 +167,7 @@ export type RunLaunch = {
167
167
  } | {
168
168
  readonly resume: {
169
169
  readonly checkpoint: unknown;
170
+ readonly readyWorkflowRunIds?: readonly string[];
170
171
  };
171
172
  readonly bindingValues?: never;
172
173
  readonly inputs?: never;
@@ -14,37 +14,6 @@ var nanoid$1 = (size = 21) => {
14
14
  return id;
15
15
  };
16
16
  //#endregion
17
- //#region src/flow/common/inputValue.ts
18
- /** An explicit clear suppresses the default; absent mappings still inherit it. */
19
- function inputValue(mapping, fallback) {
20
- if (mapping?.kind === "unset") return void 0;
21
- return mapping?.kind === "value" ? mapping.value : fallback;
22
- }
23
- //#endregion
24
- //#region ../../node_modules/.bun/dequal@2.0.3/node_modules/dequal/lite/index.mjs
25
- var has = Object.prototype.hasOwnProperty;
26
- function dequal(foo, bar) {
27
- var ctor, len;
28
- if (foo === bar) return true;
29
- if (foo && bar && (ctor = foo.constructor) === bar.constructor) {
30
- if (ctor === Date) return foo.getTime() === bar.getTime();
31
- if (ctor === RegExp) return foo.toString() === bar.toString();
32
- if (ctor === Array) {
33
- if ((len = foo.length) === bar.length) while (len-- && dequal(foo[len], bar[len]));
34
- return len === -1;
35
- }
36
- if (!ctor || typeof foo === "object") {
37
- len = 0;
38
- for (ctor in foo) {
39
- if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false;
40
- if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false;
41
- }
42
- return Object.keys(bar).length === len;
43
- }
44
- }
45
- return foo !== foo && bar !== bar;
46
- }
47
- //#endregion
48
17
  //#region ../../node_modules/.bun/zod@4.6.5/node_modules/zod/v4/core/util.js
49
18
  function getEnumValues(entries) {
50
19
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
@@ -5719,6 +5688,37 @@ function datetime(params) {
5719
5688
  return /* @__PURE__ */ _isoDateTime(ZodISODateTime, params);
5720
5689
  }
5721
5690
  //#endregion
5691
+ //#region src/flow/common/inputValue.ts
5692
+ /** An explicit clear suppresses the default; absent mappings still inherit it. */
5693
+ function inputValue(mapping, fallback) {
5694
+ if (mapping?.kind === "unset") return void 0;
5695
+ return mapping?.kind === "value" ? mapping.value : fallback;
5696
+ }
5697
+ //#endregion
5698
+ //#region ../../node_modules/.bun/dequal@2.0.3/node_modules/dequal/lite/index.mjs
5699
+ var has = Object.prototype.hasOwnProperty;
5700
+ function dequal(foo, bar) {
5701
+ var ctor, len;
5702
+ if (foo === bar) return true;
5703
+ if (foo && bar && (ctor = foo.constructor) === bar.constructor) {
5704
+ if (ctor === Date) return foo.getTime() === bar.getTime();
5705
+ if (ctor === RegExp) return foo.toString() === bar.toString();
5706
+ if (ctor === Array) {
5707
+ if ((len = foo.length) === bar.length) while (len-- && dequal(foo[len], bar[len]));
5708
+ return len === -1;
5709
+ }
5710
+ if (!ctor || typeof foo === "object") {
5711
+ len = 0;
5712
+ for (ctor in foo) {
5713
+ if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false;
5714
+ if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false;
5715
+ }
5716
+ return Object.keys(bar).length === len;
5717
+ }
5718
+ }
5719
+ return foo !== foo && bar !== bar;
5720
+ }
5721
+ //#endregion
5722
5722
  //#region ../../node_modules/.bun/@cfworker+json-schema@4.1.1/node_modules/@cfworker/json-schema/dist/esm/deep-compare-strict.js
5723
5723
  function deepCompareStrict(a, b) {
5724
5724
  const typeofa = typeof a;
@@ -9547,11 +9547,7 @@ strictObject({
9547
9547
  scope: _enum(["draft", "live"]),
9548
9548
  space: string$1().regex(/^(flow|node:.+)$/)
9549
9549
  });
9550
- var dedupeExecutorSchema = strictObject({
9551
- field: string$1().default(""),
9552
- mode: _enum(["new", "increasing"]).default("new"),
9553
- space: _enum(["node", "flow"]).default("node")
9554
- }).extend({ kind: literal("dedupe") });
9550
+ var skipDuplicatesExecutorSchema = strictObject({ fields: array(string$1().refine((field) => field.length > 0 && field.split(".").every((part) => part.length > 0), "Use a nonempty dot-separated field path.")).refine((fields) => new Set(fields).size === fields.length, "Identity fields must be distinct.").default([]) }).extend({ kind: literal("skip-duplicates") });
9555
9551
  object({
9556
9552
  version: literal(1),
9557
9553
  scope: _enum(["draft", "live"]),
@@ -9570,13 +9566,9 @@ object({
9570
9566
  })),
9571
9567
  spaces: array(object({
9572
9568
  space: string$1(),
9573
- mode: _enum(["new", "increasing"]),
9574
9569
  count: number().nonnegative(),
9575
- watermark: union([
9576
- string$1(),
9577
- number(),
9578
- _null()
9579
- ]),
9570
+ pendingCount: number().nonnegative(),
9571
+ reservedCount: number().nonnegative(),
9580
9572
  updatedAt: number()
9581
9573
  }))
9582
9574
  });
@@ -9935,7 +9927,7 @@ var managedFields = object({
9935
9927
  revisionId: text
9936
9928
  }),
9937
9929
  httpRequestExecutorSchema,
9938
- dedupeExecutorSchema
9930
+ skipDuplicatesExecutorSchema
9939
9931
  ])
9940
9932
  });
9941
9933
  var managed = managedFields.transform(limitDecisionTask);
@@ -10598,7 +10590,7 @@ var nodeErrorSchema = {
10598
10590
  }
10599
10591
  };
10600
10592
  function supportsNodeErrorHandling(node) {
10601
- return supportsNodeRetry(node) || node.kind === "task" && "executor" in node.task && node.task.executor.kind === "dedupe";
10593
+ return supportsNodeRetry(node) || node.kind === "task" && "executor" in node.task && node.task.executor.kind === "skip-duplicates";
10602
10594
  }
10603
10595
  function nodeSystemOutputPorts(node) {
10604
10596
  return supportsNodeErrorHandling(node) && node.onError == "error" ? { error: {
@@ -11997,6 +11989,8 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
11997
11989
  const changes = yield* Queue.unbounded();
11998
11990
  let remainingMs = context.remainingMs;
11999
11991
  let measuredAt = yield* Clock.currentTimeMillis;
11992
+ let nextCallCheckAt = measuredAt + (resume == null ? 1e3 : 0);
11993
+ let readyWorkflowRunIds = resume?.readyWorkflowRunIds;
12000
11994
  let idleSince;
12001
11995
  const agents = { ...resume?.checkpoint.agents };
12002
11996
  const resolveInputs = (nodeId, node) => resolveNodeInputs(nodeId, node, dependencies, context.bindingValues, launch[nodeId]);
@@ -12214,10 +12208,17 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
12214
12208
  return yield* Effect.failCause(firstCause);
12215
12209
  }
12216
12210
  if (pendingWaits.size > 0 && context.waits != null) applyResolutions(yield* context.waits.resolutions([...pendingWaits.values()].map((wait) => wait.waitId), false), now);
12217
- for (const call of pendingCalls.values()) yield* observe(call.nodeId, call.jobId, Effect.gen(function* () {
12218
- const result = yield* execute(call.nodeId, target.graph.nodes[call.nodeId], call.jobId, call.input);
12219
- yield* settleNode(call.nodeId, call.jobId, result);
12220
- })).pipe(Effect.catch(() => Effect.void));
12211
+ if (pendingCalls.size > 0 && now >= nextCallCheckAt) {
12212
+ for (const call of pendingCalls.values()) {
12213
+ if (readyWorkflowRunIds != null && !readyWorkflowRunIds.has(call.childRunId)) continue;
12214
+ yield* observe(call.nodeId, call.jobId, Effect.gen(function* () {
12215
+ const result = yield* execute(call.nodeId, target.graph.nodes[call.nodeId], call.jobId, call.input);
12216
+ yield* settleNode(call.nodeId, call.jobId, result);
12217
+ })).pipe(Effect.catch(() => Effect.void));
12218
+ }
12219
+ readyWorkflowRunIds = void 0;
12220
+ nextCallCheckAt = (yield* Clock.currentTimeMillis) + 1e3;
12221
+ }
12221
12222
  const ready = dependencies.ready();
12222
12223
  for (const nodeId of ready) executeReady(nodeId);
12223
12224
  const running = yield* FiberSet.size(active);
@@ -12252,7 +12253,7 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
12252
12253
  const activity = Queue.take(changes).pipe(Effect.as({}));
12253
12254
  const decision = pendingWaits.size == 0 || context.waits == null ? Effect.never : context.waits.resolutions([...pendingWaits.values()].map((wait) => wait.waitId), true);
12254
12255
  let next = Effect.raceFirst(activity, decision);
12255
- if (pendingCalls.size > 0) next = Effect.raceFirst(next, Effect.sleep(1e3).pipe(Effect.as({})));
12256
+ if (pendingCalls.size > 0) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, nextCallCheckAt - (yield* Clock.currentTimeMillis))).pipe(Effect.as({})));
12256
12257
  if (left != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, left)).pipe(Effect.as({})));
12257
12258
  applyResolutions(yield* next, yield* Clock.currentTimeMillis);
12258
12259
  }
@@ -12290,6 +12291,8 @@ function runFlow(prepared, options) {
12290
12291
  return Effect.gen(function* () {
12291
12292
  if (options.resume != null && (options.inputs != null || options.trigger != null || options.bindingValues != null)) return yield* Effect.fail(/* @__PURE__ */ new Error("A resumed Flow Run cannot accept launch inputs, binding values, or a Trigger seed."));
12292
12293
  const decoded = options.resume == null ? void 0 : decodeFlowRunCheckpoint(options.resume.checkpoint);
12294
+ const readyWorkflowRunIds = options.resume?.readyWorkflowRunIds === void 0 ? void 0 : new Set(array(string$1().min(1)).parse(options.resume.readyWorkflowRunIds));
12295
+ if (readyWorkflowRunIds != null && [...readyWorkflowRunIds].some((id) => !decoded?.calls?.some((call) => call.childRunId == id))) return yield* Effect.fail(/* @__PURE__ */ new Error("Ready workflow Run is not present in the checkpoint."));
12293
12296
  const outputs = (nodeId, value) => isResolutionNode(prepared.graph.nodes[nodeId]) && value.pending != null ? {
12294
12297
  ...value,
12295
12298
  pending: withWaitNotificationMessage(value.pending)
@@ -12340,7 +12343,10 @@ function runFlow(prepared, options) {
12340
12343
  inputDefs: prepared.inputDefs,
12341
12344
  outputDefs: prepared.outputDefs,
12342
12345
  outputMappings: prepared.outputMappings
12343
- }, options.runId, options.inputs, trigger, checkpoint == null ? void 0 : { checkpoint });
12346
+ }, options.runId, options.inputs, trigger, checkpoint == null ? void 0 : {
12347
+ checkpoint,
12348
+ readyWorkflowRunIds
12349
+ });
12344
12350
  });
12345
12351
  }
12346
12352
  //#endregion
@@ -1,11 +1,11 @@
1
1
  import type { JsonValue } from './flow-model.js';
2
- import type { DedupeConfig } from './flow-state.js';
2
+ import type { SkipDuplicatesConfig } from './flow-state.js';
3
3
  export interface StateConformanceHarness {
4
4
  readonly maxIdentities: number;
5
5
  /** Executes Code on the same initially empty Flow and scope on every call; returns its `result` output. */
6
6
  run(source: string): Promise<JsonValue>;
7
- /** Executes the same dedupe node across Runs, updating its business config and items. */
8
- dedupe(config: DedupeConfig, items: JsonValue[]): Promise<{
7
+ /** Executes the same Skip Duplicates node across Runs, updating its business config and items. */
8
+ skipDuplicates(config: SkipDuplicatesConfig, items: JsonValue[]): Promise<{
9
9
  items: JsonValue[];
10
10
  duplicates: JsonValue[];
11
11
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oomol-lab/open-flow",
3
- "version": "0.1.0-beta.77",
3
+ "version": "0.1.0-beta.78",
4
4
  "description": "TypeScript contracts and Browser runtime for Open Flow.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -149,6 +149,11 @@ export interface PublicationPresentation {
149
149
  readonly presentation: Presentation | null
150
150
  }
151
151
 
152
+ export interface FlowCreationOptions {
153
+ readonly tagIds?: readonly string[]
154
+ readonly presentation?: Readonly<Record<string, JsonValue>>
155
+ }
156
+
152
157
  export interface Presentation {
153
158
  readonly revision: number
154
159
  readonly updatedAt: string
@@ -703,7 +708,7 @@ export function decodeRunEvent(value: unknown) {
703
708
  nodeKind !== 'decision' &&
704
709
  nodeKind !== 'openapi' &&
705
710
  nodeKind !== 'http-request' &&
706
- nodeKind !== 'dedupe' &&
711
+ nodeKind !== 'skip-duplicates' &&
707
712
  nodeKind !== 'llm' &&
708
713
  nodeKind !== 'agent' &&
709
714
  nodeKind !== 'value' &&
@@ -941,10 +946,22 @@ export class ControlClient {
941
946
  )
942
947
  }
943
948
 
944
- async createFlow(name: string, idempotencyKey = `flow-${randomId()}`, teamId?: string, tagIds?: readonly string[]): Promise<Flow> {
949
+ async createFlow(
950
+ name: string,
951
+ idempotencyKey = `flow-${randomId()}`,
952
+ teamId?: string,
953
+ tagIds?: readonly string[],
954
+ initialPresentation?: Readonly<Record<string, JsonValue>>,
955
+ ): Promise<Flow> {
945
956
  return flow(
946
957
  await this.request('/v1/flows', {
947
- body: JSON.stringify({ name, ...(teamId == null ? {} : { teamId }), ...(tagIds == null ? {} : { tagIds }), version: 1 }),
958
+ body: JSON.stringify({
959
+ name,
960
+ ...(teamId == null ? {} : { teamId }),
961
+ ...(tagIds == null ? {} : { tagIds }),
962
+ ...(initialPresentation == null ? {} : { presentation: initialPresentation }),
963
+ version: 1,
964
+ }),
948
965
  headers: { 'idempotency-key': idempotencyKey },
949
966
  method: 'POST',
950
967
  }),
@@ -43,7 +43,7 @@ import {
43
43
  } from '../../flow/common/nodeChanges.ts'
44
44
  import { retryConfigurationSchema } from '../../flow/common/retry.ts'
45
45
  import { httpRequestTask, httpRequestConfigSchema, httpRequestInputs } from '../../http/common/httpRequest.ts'
46
- import { dedupeConfigSchema, dedupeTask } from '../../state/common/state.ts'
46
+ import { skipDuplicatesConfigSchema, skipDuplicatesTask } from '../../state/common/state.ts'
47
47
  import { withWorkflowTask, workflowTask, workflowSelectionTask, workflowCapability, workflowAgentTool } from '../../workflow/common/workflow.ts'
48
48
  import { conditionConfigurationView, prepareConditionConfiguration } from './authoringCondition.ts'
49
49
  import { authoringDiagnosticField } from './authoringDiagnosticFields.ts'
@@ -147,7 +147,7 @@ export function authoringNode(content: RevisionContent, ref: string) {
147
147
  ),
148
148
  }
149
149
  else if (task.executor.kind == 'workflow') config = { flowId: task.executor.flowId }
150
- else if (task.executor.kind == 'http-request' || task.executor.kind == 'dedupe') {
150
+ else if (task.executor.kind == 'http-request' || task.executor.kind == 'skip-duplicates') {
151
151
  const { kind: _kind, ...settings } = task.executor
152
152
  config = settings
153
153
  } else if (task.executor.kind == 'openapi') {
@@ -522,7 +522,8 @@ export async function compileAuthoring(base: RevisionContent, request: Authoring
522
522
  task = { ...before, name: String(update.name), executor: { ...executor, ...config } } as ManagedTaskDefinition
523
523
  } else task = await connectorTask(config, String(update.name))
524
524
  } else if (type == 'decision') task = decisionTask(config.questions as Parameters<typeof decisionTask>[0], String(update.name))
525
- else if (type == 'dedupe') task = { ...before, name: String(update.name), executor: { kind: 'dedupe', ...dedupeConfigSchema.parse(config) } }
525
+ else if (type == 'skip-duplicates')
526
+ task = { ...before, name: String(update.name), executor: { kind: 'skip-duplicates', ...skipDuplicatesConfigSchema.parse(config) } }
526
527
  else if (type == 'http-request') {
527
528
  task = httpRequestTask(httpRequestConfigSchema.parse(config), String(update.name), before)
528
529
  next = { ...next, inputs: httpRequestInputs(node.inputs, before, task) } as GraphNode
@@ -776,8 +777,8 @@ export async function compileAuthoring(base: RevisionContent, request: Authoring
776
777
  case 'connector':
777
778
  changes = createManagedTask(identity, await connectorTask(config, name))
778
779
  break
779
- case 'dedupe':
780
- changes = createManagedTask(identity, dedupeTask(dedupeConfigSchema.parse(config), name))
780
+ case 'skip-duplicates':
781
+ changes = createManagedTask(identity, skipDuplicatesTask(skipDuplicatesConfigSchema.parse(config), name))
781
782
  break
782
783
  case 'http-request':
783
784
  changes = createManagedTask(identity, httpRequestTask(httpRequestConfigSchema.parse(config), name))
@@ -13,7 +13,7 @@ import {
13
13
  } from '../../flow/common/nodeChanges.ts'
14
14
  import { httpRequestTask } from '../../http/common/httpRequest.ts'
15
15
  import { openApiTask, selectOperation } from '../../openapi/common/openapi.ts'
16
- import { dedupeTask } from '../../state/common/state.ts'
16
+ import { skipDuplicatesTask } from '../../state/common/state.ts'
17
17
 
18
18
  const descriptions = {
19
19
  'start': 'A Start node with a required message parameter. Supply it through trigger.outputs.',
@@ -24,7 +24,8 @@ const descriptions = {
24
24
  'poll': 'Gmail polling by key; replace CONNECTION_ID with an active Gmail connection.',
25
25
  'integration': 'Telegram webhook integration by key; replace CONNECTION_ID with an active Telegram connection.',
26
26
  'decision': 'AI Decision evaluates named questions against target and returns a complete answer object per output. Configure target before running.',
27
- 'dedupe': 'Filter items using persistent history. Records immediately, including when downstream work fails. Empty field uses each scalar item as identity.',
27
+ 'skip-duplicates':
28
+ 'Skip items already recorded by successful Runs. Configure identity fields; an empty list uses each string or numeric item. History is private to the node and only commits after Run success.',
28
29
  'http-request': 'A single HTTP request. Bind url and request inputs; outputs are body, statusCode and headers.',
29
30
  'openapi': 'A fixed JSON API operation. Replace the example document and URL before running.',
30
31
  'connector':
@@ -59,10 +60,10 @@ export function authoringExample(name: string): { version: 1; operations: readon
59
60
  { name: 'needs_support', type: 'noul', instructions: 'Does the customer need support?' },
60
61
  ])
61
62
  break
62
- case 'dedupe':
63
+ case 'skip-duplicates':
63
64
  operations = [
64
- ...createManagedTask({ nodeId: 'dedupe' }, dedupeTask()),
65
- { kind: 'graph.node.input.set', nodeId: 'dedupe', handle: 'items', value: { kind: 'value', value: ['a', 'b', 'a'] } },
65
+ ...createManagedTask({ nodeId: 'skip-duplicates' }, skipDuplicatesTask()),
66
+ { kind: 'graph.node.input.set', nodeId: 'skip-duplicates', handle: 'items', value: { kind: 'value', value: ['a', 'b', 'a'] } },
66
67
  ]
67
68
  break
68
69
  case 'http-request':
@@ -12,7 +12,7 @@ import {
12
12
  import { retryConfigurationSchema, retryNodeTypes } from '../../flow/common/retry.ts'
13
13
  import { httpRequestConfigSchema } from '../../http/common/httpRequest.ts'
14
14
  import { currentEngineContract, findEngineContract } from '../../runtime/common/engineContract.ts'
15
- import { dedupeConfigSchema } from '../../state/common/state.ts'
15
+ import { skipDuplicatesConfigSchema } from '../../state/common/state.ts'
16
16
  import { authoringLlmConfiguration } from './authoringLlm.ts'
17
17
  import { authoringOpenApiSchema } from './authoringOpenApi.ts'
18
18
 
@@ -36,7 +36,7 @@ export const authoringTypes = [
36
36
  'decision',
37
37
  'openapi',
38
38
  'http-request',
39
- 'dedupe',
39
+ 'skip-duplicates',
40
40
  'condition',
41
41
  'value',
42
42
  'wait',
@@ -120,7 +120,7 @@ export const authoringConfigurations = {
120
120
  'connector': definitions.connector.omit({ outputMode: true }).strict(),
121
121
  'openapi': authoringOpenApiSchema,
122
122
  'http-request': httpRequestConfigSchema,
123
- 'dedupe': dedupeConfigSchema,
123
+ 'skip-duplicates': skipDuplicatesConfigSchema,
124
124
  'agent': definitions.agent
125
125
  .omit({ prompt: true, tools: true })
126
126
  .extend({
@@ -286,7 +286,7 @@ export function authoringSchema(kind?: AuthoringType) {
286
286
  }
287
287
  : {
288
288
  type: kind,
289
- onError: kind == 'dedupe' || retryNodeTypes.some((nodeType) => nodeType == kind) ? { enum: ['stop', 'error'] } : false,
289
+ onError: kind == 'skip-duplicates' || retryNodeTypes.some((nodeType) => nodeType == kind) ? { enum: ['stop', 'error'] } : false,
290
290
  retry: retryNodeTypes.some((nodeType) => nodeType == kind) ? z.toJSONSchema(retryConfigurationSchema) : false,
291
291
  inputSources: ['poll', 'integration'].includes(kind)
292
292
  ? ['value', 'unset', 'default']
@@ -309,8 +309,8 @@ export function authoringSchema(kind?: AuthoringType) {
309
309
  ? 'Configure static parameters with node.add.inputs or input.set. Definitions come from the selected Trigger key; execution output and variable sources are unavailable.'
310
310
  : kind == 'llm'
311
311
  ? 'Configure model settings, message templates and history as defaults. Input bindings with the same names override these defaults; named business inputs can be bound directly. Template strings interpolate {{name}}.'
312
- : kind == 'dedupe'
313
- ? 'Bind an items array. Configure field (dot-separated identity path, empty for scalar items), mode (new or increasing), and space (node or flow). Outputs are items and duplicates. History persists immediately; downstream failures do not roll it back. Increasing mode accepts finite numbers or RFC 3339 timestamps, never mixed types.'
312
+ : kind == 'skip-duplicates'
313
+ ? 'Bind an items array. Configure fields as dot-separated identity paths; an empty list uses each scalar item. Outputs are items and duplicates. History belongs to this node and commits only when the Run completes successfully. Fields accept strings or finite numbers; missing or complex values reject the batch.'
314
314
  : kind == 'http-request'
315
315
  ? 'Configure method, query/headers modes and names, bodyFormat, authentication, responseFormat and statusPolicy. Bind url, query.<name>/header.<name> or whole query/headers objects, body and auth.token (Basic: auth.username/auth.password). Credentials accept variables or upstream outputs only. Outputs: body, statusCode, headers. Authentication is replaced as a complete choice.'
316
316
  : kind == 'openapi'
@@ -335,7 +335,7 @@ export function authoringSchema(kind?: AuthoringType) {
335
335
  'connector': { config: { action: '<Action ID>', connectionId: '<Connection ID>' } },
336
336
  'poll': { config: { key: '<Trigger key>', schedule: [{ type: 'every', unit: 'minute', value: 5 }] } },
337
337
  'integration': { config: { key: '<Trigger key>' } },
338
- 'dedupe': { config: { field: 'id', mode: 'new', space: 'node' }, inputs: { items: { kind: 'value', value: [{ id: 'a' }, { id: 'b' }] } } },
338
+ 'skip-duplicates': { config: { fields: ['id'] }, inputs: { items: { kind: 'value', value: [{ id: 'a' }, { id: 'b' }] } } },
339
339
  'http-request': { config: {}, inputs: { url: { kind: 'value', value: 'https://example.com' } } },
340
340
  'openapi': { config: { sourceUrl: 'https://example.com/openapi.json', path: '/items', method: 'get' } },
341
341
  'code': { code: 'export default ({value}) => ({result:value})', inputs: { value: { kind: 'value', value: 'Hello' } } },
@@ -34,7 +34,13 @@ const schemas = {
34
34
  updateEventSource: updateEventSourceSchema,
35
35
  eventSourceRevision: eventSourceRevisionSchema,
36
36
  importFlow: z.strictObject({ file: json, name: flowName.optional(), teamId: id.optional(), tagIds: tagIds.optional(), version }),
37
- createFlow: z.strictObject({ name: flowName, teamId: id.optional(), tagIds: tagIds.optional(), version }),
37
+ createFlow: z.strictObject({
38
+ name: flowName,
39
+ teamId: id.optional(),
40
+ tagIds: tagIds.optional(),
41
+ presentation: z.record(z.string(), json).optional(),
42
+ version,
43
+ }),
38
44
  saveTag: z.strictObject({ name: tagName, version }),
39
45
  setFlowTags: z.strictObject({ tagIds, version }),
40
46
  updateFlow: z.union([z.strictObject({ name: flowName, tagIds: tagIds.optional(), version }), z.strictObject({ name: flowName.optional(), tagIds, version })]),
@@ -23,7 +23,7 @@ const nodeKinds = new Set([
23
23
  'javascript',
24
24
  'openapi',
25
25
  'http-request',
26
- 'dedupe',
26
+ 'skip-duplicates',
27
27
  'llm',
28
28
  'value',
29
29
  'wait',
@@ -57,7 +57,7 @@ function nodeKind(
57
57
  | 'javascript'
58
58
  | 'openapi'
59
59
  | 'http-request'
60
- | 'dedupe'
60
+ | 'skip-duplicates'
61
61
  | 'llm'
62
62
  | 'value'
63
63
  | 'wait'
@@ -78,7 +78,7 @@ function nodeKind(
78
78
  | 'javascript'
79
79
  | 'openapi'
80
80
  | 'http-request'
81
- | 'dedupe'
81
+ | 'skip-duplicates'
82
82
  | 'llm'
83
83
  | 'value'
84
84
  | 'wait'
@@ -95,6 +95,7 @@ function nodeContext(
95
95
  event: Record<string, unknown>,
96
96
  flows: ReadonlyMap<string, string>,
97
97
  platformRunId: string,
98
+ identify: typeof opaqueId,
98
99
  ): {
99
100
  readonly executionId: Promise<string>
100
101
  readonly flowId: string
@@ -105,7 +106,7 @@ function nodeContext(
105
106
  const flowId = flows.get(rawRunId)
106
107
  if (flowId == null) throw new TypeError('Runtime node event preceded its run.started event.')
107
108
  return {
108
- executionId: projectExecutionId(platformRunId, rawRunId, string(event.jobId, 'Runtime node event jobId')),
109
+ executionId: identify('execution', platformRunId, rawRunId, string(event.jobId, 'Runtime node event jobId')),
109
110
  flowId,
110
111
  nodeId: string(event.nodeId, 'Runtime node event nodeId'),
111
112
  rawRunId,
@@ -184,6 +185,17 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
184
185
  const jobs = new Map<string, string>()
185
186
  const nodeProgressBuckets = new Map<string, number>()
186
187
  const runProgressBuckets = new Map<string, number>()
188
+ // Every event of a scope or execution repeats its identity, so each digest is computed once per projector.
189
+ const identities = new Map<string, Promise<string>>()
190
+ const identify: typeof opaqueId = (prefix, ...parts) => {
191
+ const key = JSON.stringify([prefix, ...parts])
192
+ let identity = identities.get(key)
193
+ if (identity == null) {
194
+ identity = opaqueId(prefix, ...parts)
195
+ identities.set(key, identity)
196
+ }
197
+ return identity
198
+ }
187
199
  return async (value) => {
188
200
  const event = object(value, 'Runtime event')
189
201
  const type = string(event.type, 'Runtime event type')
@@ -205,8 +217,8 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
205
217
  kind: 'run.started',
206
218
  payload: {
207
219
  flowId,
208
- ...(parentRunId === undefined ? {} : { parentScopeId: await opaqueId('scope', platformRunId, parentRunId) }),
209
- scopeId: await opaqueId('scope', platformRunId, rawRunId),
220
+ ...(parentRunId === undefined ? {} : { parentScopeId: await identify('scope', platformRunId, parentRunId) }),
221
+ scopeId: await identify('scope', platformRunId, rawRunId),
210
222
  },
211
223
  }
212
224
  }
@@ -226,7 +238,7 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
226
238
  payload: {
227
239
  flowId,
228
240
  progress: event.progress,
229
- scopeId: await opaqueId('scope', platformRunId, rawRunId),
241
+ scopeId: await identify('scope', platformRunId, rawRunId),
230
242
  },
231
243
  }
232
244
  }
@@ -235,7 +247,7 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
235
247
  const rawRunId = runId(event)
236
248
  if (type == 'run.failed' && rawRunId == platformRunId && event.failure != null) {
237
249
  const failure = object(event.failure, 'Runtime terminal failure')
238
- const context = nodeContext(failure, flows, platformRunId)
250
+ const context = nodeContext(failure, flows, platformRunId, identify)
239
251
  return {
240
252
  kind: 'node.failed',
241
253
  payload: {
@@ -258,13 +270,13 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
258
270
  type == 'node.completed' ||
259
271
  type == 'node.failed'
260
272
  ) {
261
- const context = nodeContext(event, flows, platformRunId)
273
+ const context = nodeContext(event, flows, platformRunId, identify)
262
274
  const executionId = await context.executionId
263
275
  const payload = {
264
276
  executionId,
265
277
  flowId: context.flowId,
266
278
  nodeId: context.nodeId,
267
- scopeId: await opaqueId('scope', platformRunId, context.rawRunId),
279
+ scopeId: await identify('scope', platformRunId, context.rawRunId),
268
280
  ...(event.systemOutputs === undefined ? {} : { systemOutputs: object(event.systemOutputs, 'Runtime system outputs') }),
269
281
  ...(event.handled === true ? { handled: true } : {}),
270
282
  }
@@ -109,7 +109,7 @@ function nodeKind(
109
109
  | 'javascript'
110
110
  | 'openapi'
111
111
  | 'http-request'
112
- | 'dedupe'
112
+ | 'skip-duplicates'
113
113
  | 'llm'
114
114
  | 'value'
115
115
  | 'wait'
@@ -13,6 +13,7 @@ import * as Effect from 'effect/Effect'
13
13
  import * as FiberSet from 'effect/FiberSet'
14
14
  import * as Queue from 'effect/Queue'
15
15
  import { nanoid } from 'nanoid'
16
+ import { z } from 'zod'
16
17
  import { isTriggerNode } from '../../flow/common/change.ts'
17
18
  import { nodeInputMappings } from '../../flow/common/condition.ts'
18
19
  import { nodeSystemOutputPorts, supportsNodeErrorHandling } from '../../flow/common/errorHandling.ts'
@@ -70,7 +71,7 @@ export type SchedulerEvent =
70
71
  | 'javascript'
71
72
  | 'openapi'
72
73
  | 'http-request'
73
- | 'dedupe'
74
+ | 'skip-duplicates'
74
75
  | 'llm'
75
76
  | 'value'
76
77
  | 'wait'
@@ -217,7 +218,7 @@ export type RunLaunch =
217
218
  readonly resume?: never
218
219
  }
219
220
  | {
220
- readonly resume: { readonly checkpoint: unknown }
221
+ readonly resume: { readonly checkpoint: unknown; readonly readyWorkflowRunIds?: readonly string[] }
221
222
  readonly bindingValues?: never
222
223
  readonly inputs?: never
223
224
  readonly trigger?: never
@@ -250,7 +251,7 @@ function runGraph(
250
251
  runId: string,
251
252
  launchInputs: Readonly<Record<string, Readonly<Record<string, JsonValue>>>> = {},
252
253
  trigger?: TriggerSeed,
253
- resume?: { readonly checkpoint: FlowRunCheckpoint },
254
+ resume?: { readonly checkpoint: FlowRunCheckpoint; readonly readyWorkflowRunIds?: ReadonlySet<string> },
254
255
  ): Effect.Effect<FlowRunOutcome, Error> {
255
256
  return Effect.scoped(
256
257
  Effect.gen(function* () {
@@ -292,6 +293,8 @@ function runGraph(
292
293
  const changes = yield* Queue.unbounded<void>()
293
294
  let remainingMs = context.remainingMs
294
295
  let measuredAt = yield* Clock.currentTimeMillis
296
+ let nextCallCheckAt = measuredAt + (resume == null ? 1000 : 0)
297
+ let readyWorkflowRunIds = resume?.readyWorkflowRunIds
295
298
  let idleSince: number | undefined
296
299
  const agents: Record<string, FlowRunCheckpoint['agents'][string]> = { ...resume?.checkpoint.agents }
297
300
  const resolveInputs = (nodeId: string, node: ExecutableNode) => resolveNodeInputs(nodeId, node, dependencies, context.bindingValues, launch[nodeId])
@@ -546,15 +549,20 @@ function runGraph(
546
549
  ),
547
550
  now,
548
551
  )
549
- for (const call of pendingCalls.values()) {
550
- yield* observe(
551
- call.nodeId,
552
- call.jobId,
553
- Effect.gen(function* () {
554
- const result = yield* execute(call.nodeId, target.graph.nodes[call.nodeId] as ExecutableNode, call.jobId, call.input)
555
- yield* settleNode(call.nodeId, call.jobId, result)
556
- }),
557
- ).pipe(Effect.catch(() => Effect.void))
552
+ if (pendingCalls.size > 0 && now >= nextCallCheckAt) {
553
+ for (const call of pendingCalls.values()) {
554
+ if (readyWorkflowRunIds != null && !readyWorkflowRunIds.has(call.childRunId)) continue
555
+ yield* observe(
556
+ call.nodeId,
557
+ call.jobId,
558
+ Effect.gen(function* () {
559
+ const result = yield* execute(call.nodeId, target.graph.nodes[call.nodeId] as ExecutableNode, call.jobId, call.input)
560
+ yield* settleNode(call.nodeId, call.jobId, result)
561
+ }),
562
+ ).pipe(Effect.catch(() => Effect.void))
563
+ }
564
+ readyWorkflowRunIds = undefined
565
+ nextCallCheckAt = (yield* Clock.currentTimeMillis) + 1000
558
566
  }
559
567
  const ready = dependencies.ready()
560
568
  for (const nodeId of ready) executeReady(nodeId)
@@ -592,7 +600,8 @@ function runGraph(
592
600
  true,
593
601
  )
594
602
  let next = Effect.raceFirst(activity, decision)
595
- if (pendingCalls.size > 0) next = Effect.raceFirst(next, Effect.sleep(1000).pipe(Effect.as({})))
603
+ if (pendingCalls.size > 0)
604
+ next = Effect.raceFirst(next, Effect.sleep(Math.max(0, nextCallCheckAt - (yield* Clock.currentTimeMillis))).pipe(Effect.as({})))
596
605
  if (left != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, left)).pipe(Effect.as({})))
597
606
  const resolutions = yield* next
598
607
  applyResolutions(resolutions, yield* Clock.currentTimeMillis)
@@ -631,6 +640,11 @@ export function runFlow(prepared: PreparedFlow, options: FlowRunOptions): Effect
631
640
  return yield* Effect.fail(new Error('A resumed Flow Run cannot accept launch inputs, binding values, or a Trigger seed.'))
632
641
  }
633
642
  const decoded = options.resume == null ? undefined : decodeFlowRunCheckpoint(options.resume.checkpoint)
643
+ const readyWorkflowRunIds =
644
+ options.resume?.readyWorkflowRunIds === undefined ? undefined : new Set(z.array(z.string().min(1)).parse(options.resume.readyWorkflowRunIds))
645
+ if (readyWorkflowRunIds != null && [...readyWorkflowRunIds].some((id) => !decoded?.calls?.some((call) => call.childRunId == id))) {
646
+ return yield* Effect.fail(new Error('Ready workflow Run is not present in the checkpoint.'))
647
+ }
634
648
  const outputs = (nodeId: string, value: Readonly<Record<string, JsonValue>>) =>
635
649
  isResolutionNode(prepared.graph.nodes[nodeId]) && value.pending != null ? { ...value, pending: withWaitNotificationMessage(value.pending) } : value
636
650
  const checkpoint =
@@ -692,7 +706,7 @@ export function runFlow(prepared: PreparedFlow, options: FlowRunOptions): Effect
692
706
  options.runId,
693
707
  options.inputs,
694
708
  trigger,
695
- checkpoint == null ? undefined : { checkpoint },
709
+ checkpoint == null ? undefined : { checkpoint, readyWorkflowRunIds },
696
710
  )
697
711
  })
698
712
  return program
@@ -167,7 +167,7 @@ export interface ConditionNode extends GraphNodeBase {
167
167
 
168
168
  export type ManagedTaskExecutor =
169
169
  | import('../../http/common/httpRequest.ts').HttpRequestExecutor
170
- | import('../../state/common/state.ts').DedupeExecutor
170
+ | import('../../state/common/state.ts').SkipDuplicatesExecutor
171
171
  | WorkflowExecutor
172
172
  | import('../../decision/common/decision.ts').DecisionExecutor
173
173
  | import('../../openapi/common/openapi.ts').OpenApiExecutor