@oomol-lab/open-flow 0.1.0-beta.76 → 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 (74) hide show
  1. package/README.md +3 -0
  2. package/dist/browser/{ContentIcon-Cm1hbVzW.js → ContentIcon-CMn0c-kl.js} +99 -6
  3. package/dist/browser/{FlowCanvas-NzKtla_c.js → FlowCanvas-BQGBcfoL.js} +1709 -613
  4. package/dist/browser/{createResourceDialog-BgdedQqu.js → createResourceDialog-DCGxfKFM.js} +63 -57
  5. package/dist/browser/{dialog-aSElFFw2.js → dialog-WzR4CRFH.js} +76 -77
  6. package/dist/browser/{exportSvg-Dcz4HTo_.js → exportSvg-CYwyYiuH.js} +2 -2
  7. package/dist/browser/{flowChanges-NZyS7HSp.js → flowChanges-Bz4dbDN_.js} +6875 -11091
  8. package/dist/browser/{flowWorkspace-Bl0SDehy.js → flowWorkspace-BI6L6a4r.js} +1415 -3037
  9. package/dist/browser/licenses.md +1 -1
  10. package/dist/browser/preview.css +619 -462
  11. package/dist/browser/preview.js +9016 -14956
  12. package/dist/browser/theme.css +4 -2
  13. package/dist/browser/{tooltip-3XMK9jkr.js → tooltip-BDcVTNpu.js} +65 -2
  14. package/dist/browser/{typeScriptSession-C9sBGOKZ.js → typeScriptSession-CRoezJPN.js} +1 -1
  15. package/dist/browser/ui.css +112 -24
  16. package/dist/browser/ui.js +3 -2
  17. package/dist/browser/workbench.css +10020 -9842
  18. package/dist/browser/workbench.d.ts +5 -2
  19. package/dist/browser/workbench.js +2382 -2668
  20. package/dist/browser/{workbenchSelect-DBGP7Y4Z.js → workbenchSelect-DtAd0zc4.js} +2 -2
  21. package/dist/command/index.js +48 -48
  22. package/dist/common/authoring.d.ts +4 -4
  23. package/dist/common/authoringExamples.d.ts +3 -2
  24. package/dist/common/authoringSchema.d.ts +15 -4
  25. package/dist/common/control-api-conformance.js +155 -15
  26. package/dist/common/control-api-errors.d.ts +12 -0
  27. package/dist/common/control-api.d.ts +39 -2
  28. package/dist/common/control-api.js +180 -37
  29. package/dist/common/control-requests.d.ts +23 -3
  30. package/dist/common/control-requests.js +141 -14
  31. package/dist/common/engine-contract.d.ts +3 -2
  32. package/dist/common/flow-authoring.js +47 -7
  33. package/dist/common/flow-change-schema.d.ts +1 -1
  34. package/dist/common/flow-change.d.ts +2 -2
  35. package/dist/common/flow-change.js +48 -8
  36. package/dist/common/flow-encoding.js +47 -7
  37. package/dist/common/flow-semantics.js +86 -8
  38. package/dist/common/flow-state.d.ts +100 -0
  39. package/dist/common/flow-state.js +5794 -0
  40. package/dist/common/mcp.d.ts +96 -3
  41. package/dist/common/mcp.js +158 -13
  42. package/dist/common/run-events.js +19 -8
  43. package/dist/common/runtime-contract.d.ts +14 -0
  44. package/dist/common/runtime-contract.js +124 -17
  45. package/dist/common/runtime-nodejs-compat.js +11 -12
  46. package/dist/common/scheduler-node-execution.d.ts +1 -0
  47. package/dist/common/scheduler.d.ts +2 -1
  48. package/dist/common/scheduler.js +95 -40
  49. package/dist/common/state-conformance.d.ts +16 -0
  50. package/dist/common/tags.d.ts +4 -0
  51. package/dist/index.d.ts +11 -0
  52. package/package.json +5 -1
  53. package/src/control/common/api.ts +101 -11
  54. package/src/control/common/authoring.ts +7 -1
  55. package/src/control/common/authoringExamples.ts +9 -0
  56. package/src/control/common/authoringSchema.ts +18 -12
  57. package/src/control/common/errors.ts +6 -0
  58. package/src/control/common/flowDecoders.ts +1 -0
  59. package/src/control/common/mcp.ts +16 -0
  60. package/src/control/common/requests.ts +16 -2
  61. package/src/control/common/tags.ts +20 -0
  62. package/src/execution/common/events.ts +22 -7
  63. package/src/execution/common/node-execution.ts +3 -0
  64. package/src/execution/common/scheduler.ts +29 -13
  65. package/src/flow/common/change.ts +2 -1
  66. package/src/flow/common/changeSchema.ts +11 -1
  67. package/src/flow/common/errorHandling.ts +1 -1
  68. package/src/flow/common/semantics.ts +27 -1
  69. package/src/runtime/common/engineContract.ts +6 -3
  70. package/src/runtime/common/nodejsGuest.generated.json +11 -12
  71. package/src/runtime/common/runtime.ts +122 -7
  72. package/src/state/common/conformance.ts +81 -0
  73. package/src/state/common/state.ts +141 -0
  74. package/src/types/index.ts +11 -0
