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

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 (64) 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-DkovGogv.js} +3 -3
  5. package/dist/browser/{dialog-BcFBfRB1.js → dialog-CFFuRM8s.js} +37 -86
  6. package/dist/browser/{exportSvg-CG8hEUNk.js → exportSvg-CYwyYiuH.js} +2 -2
  7. package/dist/browser/{flowChanges-BGo_4Ypo.js → flowChanges-CN2R-m1M.js} +1258 -422
  8. package/dist/browser/{flowWorkspace-BFHGev7s.js → flowWorkspace-CXVL1FNv.js} +634 -628
  9. package/dist/browser/licenses.md +1 -1
  10. package/dist/browser/preview.css +512 -462
  11. package/dist/browser/preview.js +8183 -6384
  12. package/dist/browser/{tooltip-BAWSUexx.js → tooltip-BDcVTNpu.js} +65 -2
  13. package/dist/browser/{typeScriptSession-CRoezJPN.js → typeScriptSession-CoEIptNC.js} +1 -1
  14. package/dist/browser/ui.css +5 -24
  15. package/dist/browser/workbench.css +512 -462
  16. package/dist/browser/workbench.d.ts +2 -3
  17. package/dist/browser/workbench.js +65 -690
  18. package/dist/browser/{workbenchSelect-CjK-xdm3.js → workbenchSelect-CkOS95gX.js} +2 -2
  19. package/dist/command/index.js +31 -31
  20. package/dist/common/authoring.d.ts +3 -3
  21. package/dist/common/authoringExamples.d.ts +3 -3
  22. package/dist/common/authoringSchema.d.ts +14 -22
  23. package/dist/common/control-api-conformance.js +91 -25
  24. package/dist/common/control-api.d.ts +8 -3
  25. package/dist/common/control-api.js +51 -25
  26. package/dist/common/control-requests.d.ts +9 -3
  27. package/dist/common/control-requests.js +54 -42
  28. package/dist/common/flow-authoring.js +32 -19
  29. package/dist/common/flow-change.d.ts +1 -1
  30. package/dist/common/flow-change.js +32 -19
  31. package/dist/common/flow-encoding.js +31 -18
  32. package/dist/common/flow-semantics.js +41 -26
  33. package/dist/common/flow-state.d.ts +49 -43
  34. package/dist/common/flow-state.js +95 -110
  35. package/dist/common/mcp.d.ts +35 -4
  36. package/dist/common/mcp.js +51 -26
  37. package/dist/common/run-events.js +19 -9
  38. package/dist/common/runtime-contract.d.ts +4 -1
  39. package/dist/common/runtime-contract.js +66 -17
  40. package/dist/common/runtime-nodejs-compat.js +11 -12
  41. package/dist/common/scheduler.d.ts +2 -1
  42. package/dist/common/scheduler.js +82 -55
  43. package/dist/common/state-conformance.d.ts +3 -3
  44. package/dist/index.d.ts +1 -0
  45. package/package.json +1 -1
  46. package/src/control/common/api.ts +36 -7
  47. package/src/control/common/authoring.ts +6 -5
  48. package/src/control/common/authoringExamples.ts +6 -5
  49. package/src/control/common/authoringSchema.ts +7 -7
  50. package/src/control/common/conformance.ts +31 -0
  51. package/src/control/common/mcp.ts +7 -2
  52. package/src/control/common/requests.ts +10 -1
  53. package/src/execution/common/events.ts +22 -10
  54. package/src/execution/common/node-execution.ts +1 -1
  55. package/src/execution/common/scheduler.ts +28 -14
  56. package/src/flow/common/change.ts +1 -1
  57. package/src/flow/common/changeSchema.ts +4 -4
  58. package/src/flow/common/errorHandling.ts +1 -1
  59. package/src/flow/common/semantics.ts +12 -6
  60. package/src/runtime/common/nodejsGuest.generated.json +11 -12
  61. package/src/runtime/common/runtime.ts +45 -12
  62. package/src/state/common/conformance.ts +32 -21
  63. package/src/state/common/state.ts +85 -80
  64. package/src/types/index.ts +1 -0
@@ -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;
@@ -9535,23 +9535,30 @@ var httpRequestExecutorSchema = strictObject({
9535
9535
  ]).default("auto"),
9536
9536
  statusPolicy: _enum(["success", "all"]).default("success")
9537
9537
  }).extend({ kind: literal("http-request") });
