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

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 (71) hide show
  1. package/README.md +1 -0
  2. package/dist/browser/{ContentIcon-Cm1hbVzW.js → ContentIcon-VZQO8VZU.js} +8 -5
  3. package/dist/browser/{FlowCanvas-NzKtla_c.js → FlowCanvas-BNGDMaWr.js} +15 -3
  4. package/dist/browser/{createResourceDialog-BgdedQqu.js → createResourceDialog-BmtakfVg.js} +63 -57
  5. package/dist/browser/{dialog-aSElFFw2.js → dialog-BcFBfRB1.js} +84 -15
  6. package/dist/browser/{exportSvg-Dcz4HTo_.js → exportSvg-CG8hEUNk.js} +2 -2
  7. package/dist/browser/{flowChanges-NZyS7HSp.js → flowChanges-BGo_4Ypo.js} +5849 -10882
  8. package/dist/browser/{flowWorkspace-Bl0SDehy.js → flowWorkspace-BFHGev7s.js} +1025 -2607
  9. package/dist/browser/preview.css +112 -5
  10. package/dist/browser/preview.js +1630 -9295
  11. package/dist/browser/theme.css +4 -2
  12. package/dist/browser/{tooltip-3XMK9jkr.js → tooltip-BAWSUexx.js} +1 -1
  13. package/dist/browser/{typeScriptSession-C9sBGOKZ.js → typeScriptSession-CRoezJPN.js} +1 -1
  14. package/dist/browser/ui.css +111 -4
  15. package/dist/browser/ui.js +3 -2
  16. package/dist/browser/workbench.css +9906 -9778
  17. package/dist/browser/workbench.d.ts +6 -2
  18. package/dist/browser/workbench.js +838 -499
  19. package/dist/browser/{workbenchSelect-DBGP7Y4Z.js → workbenchSelect-CjK-xdm3.js} +2 -2
  20. package/dist/command/index.js +48 -48
  21. package/dist/common/authoring.d.ts +4 -4
  22. package/dist/common/authoringExamples.d.ts +3 -2
  23. package/dist/common/authoringSchema.d.ts +23 -4
  24. package/dist/common/control-api-conformance.js +162 -15
  25. package/dist/common/control-api-errors.d.ts +12 -0
  26. package/dist/common/control-api.d.ts +35 -2
  27. package/dist/common/control-api.js +187 -37
  28. package/dist/common/control-requests.d.ts +22 -3
  29. package/dist/common/control-requests.js +152 -14
  30. package/dist/common/engine-contract.d.ts +3 -2
  31. package/dist/common/flow-authoring.js +55 -7
  32. package/dist/common/flow-change-schema.d.ts +1 -1
  33. package/dist/common/flow-change.d.ts +2 -2
  34. package/dist/common/flow-change.js +56 -8
  35. package/dist/common/flow-encoding.js +55 -7
  36. package/dist/common/flow-semantics.js +91 -7
  37. package/dist/common/flow-state.d.ts +123 -0
  38. package/dist/common/flow-state.js +5852 -0
  39. package/dist/common/mcp.d.ts +96 -3
  40. package/dist/common/mcp.js +165 -13
  41. package/dist/common/run-events.js +1 -0
  42. package/dist/common/runtime-contract.d.ts +14 -0
  43. package/dist/common/runtime-contract.js +118 -5
  44. package/dist/common/scheduler-node-execution.d.ts +1 -0
  45. package/dist/common/scheduler.d.ts +1 -1
  46. package/dist/common/scheduler.js +52 -3
  47. package/dist/common/state-conformance.d.ts +16 -0
  48. package/dist/common/tags.d.ts +4 -0
  49. package/dist/index.d.ts +11 -0
  50. package/package.json +5 -1
  51. package/src/control/common/api.ts +84 -11
  52. package/src/control/common/authoring.ts +6 -1
  53. package/src/control/common/authoringExamples.ts +8 -0
  54. package/src/control/common/authoringSchema.ts +18 -12
  55. package/src/control/common/errors.ts +6 -0
  56. package/src/control/common/flowDecoders.ts +1 -0
  57. package/src/control/common/mcp.ts +16 -0
  58. package/src/control/common/requests.ts +10 -2
  59. package/src/control/common/tags.ts +20 -0
  60. package/src/execution/common/events.ts +3 -0
  61. package/src/execution/common/node-execution.ts +3 -0
  62. package/src/execution/common/scheduler.ts +2 -0
  63. package/src/flow/common/change.ts +2 -1
  64. package/src/flow/common/changeSchema.ts +11 -1
  65. package/src/flow/common/errorHandling.ts +1 -1
  66. package/src/flow/common/semantics.ts +20 -0
  67. package/src/runtime/common/engineContract.ts +6 -3
  68. package/src/runtime/common/runtime.ts +116 -0
  69. package/src/state/common/conformance.ts +86 -0
  70. package/src/state/common/state.ts +169 -0
  71. package/src/types/index.ts +11 -0