@@ -63,6 +63,7 @@ export declare function executeNode(context: NodeExecutionContext, input: {
63
63
  readonly node: ExecutableNode;
64
64
  readonly jobId: string;
65
65
  readonly nodeInputs: Readonly<Record<string, JsonValue>>;
66
+ readonly launchInputs?: Readonly<Record<string, JsonValue>>;
66
67
  readonly flowId: string;
67
68
  readonly runId: string;
68
69
  readonly graph: Graph;
@@ -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' | '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;
@@ -9535,6 +9535,43 @@ var httpRequestExecutorSchema = strictObject({
9535
9535
  ]).default("auto"),
9536
9536
  statusPolicy: _enum(["success", "all"]).default("success")
9537
9537
  }).extend({ kind: literal("http-request") });
9538
+ strictObject({
9539
+ scope: _enum(["draft", "live"]),
9540
+ values: boolean().optional()
9541
+ });
9542
+ strictObject({
9543
+ scope: _enum(["draft", "live"]),
9544
+ key: string$1().min(1).refine((key) => new TextEncoder().encode(key).length <= 256).optional()
9545
+ });
9546
+ strictObject({
9547
+ scope: _enum(["draft", "live"]),
9548
+ space: string$1().regex(/^(flow|node:.+)$/)
9549
+ });
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") });
9551
+ object({
9552
+ version: literal(1),
9553
+ scope: _enum(["draft", "live"]),
9554
+ limits: object({
9555
+ maxValueBytes: number().positive(),
9556
+ maxValues: number().int().positive(),
9557
+ maxSpaces: number().int().positive(),
9558
+ maxIdentities: number().positive(),
9559
+ maxRunWrites: number().positive()
9560
+ }),
9561
+ values: array(object({
9562
+ key: string$1(),
9563
+ size: number().nonnegative(),
9564
+ updatedAt: number(),
9565
+ value: json$1().optional()
9566
+ })),
9567
+ spaces: array(object({
9568
+ space: string$1(),
9569
+ count: number().nonnegative(),
9570
+ pendingCount: number().nonnegative(),
9571
+ reservedCount: number().nonnegative(),
9572
+ updatedAt: number()
9573
+ }))
9574
+ });
9538
9575
  //#endregion
9539
9576
  //#region src/flow/common/retry.ts
9540
9577
  var retryNodeTypes = [
@@ -9889,7 +9926,8 @@ var managedFields = object({
9889
9926
  flowId: text,
9890
9927
  revisionId: text
9891
9928
  }),
9892
- httpRequestExecutorSchema
9929
+ httpRequestExecutorSchema,
9930
+ skipDuplicatesExecutorSchema
9893
9931
  ])
9894
9932
  });
9895
9933
  var managed = managedFields.transform(limitDecisionTask);
@@ -10143,7 +10181,8 @@ object({
10143
10181
  literal(9),
10144
10182
  literal(10),
10145
10183
  literal(11),
10146
- literal(12)
10184
+ literal(12),
10185
+ literal(13)
10147
10186
  ]),
10148
10187
  document,
10149
10188
  modules: record$1(text, module)
@@ -10551,7 +10590,7 @@ var nodeErrorSchema = {
10551
10590
  }
10552
10591
  };