9538
+ //#endregion
9539
+ //#region src/state/common/state.ts
9540
+ var stateReadLimits = {
9541
+ maxKeys: 16,
9542
+ maxBytes: 1048576
9543
+ };
9544
+ var keySchema = string$1().min(1).refine((value) => new TextEncoder().encode(value).length <= 256);
9545
+ strictObject({
9546
+ scope: _enum(["draft", "live"]),
9547
+ values: literal(false).optional()
9548
+ });
9538
9549
  strictObject({
9539
9550
  scope: _enum(["draft", "live"]),
9540
- values: boolean().optional()
9551
+ keys: array(keySchema).min(1).max(stateReadLimits.maxKeys).refine((keys) => new Set(keys).size === keys.length, "State keys must be distinct.")
9541
9552
  });
9542
9553
  strictObject({
9543
9554
  scope: _enum(["draft", "live"]),
9544
- key: string$1().min(1).refine((key) => new TextEncoder().encode(key).length <= 256).optional()
9555
+ key: keySchema.optional()
9545
9556
  });
9546
9557
  strictObject({
9547
9558
  scope: _enum(["draft", "live"]),
9548
9559
  space: string$1().regex(/^(flow|node:.+)$/)
9549
9560
  });
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") });
9561
+ 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
9562
  object({
9556
9563
  version: literal(1),
9557
9564
  scope: _enum(["draft", "live"]),
@@ -9562,24 +9569,30 @@ object({
9562
9569
  maxIdentities: number().positive(),
9563
9570
  maxRunWrites: number().positive()
9564
9571
  }),
9565
- values: array(object({
9572
+ values: array(strictObject({
9566
9573
  key: string$1(),
9567
9574
  size: number().nonnegative(),
9568
- updatedAt: number(),
9569
- value: json$1().optional()
9575
+ updatedAt: number()
9570
9576
  })),
9571
9577
  spaces: array(object({
9572
9578
  space: string$1(),
9573
- mode: _enum(["new", "increasing"]),
9574
9579
  count: number().nonnegative(),
9575
- watermark: union([
9576
- string$1(),
9577
- number(),
9578
- _null()
9579
- ]),
9580
+ pendingCount: number().nonnegative(),
9581
+ reservedCount: number().nonnegative(),
9580
9582
  updatedAt: number()
9581
9583
  }))
9582
9584
  });
9585
+ object({
9586
+ version: literal(1),
9587
+ scope: _enum(["draft", "live"]),
9588
+ values: array(strictObject({
9589
+ key: keySchema,
9590
+ value: json$1()
9591
+ })).min(1).max(stateReadLimits.maxKeys)
9592
+ }).refine((response) => {
9593
+ const encoder = new TextEncoder();
9594
+ return response.values.reduce((bytes, item) => bytes + (item.value === null ? 0 : encoder.encode(JSON.stringify(item.value)).byteLength), 0) <= stateReadLimits.maxBytes;
9595
+ }, "State values exceed the 1 MiB read limit.");
9583
9596
  //#endregion
9584
9597
  //#region src/flow/common/retry.ts
9585
9598
  var retryNodeTypes = [
@@ -9935,7 +9948,7 @@ var managedFields = object({
9935
9948
  revisionId: text
9936
9949
  }),
9937
9950
  httpRequestExecutorSchema,
9938
- dedupeExecutorSchema
9951
+ skipDuplicatesExecutorSchema
9939
9952
  ])
9940
9953
  });
9941
9954
  var managed = managedFields.transform(limitDecisionTask);
@@ -10598,7 +10611,7 @@ var nodeErrorSchema = {
10598
10611
  }
10599
10612
  };
10600
10613
  function supportsNodeErrorHandling(node) {
10601
- return supportsNodeRetry(node) || node.kind === "task" && "executor" in node.task && node.task.executor.kind === "dedupe";
10614
+ return supportsNodeRetry(node) || node.kind === "task" && "executor" in node.task && node.task.executor.kind === "skip-duplicates";
10602
10615
  }
10603
10616
  function nodeSystemOutputPorts(node) {
10604
10617
  return supportsNodeErrorHandling(node) && node.onError == "error" ? { error: {
@@ -11997,6 +12010,8 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
11997
12010
  const changes = yield* Queue.unbounded();
11998
12011
  let remainingMs = context.remainingMs;
11999
12012
  let measuredAt = yield* Clock.currentTimeMillis;
12013
+ let nextCallCheckAt = measuredAt + (resume == null ? 1e3 : 0);
12014
+ let readyWorkflowRunIds = resume?.readyWorkflowRunIds;
12000
12015
  let idleSince;
12001
12016
  const agents = { ...resume?.checkpoint.agents };
12002
12017
  const resolveInputs = (nodeId, node) => resolveNodeInputs(nodeId, node, dependencies, context.bindingValues, launch[nodeId]);
@@ -12214,10 +12229,17 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
12214
12229
  return yield* Effect.failCause(firstCause);
12215
12230
  }
12216
12231
  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));
