@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.
- package/README.md +2 -0
- package/dist/browser/{ContentIcon-VZQO8VZU.js → ContentIcon-CMn0c-kl.js} +93 -3
- package/dist/browser/{FlowCanvas-BNGDMaWr.js → FlowCanvas-BQGBcfoL.js} +1675 -591
- package/dist/browser/{createResourceDialog-BmtakfVg.js → createResourceDialog-DkovGogv.js} +3 -3
- package/dist/browser/{dialog-BcFBfRB1.js → dialog-CFFuRM8s.js} +37 -86
- package/dist/browser/{exportSvg-CG8hEUNk.js → exportSvg-CYwyYiuH.js} +2 -2
- package/dist/browser/{flowChanges-BGo_4Ypo.js → flowChanges-CN2R-m1M.js} +1258 -422
- package/dist/browser/{flowWorkspace-BFHGev7s.js → flowWorkspace-CXVL1FNv.js} +634 -628
- package/dist/browser/licenses.md +1 -1
- package/dist/browser/preview.css +512 -462
- package/dist/browser/preview.js +8183 -6384
- package/dist/browser/{tooltip-BAWSUexx.js → tooltip-BDcVTNpu.js} +65 -2
- package/dist/browser/{typeScriptSession-CRoezJPN.js → typeScriptSession-CoEIptNC.js} +1 -1
- package/dist/browser/ui.css +5 -24
- package/dist/browser/workbench.css +512 -462
- package/dist/browser/workbench.d.ts +2 -3
- package/dist/browser/workbench.js +65 -690
- package/dist/browser/{workbenchSelect-CjK-xdm3.js → workbenchSelect-CkOS95gX.js} +2 -2
- package/dist/command/index.js +31 -31
- package/dist/common/authoring.d.ts +3 -3
- package/dist/common/authoringExamples.d.ts +3 -3
- package/dist/common/authoringSchema.d.ts +14 -22
- package/dist/common/control-api-conformance.js +91 -25
- package/dist/common/control-api.d.ts +8 -3
- package/dist/common/control-api.js +51 -25
- package/dist/common/control-requests.d.ts +9 -3
- package/dist/common/control-requests.js +54 -42
- package/dist/common/flow-authoring.js +32 -19
- package/dist/common/flow-change.d.ts +1 -1
- package/dist/common/flow-change.js +32 -19
- package/dist/common/flow-encoding.js +31 -18
- package/dist/common/flow-semantics.js +41 -26
- package/dist/common/flow-state.d.ts +49 -43
- package/dist/common/flow-state.js +95 -110
- package/dist/common/mcp.d.ts +35 -4
- package/dist/common/mcp.js +51 -26
- package/dist/common/run-events.js +19 -9
- package/dist/common/runtime-contract.d.ts +4 -1
- package/dist/common/runtime-contract.js +66 -17
- package/dist/common/runtime-nodejs-compat.js +11 -12
- package/dist/common/scheduler.d.ts +2 -1
- package/dist/common/scheduler.js +82 -55
- package/dist/common/state-conformance.d.ts +3 -3
- package/dist/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/control/common/api.ts +36 -7
- package/src/control/common/authoring.ts +6 -5
- package/src/control/common/authoringExamples.ts +6 -5
- package/src/control/common/authoringSchema.ts +7 -7
- package/src/control/common/conformance.ts +31 -0
- package/src/control/common/mcp.ts +7 -2
- package/src/control/common/requests.ts +10 -1
- package/src/execution/common/events.ts +22 -10
- package/src/execution/common/node-execution.ts +1 -1
- package/src/execution/common/scheduler.ts +28 -14
- package/src/flow/common/change.ts +1 -1
- package/src/flow/common/changeSchema.ts +4 -4
- package/src/flow/common/errorHandling.ts +1 -1
- package/src/flow/common/semantics.ts +12 -6
- package/src/runtime/common/nodejsGuest.generated.json +11 -12
- package/src/runtime/common/runtime.ts +45 -12
- package/src/state/common/conformance.ts +32 -21
- package/src/state/common/state.ts +85 -80
- 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' | '
|
|
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;
|
package/dist/common/scheduler.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
9576
|
-
|
|
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
|
-
|
|
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 === "
|
|
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
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
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(
|
|
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 : {
|
|
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 {
|
|
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
|
|
8
|
-
|
|
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,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 !== '
|
|
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(
|
|
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({
|
|
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
|
|
1004
|
-
const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}
|
|
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 {
|
|
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 == '
|
|
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 == '
|
|
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 '
|
|
780
|
-
changes = createManagedTask(identity,
|
|
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 {
|
|
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
|
-
'
|
|
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 '
|
|
63
|
+
case 'skip-duplicates':
|
|
63
64
|
operations = [
|
|
64
|
-
...createManagedTask({ nodeId: '
|
|
65
|
-
{ kind: 'graph.node.input.set', nodeId: '
|
|
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 {
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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 == '
|
|
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 == '
|
|
313
|
-
? 'Bind an items array. Configure
|
|
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
|
-
'
|
|
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
|
|
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({
|
|
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),
|