10553
10592
  function supportsNodeErrorHandling(node) {
10554
- return supportsNodeRetry(node);
10593
+ return supportsNodeRetry(node) || node.kind === "task" && "executor" in node.task && node.task.executor.kind === "skip-duplicates";
10555
10594
  }
10556
10595
  function nodeSystemOutputPorts(node) {
10557
10596
  return supportsNodeErrorHandling(node) && node.onError == "error" ? { error: {
@@ -11580,6 +11619,7 @@ function executeNode(context, input) {
11580
11619
  if (additional.has(handle)) return false;
11581
11620
  const task = "executor" in node.task ? node.task : void 0;
11582
11621
  if (task == null || !["openapi", "http-request"].includes(task.executor.kind)) return true;
11622
+ if (Object.hasOwn(input.launchInputs ?? {}, handle)) return true;
11583
11623
  const mapping = node.inputs[handle];
11584
11624
  return mapping?.kind != "unset" && (mapping != null || task.inputs.some((port) => "handle" in port && port.handle == handle && port.value !== void 0));
11585
11625
  })),
@@ -11949,6 +11989,8 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
11949
11989
  const changes = yield* Queue.unbounded();
11950
11990
  let remainingMs = context.remainingMs;
11951
11991
  let measuredAt = yield* Clock.currentTimeMillis;
11992
+ let nextCallCheckAt = measuredAt + (resume == null ? 1e3 : 0);
11993
+ let readyWorkflowRunIds = resume?.readyWorkflowRunIds;
11952
11994
  let idleSince;
11953
11995
  const agents = { ...resume?.checkpoint.agents };
11954
11996
  const resolveInputs = (nodeId, node) => resolveNodeInputs(nodeId, node, dependencies, context.bindingValues, launch[nodeId]);
@@ -11994,6 +12036,7 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
11994
12036
  node,
11995
12037
  jobId,
11996
12038
  nodeInputs,
12039
+ launchInputs: launch[nodeId],
11997
12040
  resolution,
11998
12041
  flowId: target.flowId,
11999
12042
  runId,
@@ -12165,10 +12208,17 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
12165
12208
  return yield* Effect.failCause(firstCause);
12166
12209
  }
12167
12210
  if (pendingWaits.size > 0 && context.waits != null) applyResolutions(yield* context.waits.resolutions([...pendingWaits.values()].map((wait) => wait.waitId), false), now);
12168
- for (const call of pendingCalls.values()) yield* observe(call.nodeId, call.jobId, Effect.gen(function* () {
12169
- const result = yield* execute(call.nodeId, target.graph.nodes[call.nodeId], call.jobId, call.input);
12170
- yield* settleNode(call.nodeId, call.jobId, result);
12171
- })).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
+ }
12172
12222
  const ready = dependencies.ready();
12173
12223
  for (const nodeId of ready) executeReady(nodeId);
12174
12224
  const running = yield* FiberSet.size(active);
@@ -12203,7 +12253,7 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
12203
12253
  const activity = Queue.take(changes).pipe(Effect.as({}));
12204
12254
  const decision = pendingWaits.size == 0 || context.waits == null ? Effect.never : context.waits.resolutions([...pendingWaits.values()].map((wait) => wait.waitId), true);
12205
12255
  let next = Effect.raceFirst(activity, decision);
12206
- 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({})));
12207
12257
  if (left != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, left)).pipe(Effect.as({})));
12208
12258
  applyResolutions(yield* next, yield* Clock.currentTimeMillis);
12209
12259
  }
@@ -12241,6 +12291,8 @@ function runFlow(prepared, options) {
12241
12291
  return Effect.gen(function* () {
12242
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."));
12243
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."));
12244
12296
  const outputs = (nodeId, value) => isResolutionNode(prepared.graph.nodes[nodeId]) && value.pending != null ? {
12245
12297
  ...value,
12246
12298
  pending: withWaitNotificationMessage(value.pending)
@@ -12291,7 +12343,10 @@ function runFlow(prepared, options) {
12291
12343
  inputDefs: prepared.inputDefs,
12292
12344
  outputDefs: prepared.outputDefs,
12293
12345
  outputMappings: prepared.outputMappings
12294
- }, 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
+ });
12295
12350
  });
12296
12351
  }