@@ -9535,6 +9535,51 @@ 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 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") });
9555
+ object({
9556
+ version: literal(1),
9557
+ scope: _enum(["draft", "live"]),
9558
+ limits: object({
9559
+ maxValueBytes: number().positive(),
9560
+ maxValues: number().int().positive(),
9561
+ maxSpaces: number().int().positive(),
9562
+ maxIdentities: number().positive(),
9563
+ maxRunWrites: number().positive()
9564
+ }),
9565
+ values: array(object({
9566
+ key: string$1(),
9567
+ size: number().nonnegative(),
9568
+ updatedAt: number(),
9569
+ value: json$1().optional()
9570
+ })),
9571
+ spaces: array(object({
9572
+ space: string$1(),
9573
+ mode: _enum(["new", "increasing"]),
9574
+ count: number().nonnegative(),
9575
+ watermark: union([
9576
+ string$1(),
9577
+ number(),
9578
+ _null()
9579
+ ]),
9580
+ updatedAt: number()
9581
+ }))
9582
+ });
9538
9583
  //#endregion
9539
9584
  //#region src/flow/common/retry.ts
9540
9585
  var retryNodeTypes = [
@@ -9889,7 +9934,8 @@ var managedFields = object({
9889
9934
  flowId: text,
9890
9935
  revisionId: text
9891
9936
  }),
9892
- httpRequestExecutorSchema
9937
+ httpRequestExecutorSchema,
9938
+ dedupeExecutorSchema
9893
9939
  ])
9894
9940
  });
9895
9941
  var managed = managedFields.transform(limitDecisionTask);
@@ -10143,7 +10189,8 @@ object({
10143
10189
  literal(9),
10144
10190
  literal(10),
10145
10191
  literal(11),
10146
- literal(12)
10192
+ literal(12),
10193
+ literal(13)
10147
10194
  ]),
10148
10195
  document,
10149
10196
  modules: record$1(text, module)
@@ -10551,7 +10598,7 @@ var nodeErrorSchema = {
10551
10598
  }
10552
10599
  };
10553
10600
  function supportsNodeErrorHandling(node) {
10554
- return supportsNodeRetry(node);
10601
+ return supportsNodeRetry(node) || node.kind === "task" && "executor" in node.task && node.task.executor.kind === "dedupe";
10555
10602
  }