12232
+ if (pendingCalls.size > 0 && now >= nextCallCheckAt) {
12233
+ for (const call of pendingCalls.values()) {
12234
+ if (readyWorkflowRunIds != null && !readyWorkflowRunIds.has(call.childRunId)) continue;
12235
+ yield* observe(call.nodeId, call.jobId, Effect.gen(function* () {
12236
+ const result = yield* execute(call.nodeId, target.graph.nodes[call.nodeId], call.jobId, call.input);
12237
+ yield* settleNode(call.nodeId, call.jobId, result);
12238
+ })).pipe(Effect.catch(() => Effect.void));
12239
+ }
12240
+ readyWorkflowRunIds = void 0;
12241
+ nextCallCheckAt = (yield* Clock.currentTimeMillis) + 1e3;
12242
+ }
12221
12243
  const ready = dependencies.ready();
12222
12244
  for (const nodeId of ready) executeReady(nodeId);
12223
12245
  const running = yield* FiberSet.size(active);
@@ -12252,7 +12274,7 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
12252
12274
  const activity = Queue.take(changes).pipe(Effect.as({}));
12253
12275
  const decision = pendingWaits.size == 0 || context.waits == null ? Effect.never : context.waits.resolutions([...pendingWaits.values()].map((wait) => wait.waitId), true);
12254
12276
  let next = Effect.raceFirst(activity, decision);
12255
- if (pendingCalls.size > 0) next = Effect.raceFirst(next, Effect.sleep(1e3).pipe(Effect.as({})));
12277
+ if (pendingCalls.size > 0) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, nextCallCheckAt - (yield* Clock.currentTimeMillis))).pipe(Effect.as({})));
12256
12278
  if (left != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, left)).pipe(Effect.as({})));
12257
12279
  applyResolutions(yield* next, yield* Clock.currentTimeMillis);
12258
12280
  }
@@ -12290,6 +12312,8 @@ function runFlow(prepared, options) {
12290
12312
  return Effect.gen(function* () {
12291
12313
  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
12314
  const decoded = options.resume == null ? void 0 : decodeFlowRunCheckpoint(options.resume.checkpoint);
12315
+ const readyWorkflowRunIds = options.resume?.readyWorkflowRunIds === void 0 ? void 0 : new Set(array(string$1().min(1)).parse(options.resume.readyWorkflowRunIds));
12316
+ 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
12317
  const outputs = (nodeId, value) => isResolutionNode(prepared.graph.nodes[nodeId]) && value.pending != null ? {
12294
12318
  ...value,
12295
12319
  pending: withWaitNotificationMessage(value.pending)
@@ -12340,7 +12364,10 @@ function runFlow(prepared, options) {
12340
12364
  inputDefs: prepared.inputDefs,
12341
12365
  outputDefs: prepared.outputDefs,
12342
12366
  outputMappings: prepared.outputMappings
12343
- }, options.runId, options.inputs, trigger, checkpoint == null ? void 0 : { checkpoint });
12367
+ }, options.runId, options.inputs, trigger, checkpoint == null ? void 0 : {
12368
+ checkpoint,
12369
+ readyWorkflowRunIds
12370
+ });
12344
12371
  });
12345
12372
  }
12346
12373
  //#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/dist/index.d.ts CHANGED
@@ -104,6 +104,7 @@ export interface TaskContext<Actions extends object = Record<string, never>, Wor
104
104
  readonly blockId: string;
105
105
  readonly state: {
106
106
  get(key: string): Promise<JsonValue | null>;
107
+ get(keys: readonly string[]): Promise<(JsonValue | null)[]>;
107
108
  set(key: string, value: JsonValue): Promise<void>;
108
109
  insert(key: string, value: Exclude<JsonValue, null>): Promise<boolean>;
109
110
  delete(key: string): Promise<void>;
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.79",
4
4
  "description": "TypeScript contracts and Browser runtime for Open Flow.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -1,8 +1,8 @@
1
1
  import type { CanvasCommand } from '../../canvas/common/commands.ts'
2
- import type { FlowStateView, StateScope } from '../../state/common/state.ts'
2
+ import type { FlowStateValues, FlowStateView, StateScope } from '../../state/common/state.ts'
3
3
 
4
4
  import { currentEngineContract } from '../../runtime/common/engineContract.ts'
5
- import { flowStateViewSchema } from '../../state/common/state.ts'
5
+ import { flowStateValuesSchema, flowStateViewSchema } from '../../state/common/state.ts'
6
6
  import { workflowContractSchema, workflowReferencesSchema } from '../../workflow/common/workflow.ts'
7
7
  export type { CanvasCommand, CanvasCommandSubscriber } from '../../canvas/common/commands.ts'
8
8
  import type { LiveDetails } from './liveDetails.ts'
@@ -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
  }),