12297
12352
  //#endregion
@@ -0,0 +1,16 @@
1
+ import type { JsonValue } from './flow-model.js';
2
+ import type { SkipDuplicatesConfig } from './flow-state.js';
3
+ export interface StateConformanceHarness {
4
+ readonly maxIdentities: number;
5
+ /** Executes Code on the same initially empty Flow and scope on every call; returns its `result` output. */
6
+ run(source: string): Promise<JsonValue>;
7
+ /** Executes the same Skip Duplicates node across Runs, updating its business config and items. */
8
+ skipDuplicates(config: SkipDuplicatesConfig, items: JsonValue[]): Promise<{
9
+ items: JsonValue[];
10
+ duplicates: JsonValue[];
11
+ }>;
12
+ }
13
+ export declare const stateConformanceCases: readonly {
14
+ name: string;
15
+ verify(harness: StateConformanceHarness): Promise<void>;
16
+ }[];
@@ -0,0 +1,4 @@
1
+ import type { FlowQuery, Tag, TagListItem } from './control-api.js';
2
+ export declare function normalizeFlowQuery(query?: FlowQuery): Required<FlowQuery>;
3
+ export declare function decodeTag(value: unknown): Tag;
4
+ export declare function decodeTagListItem(value: unknown): TagListItem;
package/dist/index.d.ts CHANGED
@@ -102,6 +102,17 @@ export interface TaskContext<Actions extends object = Record<string, never>, Wor
102
102
  readonly inputs: Readonly<Record<string, unknown>>;
103
103
  readonly runId: string;
104
104
  readonly blockId: string;
105
+ readonly state: {
106
+ get(key: string): Promise<JsonValue | null>;
107
+ set(key: string, value: JsonValue): Promise<void>;
108
+ insert(key: string, value: Exclude<JsonValue, null>): Promise<boolean>;
109
+ delete(key: string): Promise<void>;
110
+ list(): Promise<string[]>;
111
+ };
112
+ readonly dedupe: {
113
+ insert(ids: readonly (string | number)[]): Promise<boolean[]>;
114
+ has(ids: readonly (string | number)[]): Promise<boolean[]>;
115
+ };
105
116
  readonly artifact: ArtifactCapability;
106
117
  /** Reads the selected direct predecessors, sorted by node ID; empty for a graph root. */
107
118
  getPrevious(): Promise<readonly PreviousNode[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oomol-lab/open-flow",
3
- "version": "0.1.0-beta.76",
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": {
@@ -25,6 +25,10 @@
25
25
  "types": "./dist/common/decision.d.ts",
26
26
  "import": "./dist/common/decision.js"
27
27
  },