10556
10603
  function nodeSystemOutputPorts(node) {
10557
10604
  return supportsNodeErrorHandling(node) && node.onError == "error" ? { error: {
@@ -11580,6 +11627,7 @@ function executeNode(context, input) {
11580
11627
  if (additional.has(handle)) return false;
11581
11628
  const task = "executor" in node.task ? node.task : void 0;
11582
11629
  if (task == null || !["openapi", "http-request"].includes(task.executor.kind)) return true;
11630
+ if (Object.hasOwn(input.launchInputs ?? {}, handle)) return true;
11583
11631
  const mapping = node.inputs[handle];
11584
11632
  return mapping?.kind != "unset" && (mapping != null || task.inputs.some((port) => "handle" in port && port.handle == handle && port.value !== void 0));
11585
11633
  })),
@@ -11994,6 +12042,7 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
11994
12042
  node,
11995
12043
  jobId,
11996
12044
  nodeInputs,
12045
+ launchInputs: launch[nodeId],
11997
12046
  resolution,
11998
12047
  flowId: target.flowId,
11999
12048
  runId,
@@ -0,0 +1,16 @@
1
+ import type { JsonValue } from './flow-model.js';
2
+ import type { DedupeConfig } 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 dedupe node across Runs, updating its business config and items. */
8
+ dedupe(config: DedupeConfig, 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.77",
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
@@ -677,6 +703,7 @@ export function decodeRunEvent(value: unknown) {
677
703
  nodeKind !== 'decision' &&
678
704
  nodeKind !== 'openapi' &&
679
705
  nodeKind !== 'http-request' &&
706
+ nodeKind !== 'dedupe' &&
680
707
  nodeKind !== 'llm' &&
681
708
  nodeKind !== 'agent' &&
682
709
  nodeKind !== 'value' &&
@@ -809,9 +836,13 @@ export class ControlClient {
809
836
  }
810
837
 
811
838
  async listFlows(
812
- options: { readonly cursor?: string; readonly includeTotal?: boolean; readonly limit?: number; readonly signal?: AbortSignal } = {},
839
+ options: FlowQuery & { readonly cursor?: string; readonly includeTotal?: boolean; readonly limit?: number; readonly signal?: AbortSignal } = {},
813
840
  ): Promise<FlowPage> {
814
841
  const parameters = new URLSearchParams()
842
+ const filter = normalizeFlowQuery(options)
843
+ if (filter.query) parameters.set('query', filter.query)
844
+ if (filter.tagIds.length) parameters.set('tagIds', filter.tagIds.join(','))
845
+ if (filter.untagged) parameters.set('untagged', 'true')
815
846
  if (options.cursor != null) parameters.set('cursor', options.cursor)
816
847
  if (options.limit != null) parameters.set('limit', String(options.limit))
817
848
  if (options.includeTotal != null) parameters.set('includeTotal', String(options.includeTotal))
@@ -897,20 +928,23 @@ export class ControlClient {
897
928
  }
898
929
  }
899
930
 
900
- async importFlow(file: WorkflowFile, options: { readonly name?: string; readonly teamId?: string; readonly idempotencyKey?: string } = {}): Promise<Flow> {
931
+ async importFlow(
932
+ file: WorkflowFile,
933
+ options: { readonly name?: string; readonly teamId?: string; readonly idempotencyKey?: string; readonly tagIds?: readonly string[] } = {},
934
+ ): Promise<Flow> {
901
935
  return flow(
902
936
  await this.request('/v1/flows/import', {
903
937
  method: 'POST',
904
938
  headers: { 'Idempotency-Key': options.idempotencyKey ?? `flow-${randomId()}` },
905
- body: JSON.stringify({ file, name: options.name, teamId: options.teamId, version: 1 }),
939
+ body: JSON.stringify({ file, name: options.name, teamId: options.teamId, tagIds: options.tagIds, version: 1 }),
906
940
  }),
907
941
  )
908
942
  }
909
943
 
910
- async createFlow(name: string, idempotencyKey = `flow-${randomId()}`, teamId?: string): Promise<Flow> {
944
+ async createFlow(name: string, idempotencyKey = `flow-${randomId()}`, teamId?: string, tagIds?: readonly string[]): Promise<Flow> {
911
945
  return flow(
912
946
  await this.request('/v1/flows', {
913
- body: JSON.stringify({ name, ...(teamId == null ? {} : { teamId }), version: 1 }),
947
+ body: JSON.stringify({ name, ...(teamId == null ? {} : { teamId }), ...(tagIds == null ? {} : { tagIds }), version: 1 }),
914
948
  headers: { 'idempotency-key': idempotencyKey },
915
949
  method: 'POST',
916
950
  }),
@@ -939,13 +973,52 @@ export class ControlClient {
939
973
  return source.listeners.map(errorListener)
940
974
  }
941
975
 
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
- }),
976
+ async listTags(signal?: AbortSignal): Promise<{ readonly tags: readonly TagListItem[]; readonly version: 1 }> {
977
+ const source = record(await this.request('/v1/tags', { signal }))
978
+ if (source.version != 1 || !Array.isArray(source.tags)) return invalidResponse()
979
+ return { tags: source.tags.map(decodeTagListItem), version: 1 }
980
+ }
981
+
982
+ async createTag(name: string): Promise<Tag> {
983
+ return decodeTag(await this.request('/v1/tags', { method: 'POST', body: JSON.stringify({ name, version: 1 }) }))
984
+ }
985
+
986
+ async renameTag(tagId: string, name: string): Promise<Tag> {
987
+ return decodeTag(await this.request(`/v1/tags/${segment(tagId)}`, { method: 'PATCH', body: JSON.stringify({ name, version: 1 }) }))
988
+ }
989
+
990
+ async deleteTag(tagId: string): Promise<void> {
991
+ const source = record(await this.request(`/v1/tags/${segment(tagId)}`, { method: 'DELETE' }))
992
+ if (source.version != 1) invalidResponse()
993
+ }
994
+
995
+ async setFlowTags(flowId: string, tagIds: readonly string[]): Promise<Flow> {
996
+ return flow(await this.request(`/v1/flows/${segment(flowId)}/tags`, { method: 'PUT', body: JSON.stringify({ tagIds, version: 1 }) }))
997
+ }
998
+
999
+ async updateFlow(flowId: string, changes: FlowUpdate): Promise<Flow> {
1000
+ return flow(await this.request(`/v1/flows/${segment(flowId)}`, { method: 'PATCH', body: JSON.stringify({ ...changes, version: 1 }) }))
1001
+ }
1002
+
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}`))
1005
+ return result.success && result.data.scope === scope ? result.data : invalidResponse()
1006
+ }
1007
+
1008
+ async clearFlowState(flowId: string, scope: StateScope, key?: string): Promise<void> {
1009
+ const source = record(
1010
+ await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}${key == null ? '' : `&key=${encodeURIComponent(key)}`}`, { method: 'DELETE' }),
948
1011
  )
1012
+ if (source.version !== 1 || source.cleared !== true) invalidResponse()
1013
+ }
1014
+
1015
+ async clearFlowDedupe(flowId: string, scope: StateScope, space: string): Promise<void> {
1016
+ const source = record(await this.request(`/v1/flows/${segment(flowId)}/dedupe?scope=${scope}&space=${encodeURIComponent(space)}`, { method: 'DELETE' }))
1017
+ if (source.version !== 1 || source.cleared !== true) invalidResponse()
1018
+ }
1019
+
1020
+ async renameFlow(flowId: string, name: string): Promise<Flow> {
1021
+ return this.updateFlow(flowId, { name })
949
1022
  }
950
1023
 
951
1024
  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 { dedupeConfigSchema, dedupeTask } 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 == 'dedupe') {
150
151
  const { kind: _kind, ...settings } = task.executor
151
152
  config = settings
152
153
  } else if (task.executor.kind == 'openapi') {
@@ -521,6 +522,7 @@ 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 == 'dedupe') task = { ...before, name: String(update.name), executor: { kind: 'dedupe', ...dedupeConfigSchema.parse(config) } }
524
526
  else if (type == 'http-request') {
525
527
  task = httpRequestTask(httpRequestConfigSchema.parse(config), String(update.name), before)
526
528
  next = { ...next, inputs: httpRequestInputs(node.inputs, before, task) } as GraphNode
@@ -774,6 +776,9 @@ export async function compileAuthoring(base: RevisionContent, request: Authoring
774
776
  case 'connector':
775
777
  changes = createManagedTask(identity, await connectorTask(config, name))
776
778
  break
779
+ case 'dedupe':
780
+ changes = createManagedTask(identity, dedupeTask(dedupeConfigSchema.parse(config), name))
781
+ break
777
782
  case 'http-request':
778
783
  changes = createManagedTask(identity, httpRequestTask(httpRequestConfigSchema.parse(config), name))
779
784
  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 { dedupeTask } 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,7 @@ 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
+ 'dedupe': 'Filter items using persistent history. Records immediately, including when downstream work fails. Empty field uses each scalar item as identity.',
26
28
  'http-request': 'A single HTTP request. Bind url and request inputs; outputs are body, statusCode and headers.',
27
29
  'openapi': 'A fixed JSON API operation. Replace the example document and URL before running.',
28
30
  'connector':
@@ -57,6 +59,12 @@ export function authoringExample(name: string): { version: 1; operations: readon
57
59
  { name: 'needs_support', type: 'noul', instructions: 'Does the customer need support?' },
58
60
  ])
59
61
  break
62
+ case 'dedupe':
63
+ operations = [
64
+ ...createManagedTask({ nodeId: 'dedupe' }, dedupeTask()),
65
+ { kind: 'graph.node.input.set', nodeId: 'dedupe', handle: 'items', value: { kind: 'value', value: ['a', 'b', 'a'] } },
66
+ ]
67
+ break
60
68
  case 'http-request':
61
69
  operations = [
62
70
  ...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 { dedupeConfigSchema } 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
+ 'dedupe',
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
+ 'dedupe': dedupeConfigSchema,
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 == 'dedupe' || 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 == '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.'
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
+ 'dedupe': { config: { field: 'id', mode: 'new', space: 'node' }, 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' } } },
@@ -42,6 +42,9 @@ export const controlErrorCode = {
42
42
  triggerKeyInvalid: 'trigger-key.invalid',
43
43
  triggerKeyNotFound: 'trigger-key.not-found',
44
44
  triggerNotFound: 'trigger.not-found',
45
+ tagInvalid: 'tag.invalid',
46
+ tagConflict: 'tag.conflict',
47
+ tagNotFound: 'tag.not-found',
45
48
  variableInvalid: 'variable.invalid',
46
49
  variableLimitReached: 'variable.limit-reached',
47
50
  variableNotFound: 'variable.not-found',
@@ -93,6 +96,9 @@ export const controlErrorMetadata = {
93
96
  [controlErrorCode.triggerKeyInvalid]: { status: 409 },
94
97
  [controlErrorCode.triggerKeyNotFound]: { status: 404 },
95
98
  [controlErrorCode.triggerNotFound]: { status: 404 },
99
+ [controlErrorCode.tagInvalid]: { status: 400 },
100
+ [controlErrorCode.tagConflict]: { status: 409 },
101
+ [controlErrorCode.tagNotFound]: { status: 404 },
96
102
  [controlErrorCode.variableInvalid]: { status: 400 },
97
103
  [controlErrorCode.variableLimitReached]: { status: 409 },
98
104
  [controlErrorCode.variableNotFound]: { status: 404 },
@@ -60,6 +60,7 @@ export function flow(value: unknown): Flow {
60
60
  revisionId: string(target.revisionId),
61
61
  },
62
62
  }),
63
+ tagIds: source.tagIds === undefined ? [] : strings(source.tagIds),
63
64
  createdAt: string(source.createdAt),
64
65
  ...(source.connectorTeamId == null ? {} : { connectorTeamId: string(source.connectorTeamId) }),
65
66
  draftRevisionId: string(source.draftRevisionId),
@@ -5,6 +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
9
  import { authoringReadSchema, authoringSearchSchema, authoringRequestSchema, authoringSchemaQuery } from './authoringSchema.ts'
9
10
  import { resultQuerySchema } from './resultQuery.ts'
10
11
 
@@ -50,6 +51,21 @@ function tool<Args>(description: string, schema: z.ZodType<Args>, readOnly: bool
50
51
  }
51
52
 
52
53
  export const mcpTools = {
54
+ flow_state: tool(
55
+ 'Inspect Flow state usage, business keys and deduplication spaces. Values are omitted unless requested. Scope is required.',
56
+ stateQuerySchema.extend({ flowId: flow }),
57
+ true,
58
+ ),
59
+ flow_state_clear: tool(
60
+ 'Delete one business state key or clear all business values. Explicit draft/live scope is required; live changes production state immediately.',
61
+ stateClearSchema.extend({ flowId: flow }),
62
+ false,
63
+ ),
64
+ flow_dedupe_clear: tool(
65
+ 'Clear one deduplication space (flow or node:<id>). Explicit draft/live scope is required; live changes production history immediately.',
66
+ dedupeClearSchema.extend({ flowId: flow }),
67
+ false,
68
+ ),
53
69
  flow_canvas: tool(
54
70
  'Broadcast a transient canvas command to connected Workbenches viewing this Flow. Success only confirms Server broadcast, not delivery or execution. No replay, session selection or Presentation save. Pan uses screen pixels (positive right/down); zoom is an absolute ratio (0.1–3); fit includes all nodes. Repeating pan moves again.',
55
71
  z.strictObject({ flowId: flow, command: canvasCommandSchema }),
@@ -18,6 +18,8 @@ const json: z.ZodType<JsonValue> = z.json()
18
18
  const id = z.string().min(1)
19
19
  const version = z.literal(1)
20
20
  const flowName = id.refine((value) => value == value.trim() && resourceNameIssue(value) == null, 'Flow name is invalid.')
21
+ const tagIds = z.array(id).transform((ids) => [...new Set(ids)].toSorted())
22
+ const tagName = id.transform((name) => name.trim()).refine((name) => resourceNameIssue(name) == null, 'Tag name is invalid.')
21
23
  const inputs = z.record(z.string(), z.record(z.string(), json))
22
24
  const trigger = z.strictObject({ nodeId: id, outputs: z.record(z.string(), json) })
23
25
  const schemas = {
@@ -31,8 +33,11 @@ const schemas = {
31
33
  createEventSource: createEventSourceSchema,
32
34
  updateEventSource: updateEventSourceSchema,
33
35
  eventSourceRevision: eventSourceRevisionSchema,
34
- importFlow: z.strictObject({ file: json, name: flowName.optional(), teamId: id.optional(), version }),
35
- createFlow: z.strictObject({ name: flowName, teamId: id.optional(), version }),
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 }),
38
+ saveTag: z.strictObject({ name: tagName, version }),
39
+ setFlowTags: z.strictObject({ tagIds, version }),
40
+ updateFlow: z.union([z.strictObject({ name: flowName, tagIds: tagIds.optional(), version }), z.strictObject({ name: flowName.optional(), tagIds, version })]),
36
41
  renameFlow: z.strictObject({ name: flowName, version }),
37
42
  changeDraft: z.strictObject({ expectedRevisionId: id, operations: z.array(json).min(1), version }),
38
43
  repairDraft: z.strictObject({ expectedRevisionId: id, version }),
@@ -79,6 +84,9 @@ export const controlRequests = {
79
84
  eventSourceRevision: decoder(schemas.eventSourceRevision),
80
85
  importFlow: decoder(schemas.importFlow),
81
86
  createFlow: decoder(schemas.createFlow),
87
+ saveTag: decoder(schemas.saveTag),
88
+ setFlowTags: decoder(schemas.setFlowTags),
89
+ updateFlow: decoder(schemas.updateFlow),
82
90
  renameFlow: decoder(schemas.renameFlow),
83
91
  changeDraft(value: unknown): { expectedRevisionId: string; operations: readonly DraftOperation[]; version: 1 } {
84
92
  const body = schemas.changeDraft.parse(value)
@@ -0,0 +1,20 @@
1
+ import type { FlowQuery, Tag, TagListItem } from './api.ts'
2
+
3
+ import { integer, invalidResponse, record, string } from './decoding.ts'
4
+
5
+ export function normalizeFlowQuery(query: FlowQuery = {}): Required<FlowQuery> {
6
+ return { query: query.query?.trim().toLowerCase() ?? '', tagIds: [...new Set(query.tagIds ?? [])].toSorted(), untagged: query.untagged ?? false }
7
+ }
8
+
9
+ export function decodeTag(value: unknown): Tag {
10
+ const source = record(value)
11
+ if (source.version != 1) return invalidResponse()
12
+ return { tagId: string(source.tagId), name: string(source.name), version: 1 }
13
+ }
14
+
15
+ export function decodeTagListItem(value: unknown): TagListItem {
16
+ const source = record(value)
17
+ const referenceCount = integer(source.referenceCount)
18
+ if (referenceCount < 0) return invalidResponse()
19
+ return { ...decodeTag(source), referenceCount }
20
+ }
@@ -23,6 +23,7 @@ const nodeKinds = new Set([
23
23
  'javascript',
24
24
  'openapi',
25
25
  'http-request',
26
+ 'dedupe',
26
27
  'llm',
27
28
  'value',
28
29
  'wait',
@@ -56,6 +57,7 @@ function nodeKind(
56
57
  | 'javascript'
57
58
  | 'openapi'
58
59
  | 'http-request'
60
+ | 'dedupe'
59
61
  | 'llm'
60
62
  | 'value'
61
63
  | 'wait'
@@ -76,6 +78,7 @@ function nodeKind(
76
78
  | 'javascript'
77
79
  | 'openapi'
78
80
  | 'http-request'
81
+ | 'dedupe'
79
82
  | 'llm'
80
83
  | 'value'
81
84
  | 'wait'
@@ -109,6 +109,7 @@ function nodeKind(
109
109
  | 'javascript'
110
110
  | 'openapi'
111
111
  | 'http-request'
112
+ | 'dedupe'
112
113
  | 'llm'
113
114
  | 'value'
114
115
  | 'wait'
@@ -301,6 +302,7 @@ export function executeNode(
301
302
  readonly node: ExecutableNode
302
303
  readonly jobId: string
303
304
  readonly nodeInputs: Readonly<Record<string, JsonValue>>
305
+ readonly launchInputs?: Readonly<Record<string, JsonValue>>
304
306
  readonly flowId: string
305
307
  readonly runId: string
306
308
  readonly graph: Graph
@@ -368,6 +370,7 @@ export function executeNode(
368
370
  if (additional.has(handle)) return false
369
371
  const task = 'executor' in node.task ? node.task : undefined
370
372
  if (task == null || !['openapi', 'http-request'].includes(task.executor.kind)) return true
373
+ if (Object.hasOwn(input.launchInputs ?? {}, handle)) return true
371
374
  const mapping = node.inputs[handle]
372
375
  return (
373
376
  mapping?.kind != 'unset' &&
@@ -70,6 +70,7 @@ export type SchedulerEvent =
70
70
  | 'javascript'
71
71
  | 'openapi'
72
72
  | 'http-request'
73
+ | 'dedupe'
73
74
  | 'llm'
74
75
  | 'value'
75
76
  | 'wait'
@@ -344,6 +345,7 @@ function runGraph(
344
345
  node,
345
346
  jobId,
346
347
  nodeInputs,
348
+ launchInputs: launch[nodeId],
347
349
  resolution,
348
350
  flowId: target.flowId,
349
351
  runId,