@@ -1000,11 +1017,23 @@ export class ControlClient {
1000
1017
  return flow(await this.request(`/v1/flows/${segment(flowId)}`, { method: 'PATCH', body: JSON.stringify({ ...changes, version: 1 }) }))
1001
1018
  }
1002
1019
 
1003
- async getFlowState(flowId: string, scope: StateScope, values = false): Promise<FlowStateView> {
1004
- const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}&values=${values}`))
1020
+ async getFlowState(flowId: string, scope: StateScope): Promise<FlowStateView> {
1021
+ const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}`))
1005
1022
  return result.success && result.data.scope === scope ? result.data : invalidResponse()
1006
1023
  }
1007
1024
 
1025
+ async getFlowStateValues(flowId: string, scope: StateScope, keys: readonly string[], signal?: AbortSignal): Promise<FlowStateValues> {
1026
+ const result = flowStateValuesSchema.safeParse(
1027
+ await this.request(`/v1/flows/${segment(flowId)}/state/read`, { method: 'POST', body: JSON.stringify({ version: 1, scope, keys }), signal }),
1028
+ )
1029
+ return result.success &&
1030
+ result.data.scope === scope &&
1031
+ result.data.values.length === keys.length &&
1032
+ result.data.values.every((item, index) => item.key === keys[index])
1033
+ ? result.data
1034
+ : invalidResponse()
1035
+ }
1036
+
1008
1037
  async clearFlowState(flowId: string, scope: StateScope, key?: string): Promise<void> {
1009
1038
  const source = record(
1010
1039
  await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}${key == null ? '' : `&key=${encodeURIComponent(key)}`}`, { method: 'DELETE' }),
@@ -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' } } },
@@ -205,6 +205,37 @@ function liveRunRequest(harness: ControlApiConformanceHarness, publicationId: st
205
205
  }
206
206
 
207
207
  export const controlApiConformanceCases: readonly ControlApiConformanceCase[] = [
208
+ {
209
+ name: 'reads State metadata and bounded explicit keys without scope-wide value retrieval',
210
+ async verify(harness) {
211
+ const api = new ControlClient((url, init) => harness.request(new Request(new URL(url, harness.origin), init)))
212
+ const flow = await api.createFlow('State reads')
213
+ equal((await api.getFlowState(flow.flowId, 'draft')).values, [], 'New Flow State metadata')
214
+ equal(
215
+ await api.getFlowStateValues(flow.flowId, 'live', ['second', 'first']),
216
+ {
217
+ version: 1,
218
+ scope: 'live',
219
+ values: [
220
+ { key: 'second', value: null },
221
+ { key: 'first', value: null },
222
+ ],
223
+ },
224
+ 'Missing named State keys',
225
+ )
226
+ const metadata = await request(harness, `/v1/flows/${flow.flowId}/state?scope=draft&values=false`)
227
+ await json(metadata, 200, 'Legacy metadata-only query')
228
+ equal(metadata.headers.get('cache-control'), 'no-store', 'State metadata cache policy')
229
+ await error(await request(harness, `/v1/flows/${flow.flowId}/state?scope=draft&values=true`), 400, 'flow.invalid', 'Scope-wide State values')
230
+ for (const input of [{ scope: 'draft', keys: [] }, { keys: ['key'] }, { scope: 'draft', keys: Array.from({ length: 17 }, (_, index) => String(index)) }])
231
+ await error(
232
+ await request(harness, `/v1/flows/${flow.flowId}/state/read`, { method: 'POST', body: JSON.stringify({ version: 1, ...input }) }),
233
+ 400,
234
+ 'flow.invalid',
235
+ 'Invalid named State read',
236
+ )
237
+ },
238
+ },
208
239
  {
209
240
  name: 'imports workflow files atomically and exports a new unpublished draft with its Presentation',
210
241
  async verify(harness) {
@@ -5,7 +5,7 @@ import { canvasCommandSchema } from '../../canvas/common/commands.ts'
5
5
  import { runStatuses } from '../../execution/common/runLifecycle.ts'
6
6
  import { waitCommentSchema } from '../../execution/common/wait.ts'
7
7
  import { resourceNameIssue } from '../../flow/common/change.ts'
8
- import { stateQuerySchema, stateClearSchema, dedupeClearSchema } from '../../state/common/state.ts'
8
+ import { stateQuerySchema, stateReadSchema, stateClearSchema, dedupeClearSchema } from '../../state/common/state.ts'
9
9
  import { authoringReadSchema, authoringSearchSchema, authoringRequestSchema, authoringSchemaQuery } from './authoringSchema.ts'
10
10
  import { resultQuerySchema } from './resultQuery.ts'
11
11
 
@@ -52,10 +52,15 @@ function tool<Args>(description: string, schema: z.ZodType<Args>, readOnly: bool
52
52
 
53
53
  export const mcpTools = {
54
54
  flow_state: tool(
55
- 'Inspect Flow state usage, business keys and deduplication spaces. Values are omitted unless requested. Scope is required.',
55
+ 'Inspect Flow state usage, business key metadata and deduplication spaces. Never returns values. Scope is required.',
56
56
  stateQuerySchema.extend({ flowId: flow }),
57
57
  true,
58
58
  ),
59
+ flow_state_read: tool(
60
+ 'Read values for 1–16 distinct named State keys in one explicit scope. Values are returned in request order; missing keys return null. The combined stored JSON limit is 1 MiB; oversized reads fail without partial results.',
61
+ stateReadSchema.extend({ flowId: flow }),
62
+ true,
63
+ ),
59
64
  flow_state_clear: tool(
60
65
  'Delete one business state key or clear all business values. Explicit draft/live scope is required; live changes production state immediately.',
61
66
  stateClearSchema.extend({ flowId: flow }),
@@ -10,6 +10,7 @@ import { decodeDraftOperations, draftOperationsSchema } from './draftOperations.
10
10
  export { decodeDraftOperations, draftOperationsSchema, resolveDraftOperations, type DraftOperation } from './draftOperations.ts'
11
11
  import { z } from 'zod'
12
12
  import { resourceNameIssue } from '../../flow/common/change.ts'
13
+ import { stateReadSchema } from '../../state/common/state.ts'
13
14
  import { createEventSourceSchema, updateEventSourceSchema, eventSourceRevisionSchema } from './eventSourceSchemas.ts'
14
15
 
15
16
  export const waitActionBodySchema: { parse(value: unknown): { comment?: string | null } } = z.strictObject({ comment: waitCommentSchema })
@@ -23,6 +24,7 @@ const tagName = id.transform((name) => name.trim()).refine((name) => resourceNam
23
24
  const inputs = z.record(z.string(), z.record(z.string(), json))
24
25
  const trigger = z.strictObject({ nodeId: id, outputs: z.record(z.string(), json) })
25
26
  const schemas = {
27
+ readFlowState: stateReadSchema.extend({ version }),
26
28
  canvas: z.strictObject({ command: canvasCommandSchema, version }),
27
29
  authoringCheck: authoringCheckSchema,
28
30
  authoringEdit: authoringRequestSchema,
@@ -34,7 +36,13 @@ const schemas = {
34
36
  updateEventSource: updateEventSourceSchema,
35
37
  eventSourceRevision: eventSourceRevisionSchema,
36
38
  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 }),
39
+ createFlow: z.strictObject({
40
+ name: flowName,
41
+ teamId: id.optional(),
42
+ tagIds: tagIds.optional(),
43
+ presentation: z.record(z.string(), json).optional(),
44
+ version,
45
+ }),
38
46
  saveTag: z.strictObject({ name: tagName, version }),
39
47
  setFlowTags: z.strictObject({ tagIds, version }),
40
48
  updateFlow: z.union([z.strictObject({ name: flowName, tagIds: tagIds.optional(), version }), z.strictObject({ name: flowName.optional(), tagIds, version })]),
@@ -72,6 +80,7 @@ function decoder<Value>(schema: z.ZodType<Value>): (value: unknown) => Value {
72
80
  }
73
81
 
74
82
  export const controlRequests = {
83
+ readFlowState: decoder(schemas.readFlowState),
75
84
  canvas: decoder(schemas.canvas),
76
85
  authoringCheck: decoder(schemas.authoringCheck),
77
86
  authoringEdit: decoder(schemas.authoringEdit),