28
+ "./flow-state": {
29
+ "types": "./dist/common/flow-state.d.ts",
30
+ "import": "./dist/common/flow-state.js"
31
+ },
28
32
  "./http-request": {
29
33
  "types": "./dist/common/http-request.d.ts",
30
34
  "import": "./dist/common/http-request.js"
@@ -1,6 +1,8 @@
1
1
  import type { CanvasCommand } from '../../canvas/common/commands.ts'
2
+ import type { FlowStateView, StateScope } from '../../state/common/state.ts'
2
3
 
3
4
  import { currentEngineContract } from '../../runtime/common/engineContract.ts'
5
+ import { flowStateViewSchema } from '../../state/common/state.ts'
4
6
  import { workflowContractSchema, workflowReferencesSchema } from '../../workflow/common/workflow.ts'
5
7
  export type { CanvasCommand, CanvasCommandSubscriber } from '../../canvas/common/commands.ts'
6
8
  import type { LiveDetails } from './liveDetails.ts'
@@ -77,6 +79,8 @@ export type { FlowCatalogEvent, FlowChangeEvent } from './flowNotifications.ts'
77
79
 
78
80
  import { ApiError } from './errors.ts'
79
81
  import { randomId } from './random.ts'
82
+ import { decodeTag, decodeTagListItem, normalizeFlowQuery } from './tags.ts'
83
+ export { normalizeFlowQuery } from './tags.ts'
80
84
 
81
85
  export interface CanvasBroadcast {
82
86
  readonly version: 1
@@ -103,7 +107,29 @@ export interface FlowResources {
103
107
  }
104
108
  }
105
109
 
110
+ export interface FlowUpdate {
111
+ readonly name?: string
112
+ readonly tagIds?: readonly string[]
113
+ }
114
+
115
+ export interface FlowQuery {
116
+ readonly query?: string
117
+ readonly tagIds?: readonly string[]
118
+ readonly untagged?: boolean
119
+ }
120
+
121
+ export interface Tag {
122
+ readonly tagId: string
123
+ readonly name: string
124
+ readonly version: 1
125
+ }
126
+
127
+ export interface TagListItem extends Tag {
128
+ readonly referenceCount: number
129
+ }
130
+
106
131
  export interface Flow {
132
+ readonly tagIds?: readonly string[]
107
133
  readonly live?: { readonly enabled: boolean; readonly hasStart: boolean; readonly publicationId: string; readonly revisionId: string }
108
134
 
109
135
  readonly createdAt: string
@@ -123,6 +149,11 @@ export interface PublicationPresentation {
123
149
  readonly presentation: Presentation | null
124
150
  }
125
151
 
152
+ export interface FlowCreationOptions {
153
+ readonly tagIds?: readonly string[]
154
+ readonly presentation?: Readonly<Record<string, JsonValue>>
155
+ }
156
+
126
157
  export interface Presentation {
127
158
  readonly revision: number
128
159
  readonly updatedAt: string
@@ -677,6 +708,7 @@ export function decodeRunEvent(value: unknown) {
677
708
  nodeKind !== 'decision' &&
678
709
  nodeKind !== 'openapi' &&
679
710
  nodeKind !== 'http-request' &&
711
+ nodeKind !== 'skip-duplicates' &&
680
712
  nodeKind !== 'llm' &&
681
713
  nodeKind !== 'agent' &&
682
714
  nodeKind !== 'value' &&
@@ -809,9 +841,13 @@ export class ControlClient {
809
841
  }
810
842
 
811
843
  async listFlows(
812
- options: { readonly cursor?: string; readonly includeTotal?: boolean; readonly limit?: number; readonly signal?: AbortSignal } = {},
844
+ options: FlowQuery & { readonly cursor?: string; readonly includeTotal?: boolean; readonly limit?: number; readonly signal?: AbortSignal } = {},
813
845
  ): Promise<FlowPage> {
814
846
  const parameters = new URLSearchParams()
847
+ const filter = normalizeFlowQuery(options)
848
+ if (filter.query) parameters.set('query', filter.query)
849
+ if (filter.tagIds.length) parameters.set('tagIds', filter.tagIds.join(','))
850
+ if (filter.untagged) parameters.set('untagged', 'true')
815
851
  if (options.cursor != null) parameters.set('cursor', options.cursor)
816
852
  if (options.limit != null) parameters.set('limit', String(options.limit))
817
853
  if (options.includeTotal != null) parameters.set('includeTotal', String(options.includeTotal))
@@ -897,20 +933,35 @@ export class ControlClient {
897
933
  }
898
934
  }
899
935
 
900
- async importFlow(file: WorkflowFile, options: { readonly name?: string; readonly teamId?: string; readonly idempotencyKey?: string } = {}): Promise<Flow> {
936
+ async importFlow(
937
+ file: WorkflowFile,
938
+ options: { readonly name?: string; readonly teamId?: string; readonly idempotencyKey?: string; readonly tagIds?: readonly string[] } = {},
939
+ ): Promise<Flow> {
901
940
  return flow(
902
941
  await this.request('/v1/flows/import', {
903
942
  method: 'POST',
904
943
  headers: { 'Idempotency-Key': options.idempotencyKey ?? `flow-${randomId()}` },
905
- body: JSON.stringify({ file, name: options.name, teamId: options.teamId, version: 1 }),
944
+ body: JSON.stringify({ file, name: options.name, teamId: options.teamId, tagIds: options.tagIds, version: 1 }),
906
945
  }),
907
946
  )
908
947
  }
909
948
 
910
- async createFlow(name: string, idempotencyKey = `flow-${randomId()}`, teamId?: 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> {
911
956
  return flow(
912
957
  await this.request('/v1/flows', {
913
- body: JSON.stringify({ name, ...(teamId == null ? {} : { teamId }), 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
+ }),
914
965
  headers: { 'idempotency-key': idempotencyKey },
915
966
  method: 'POST',
916
967
  }),
@@ -939,13 +990,52 @@ export class ControlClient {
939
990
  return source.listeners.map(errorListener)
940
991
  }
941
992
 
942
- async renameFlow(flowId: string, name: string): Promise<Flow> {
943
- return flow(
944
- await this.request(`/v1/flows/${segment(flowId)}`, {
945
- body: JSON.stringify({ name, version: 1 }),
946
- method: 'PATCH',
947
- }),
993
+ async listTags(signal?: AbortSignal): Promise<{ readonly tags: readonly TagListItem[]; readonly version: 1 }> {
994
+ const source = record(await this.request('/v1/tags', { signal }))
995
+ if (source.version != 1 || !Array.isArray(source.tags)) return invalidResponse()
996
+ return { tags: source.tags.map(decodeTagListItem), version: 1 }
997
+ }
998
+
999
+ async createTag(name: string): Promise<Tag> {
1000
+ return decodeTag(await this.request('/v1/tags', { method: 'POST', body: JSON.stringify({ name, version: 1 }) }))
1001
+ }
1002
+
1003
+ async renameTag(tagId: string, name: string): Promise<Tag> {
1004
+ return decodeTag(await this.request(`/v1/tags/${segment(tagId)}`, { method: 'PATCH', body: JSON.stringify({ name, version: 1 }) }))
1005
+ }
1006
+
1007
+ async deleteTag(tagId: string): Promise<void> {
1008
+ const source = record(await this.request(`/v1/tags/${segment(tagId)}`, { method: 'DELETE' }))
1009
+ if (source.version != 1) invalidResponse()
1010
+ }
1011
+
1012
+ async setFlowTags(flowId: string, tagIds: readonly string[]): Promise<Flow> {
1013
+ return flow(await this.request(`/v1/flows/${segment(flowId)}/tags`, { method: 'PUT', body: JSON.stringify({ tagIds, version: 1 }) }))
1014
+ }
1015
+
1016
+ async updateFlow(flowId: string, changes: FlowUpdate): Promise<Flow> {
1017
+ return flow(await this.request(`/v1/flows/${segment(flowId)}`, { method: 'PATCH', body: JSON.stringify({ ...changes, version: 1 }) }))
1018
+ }
1019
+
1020
+ async getFlowState(flowId: string, scope: StateScope, values = false): Promise<FlowStateView> {
1021
+ const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}&values=${values}`))
1022
+ return result.success && result.data.scope === scope ? result.data : invalidResponse()
1023
+ }
1024
+
1025
+ async clearFlowState(flowId: string, scope: StateScope, key?: string): Promise<void> {
1026
+ const source = record(
1027
+ await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}${key == null ? '' : `&key=${encodeURIComponent(key)}`}`, { method: 'DELETE' }),
948
1028
  )
1029
+ if (source.version !== 1 || source.cleared !== true) invalidResponse()
1030
+ }
1031
+
1032
+ async clearFlowDedupe(flowId: string, scope: StateScope, space: string): Promise<void> {
1033
+ const source = record(await this.request(`/v1/flows/${segment(flowId)}/dedupe?scope=${scope}&space=${encodeURIComponent(space)}`, { method: 'DELETE' }))
1034
+ if (source.version !== 1 || source.cleared !== true) invalidResponse()
1035
+ }
1036
+
1037
+ async renameFlow(flowId: string, name: string): Promise<Flow> {
1038
+ return this.updateFlow(flowId, { name })
949
1039
  }
950
1040
 
951
1041
  async deleteFlow(flowId: string): Promise<Flow> {
@@ -43,6 +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 { skipDuplicatesConfigSchema, skipDuplicatesTask } from '../../state/common/state.ts'
46
47
  import { withWorkflowTask, workflowTask, workflowSelectionTask, workflowCapability, workflowAgentTool } from '../../workflow/common/workflow.ts'
47
48
  import { conditionConfigurationView, prepareConditionConfiguration } from './authoringCondition.ts'
48
49
  import { authoringDiagnosticField } from './authoringDiagnosticFields.ts'
@@ -146,7 +147,7 @@ export function authoringNode(content: RevisionContent, ref: string) {
146
147
  ),
147
148
  }
148
149
  else if (task.executor.kind == 'workflow') config = { flowId: task.executor.flowId }
149
- else if (task.executor.kind == 'http-request') {
150
+ else if (task.executor.kind == 'http-request' || task.executor.kind == 'skip-duplicates') {
150
151
  const { kind: _kind, ...settings } = task.executor
151
152
  config = settings
152
153
  } else if (task.executor.kind == 'openapi') {
@@ -521,6 +522,8 @@ export async function compileAuthoring(base: RevisionContent, request: Authoring
521
522
  task = { ...before, name: String(update.name), executor: { ...executor, ...config } } as ManagedTaskDefinition
522
523
  } else task = await connectorTask(config, String(update.name))
523
524
  } else if (type == 'decision') task = decisionTask(config.questions as Parameters<typeof decisionTask>[0], String(update.name))
525
+ else if (type == 'skip-duplicates')
526
+ task = { ...before, name: String(update.name), executor: { kind: 'skip-duplicates', ...skipDuplicatesConfigSchema.parse(config) } }
524
527
  else if (type == 'http-request') {
525
528
  task = httpRequestTask(httpRequestConfigSchema.parse(config), String(update.name), before)
526
529
  next = { ...next, inputs: httpRequestInputs(node.inputs, before, task) } as GraphNode
@@ -774,6 +777,9 @@ export async function compileAuthoring(base: RevisionContent, request: Authoring
774
777
  case 'connector':
775
778
  changes = createManagedTask(identity, await connectorTask(config, name))
776
779
  break
780
+ case 'skip-duplicates':
781
+ changes = createManagedTask(identity, skipDuplicatesTask(skipDuplicatesConfigSchema.parse(config), name))
782
+ break
777
783
  case 'http-request':
778
784
  changes = createManagedTask(identity, httpRequestTask(httpRequestConfigSchema.parse(config), name))
779
785
  break
@@ -13,6 +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 { skipDuplicatesTask } from '../../state/common/state.ts'
16
17
 
17
18
  const descriptions = {
18
19
  'start': 'A Start node with a required message parameter. Supply it through trigger.outputs.',
@@ -23,6 +24,8 @@ const descriptions = {
23
24
  'poll': 'Gmail polling by key; replace CONNECTION_ID with an active Gmail connection.',
24
25
  'integration': 'Telegram webhook integration by key; replace CONNECTION_ID with an active Telegram connection.',
25
26
  'decision': 'AI Decision evaluates named questions against target and returns a complete answer object per output. Configure target before running.',
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.',
26
29
  'http-request': 'A single HTTP request. Bind url and request inputs; outputs are body, statusCode and headers.',
27
30
  'openapi': 'A fixed JSON API operation. Replace the example document and URL before running.',
28
31
  'connector':
@@ -57,6 +60,12 @@ export function authoringExample(name: string): { version: 1; operations: readon
57
60
  { name: 'needs_support', type: 'noul', instructions: 'Does the customer need support?' },
58
61
  ])
59
62
  break
63
+ case 'skip-duplicates':
64
+ operations = [
65
+ ...createManagedTask({ nodeId: 'skip-duplicates' }, skipDuplicatesTask()),
66
+ { kind: 'graph.node.input.set', nodeId: 'skip-duplicates', handle: 'items', value: { kind: 'value', value: ['a', 'b', 'a'] } },
67
+ ]
68
+ break
60
69
  case 'http-request':
61
70
  operations = [
62
71
  ...createManagedTask({ nodeId: 'http' }, httpRequestTask()),
@@ -12,6 +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 { skipDuplicatesConfigSchema } from '../../state/common/state.ts'
15
16
  import { authoringLlmConfiguration } from './authoringLlm.ts'
16
17
  import { authoringOpenApiSchema } from './authoringOpenApi.ts'
17
18
 
@@ -35,6 +36,7 @@ export const authoringTypes = [
35
36
  'decision',
36
37
  'openapi',
37
38
  'http-request',
39
+ 'skip-duplicates',
38
40
  'condition',
39
41
  'value',
40
42
  'wait',
@@ -118,6 +120,7 @@ export const authoringConfigurations = {
118
120
  'connector': definitions.connector.omit({ outputMode: true }).strict(),
119
121
  'openapi': authoringOpenApiSchema,
120
122
  'http-request': httpRequestConfigSchema,
123
+ 'skip-duplicates': skipDuplicatesConfigSchema,
121
124
  'agent': definitions.agent
122
125
  .omit({ prompt: true, tools: true })
123
126
  .extend({
@@ -283,7 +286,7 @@ export function authoringSchema(kind?: AuthoringType) {
283
286
  }
284
287
  : {
285
288
  type: kind,
286
- onError: retryNodeTypes.some((nodeType) => nodeType == kind) ? { enum: ['stop', 'error'] } : false,
289
+ onError: kind == 'skip-duplicates' || retryNodeTypes.some((nodeType) => nodeType == kind) ? { enum: ['stop', 'error'] } : false,
287
290
  retry: retryNodeTypes.some((nodeType) => nodeType == kind) ? z.toJSONSchema(retryConfigurationSchema) : false,
288
291
  inputSources: ['poll', 'integration'].includes(kind)
289
292
  ? ['value', 'unset', 'default']
@@ -306,17 +309,19 @@ export function authoringSchema(kind?: AuthoringType) {
306
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.'
307
310
  : kind == 'llm'
308
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}}.'
309
- : kind == 'http-request'
310
- ? '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.'
311
- : kind == 'openapi'
312
- ? 'Select an operation; the tool resolves its ports and authentication. Read authenticationChoices from node details. authentication is replaced as a complete choice. Bind advertised credentials through inputs.'
313
- : kind == 'workflow'
314
- ? 'Select another workflow with config.flowId. A workflow cannot call itself, including in a disconnected node. Owned unpublished or unavailable targets may be saved as incomplete drafts. Use node.refresh to load the current published interface.'
315
- : kind == 'agent'
316
- ? 'Tools accept Connector Actions or {kind: "workflow", flowId, name, description, approval, inputs}. Workflow parameter sources use model, input or value. The platform derives the interface. Refresh a workflow tool with node.refresh and tool: its name.'
317
- : kind == 'code'
318
- ? codeGuidance
319
- : 'Supply business configuration and input data. Fixed definitions are assembled by the tool. Existing constraints and defaults are preserved on partial updates.',
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
+ : kind == 'http-request'
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
+ : kind == 'openapi'
317
+ ? 'Select an operation; the tool resolves its ports and authentication. Read authenticationChoices from node details. authentication is replaced as a complete choice. Bind advertised credentials through inputs.'
318
+ : kind == 'workflow'
319
+ ? 'Select another workflow with config.flowId. A workflow cannot call itself, including in a disconnected node. Owned unpublished or unavailable targets may be saved as incomplete drafts. Use node.refresh to load the current published interface.'
320
+ : kind == 'agent'
321
+ ? 'Tools accept Connector Actions or {kind: "workflow", flowId, name, description, approval, inputs}. Workflow parameter sources use model, input or value. The platform derives the interface. Refresh a workflow tool with node.refresh and tool: its name.'
322
+ : kind == 'code'
323
+ ? codeGuidance
324
+ : 'Supply business configuration and input data. Fixed definitions are assembled by the tool. Existing constraints and defaults are preserved on partial updates.',
320
325
  example: {
321
326
  op: 'node.add',
322
327
  as: 'step',
@@ -330,6 +335,7 @@ export function authoringSchema(kind?: AuthoringType) {
330
335
  'connector': { config: { action: '<Action ID>', connectionId: '<Connection ID>' } },
331
336
  'poll': { config: { key: '<Trigger key>', schedule: [{ type: 'every', unit: 'minute', value: 5 }] } },
332
337
  'integration': { config: { key: '<Trigger key>' } },
338
+ 'skip-duplicates': { config: { fields: ['id'] }, inputs: { items: { kind: 'value', value: [{ id: 'a' }, { id: 'b' }] } } },
333
339
  'http-request': { config: {}, inputs: { url: { kind: 'value', value: 'https://example.com' } } },
334
340
  'openapi': { config: { sourceUrl: 'https://example.com/openapi.json', path: '/items', method: 'get' } },
335
341
  'code': { code: 'export default ({value}) => ({result:value})', inputs: { value: { kind: 'value', value: 'Hello' } } },