@oomol-lab/open-flow 0.1.0-beta.77 → 0.1.0-beta.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-DCGxfKFM.js} +3 -3
- package/dist/browser/{dialog-BcFBfRB1.js → dialog-WzR4CRFH.js} +11 -81
- package/dist/browser/{exportSvg-CG8hEUNk.js → exportSvg-CYwyYiuH.js} +2 -2
- package/dist/browser/{flowChanges-BGo_4Ypo.js → flowChanges-Bz4dbDN_.js} +1204 -387
- package/dist/browser/{flowWorkspace-BFHGev7s.js → flowWorkspace-BI6L6a4r.js} +557 -597
- package/dist/browser/licenses.md +1 -1
- package/dist/browser/preview.css +512 -462
- package/dist/browser/preview.js +8457 -6732
- package/dist/browser/{tooltip-BAWSUexx.js → tooltip-BDcVTNpu.js} +65 -2
- 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-DtAd0zc4.js} +2 -2
- package/dist/command/index.js +25 -25
- 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 +11 -18
- package/dist/common/control-api.d.ts +5 -1
- package/dist/common/control-api.js +11 -18
- package/dist/common/control-requests.d.ts +4 -3
- package/dist/common/control-requests.js +26 -37
- package/dist/common/flow-authoring.js +6 -14
- package/dist/common/flow-change.d.ts +1 -1
- package/dist/common/flow-change.js +6 -14
- package/dist/common/flow-encoding.js +5 -13
- package/dist/common/flow-semantics.js +15 -21
- package/dist/common/flow-state.d.ts +16 -39
- package/dist/common/flow-state.js +47 -105
- package/dist/common/mcp.d.ts +3 -3
- package/dist/common/mcp.js +11 -18
- package/dist/common/run-events.js +19 -9
- package/dist/common/runtime-contract.js +6 -12
- package/dist/common/runtime-nodejs-compat.js +11 -12
- package/dist/common/scheduler.d.ts +2 -1
- package/dist/common/scheduler.js +56 -50
- package/dist/common/state-conformance.d.ts +3 -3
- package/package.json +1 -1
- package/src/control/common/api.ts +20 -3
- 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/requests.ts +7 -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 +6 -7
- package/src/state/common/conformance.ts +16 -21
- package/src/state/common/state.ts +44 -72
|
@@ -33,7 +33,7 @@ export type SchedulerEvent = {
|
|
|
33
33
|
readonly inputs: Readonly<Record<string, JsonValue>>;
|
|
34
34
|
readonly jobId: string;
|
|
35
35
|
readonly nodeId: string;
|
|
36
|
-
readonly nodeKind: 'agent' | 'approval' | 'condition' | 'connector' | 'decision' | 'javascript' | 'openapi' | 'http-request' | '
|
|
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;
|
|
@@ -9547,11 +9547,7 @@ strictObject({
|
|
|
9547
9547
|
scope: _enum(["draft", "live"]),
|
|
9548
9548
|
space: string$1().regex(/^(flow|node:.+)$/)
|
|
9549
9549
|
});
|
|
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") });
|
|
9550
|
+
var skipDuplicatesExecutorSchema = strictObject({ fields: array(string$1().refine((field) => field.length > 0 && field.split(".").every((part) => part.length > 0), "Use a nonempty dot-separated field path.")).refine((fields) => new Set(fields).size === fields.length, "Identity fields must be distinct.").default([]) }).extend({ kind: literal("skip-duplicates") });
|
|
9555
9551
|
object({
|
|
9556
9552
|
version: literal(1),
|
|
9557
9553
|
scope: _enum(["draft", "live"]),
|
|
@@ -9570,13 +9566,9 @@ object({
|
|
|
9570
9566
|
})),
|
|
9571
9567
|
spaces: array(object({
|
|
9572
9568
|
space: string$1(),
|
|
9573
|
-
mode: _enum(["new", "increasing"]),
|
|
9574
9569
|
count: number().nonnegative(),
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
number(),
|
|
9578
|
-
_null()
|
|
9579
|
-
]),
|
|
9570
|
+
pendingCount: number().nonnegative(),
|
|
9571
|
+
reservedCount: number().nonnegative(),
|
|
9580
9572
|
updatedAt: number()
|
|
9581
9573
|
}))
|
|
9582
9574
|
});
|
|
@@ -9935,7 +9927,7 @@ var managedFields = object({
|
|
|
9935
9927
|
revisionId: text
|
|
9936
9928
|
}),
|
|
9937
9929
|
httpRequestExecutorSchema,
|
|
9938
|
-
|
|
9930
|
+
skipDuplicatesExecutorSchema
|
|
9939
9931
|
])
|
|
9940
9932
|
});
|
|
9941
9933
|
var managed = managedFields.transform(limitDecisionTask);
|
|
@@ -10598,7 +10590,7 @@ var nodeErrorSchema = {
|
|
|
10598
10590
|
}
|
|
10599
10591
|
};
|
|
10600
10592
|
function supportsNodeErrorHandling(node) {
|
|
10601
|
-
return supportsNodeRetry(node) || node.kind === "task" && "executor" in node.task && node.task.executor.kind === "
|
|
10593
|
+
return supportsNodeRetry(node) || node.kind === "task" && "executor" in node.task && node.task.executor.kind === "skip-duplicates";
|
|
10602
10594
|
}
|
|
10603
10595
|
function nodeSystemOutputPorts(node) {
|
|
10604
10596
|
return supportsNodeErrorHandling(node) && node.onError == "error" ? { error: {
|
|
@@ -11997,6 +11989,8 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
|
|
|
11997
11989
|
const changes = yield* Queue.unbounded();
|
|
11998
11990
|
let remainingMs = context.remainingMs;
|
|
11999
11991
|
let measuredAt = yield* Clock.currentTimeMillis;
|
|
11992
|
+
let nextCallCheckAt = measuredAt + (resume == null ? 1e3 : 0);
|
|
11993
|
+
let readyWorkflowRunIds = resume?.readyWorkflowRunIds;
|
|
12000
11994
|
let idleSince;
|
|
12001
11995
|
const agents = { ...resume?.checkpoint.agents };
|
|
12002
11996
|
const resolveInputs = (nodeId, node) => resolveNodeInputs(nodeId, node, dependencies, context.bindingValues, launch[nodeId]);
|
|
@@ -12214,10 +12208,17 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
|
|
|
12214
12208
|
return yield* Effect.failCause(firstCause);
|
|
12215
12209
|
}
|
|
12216
12210
|
if (pendingWaits.size > 0 && context.waits != null) applyResolutions(yield* context.waits.resolutions([...pendingWaits.values()].map((wait) => wait.waitId), false), now);
|
|
12217
|
-
|
|
12218
|
-
|
|
12219
|
-
|
|
12220
|
-
|
|
12211
|
+
if (pendingCalls.size > 0 && now >= nextCallCheckAt) {
|
|
12212
|
+
for (const call of pendingCalls.values()) {
|
|
12213
|
+
if (readyWorkflowRunIds != null && !readyWorkflowRunIds.has(call.childRunId)) continue;
|
|
12214
|
+
yield* observe(call.nodeId, call.jobId, Effect.gen(function* () {
|
|
12215
|
+
const result = yield* execute(call.nodeId, target.graph.nodes[call.nodeId], call.jobId, call.input);
|
|
12216
|
+
yield* settleNode(call.nodeId, call.jobId, result);
|
|
12217
|
+
})).pipe(Effect.catch(() => Effect.void));
|
|
12218
|
+
}
|
|
12219
|
+
readyWorkflowRunIds = void 0;
|
|
12220
|
+
nextCallCheckAt = (yield* Clock.currentTimeMillis) + 1e3;
|
|
12221
|
+
}
|
|
12221
12222
|
const ready = dependencies.ready();
|
|
12222
12223
|
for (const nodeId of ready) executeReady(nodeId);
|
|
12223
12224
|
const running = yield* FiberSet.size(active);
|
|
@@ -12252,7 +12253,7 @@ function runGraph(context, target, runId, launchInputs = {}, trigger, resume) {
|
|
|
12252
12253
|
const activity = Queue.take(changes).pipe(Effect.as({}));
|
|
12253
12254
|
const decision = pendingWaits.size == 0 || context.waits == null ? Effect.never : context.waits.resolutions([...pendingWaits.values()].map((wait) => wait.waitId), true);
|
|
12254
12255
|
let next = Effect.raceFirst(activity, decision);
|
|
12255
|
-
if (pendingCalls.size > 0) next = Effect.raceFirst(next, Effect.sleep(
|
|
12256
|
+
if (pendingCalls.size > 0) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, nextCallCheckAt - (yield* Clock.currentTimeMillis))).pipe(Effect.as({})));
|
|
12256
12257
|
if (left != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, left)).pipe(Effect.as({})));
|
|
12257
12258
|
applyResolutions(yield* next, yield* Clock.currentTimeMillis);
|
|
12258
12259
|
}
|
|
@@ -12290,6 +12291,8 @@ function runFlow(prepared, options) {
|
|
|
12290
12291
|
return Effect.gen(function* () {
|
|
12291
12292
|
if (options.resume != null && (options.inputs != null || options.trigger != null || options.bindingValues != null)) return yield* Effect.fail(/* @__PURE__ */ new Error("A resumed Flow Run cannot accept launch inputs, binding values, or a Trigger seed."));
|
|
12292
12293
|
const decoded = options.resume == null ? void 0 : decodeFlowRunCheckpoint(options.resume.checkpoint);
|
|
12294
|
+
const readyWorkflowRunIds = options.resume?.readyWorkflowRunIds === void 0 ? void 0 : new Set(array(string$1().min(1)).parse(options.resume.readyWorkflowRunIds));
|
|
12295
|
+
if (readyWorkflowRunIds != null && [...readyWorkflowRunIds].some((id) => !decoded?.calls?.some((call) => call.childRunId == id))) return yield* Effect.fail(/* @__PURE__ */ new Error("Ready workflow Run is not present in the checkpoint."));
|
|
12293
12296
|
const outputs = (nodeId, value) => isResolutionNode(prepared.graph.nodes[nodeId]) && value.pending != null ? {
|
|
12294
12297
|
...value,
|
|
12295
12298
|
pending: withWaitNotificationMessage(value.pending)
|
|
@@ -12340,7 +12343,10 @@ function runFlow(prepared, options) {
|
|
|
12340
12343
|
inputDefs: prepared.inputDefs,
|
|
12341
12344
|
outputDefs: prepared.outputDefs,
|
|
12342
12345
|
outputMappings: prepared.outputMappings
|
|
12343
|
-
}, options.runId, options.inputs, trigger, checkpoint == null ? void 0 : {
|
|
12346
|
+
}, options.runId, options.inputs, trigger, checkpoint == null ? void 0 : {
|
|
12347
|
+
checkpoint,
|
|
12348
|
+
readyWorkflowRunIds
|
|
12349
|
+
});
|
|
12344
12350
|
});
|
|
12345
12351
|
}
|
|
12346
12352
|
//#endregion
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { JsonValue } from './flow-model.js';
|
|
2
|
-
import type {
|
|
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/package.json
CHANGED
|
@@ -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
|
}),
|
|
@@ -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' } } },
|
|
@@ -34,7 +34,13 @@ const schemas = {
|
|
|
34
34
|
updateEventSource: updateEventSourceSchema,
|
|
35
35
|
eventSourceRevision: eventSourceRevisionSchema,
|
|
36
36
|
importFlow: z.strictObject({ file: json, name: flowName.optional(), teamId: id.optional(), tagIds: tagIds.optional(), version }),
|
|
37
|
-
createFlow: z.strictObject({
|
|
37
|
+
createFlow: z.strictObject({
|
|
38
|
+
name: flowName,
|
|
39
|
+
teamId: id.optional(),
|
|
40
|
+
tagIds: tagIds.optional(),
|
|
41
|
+
presentation: z.record(z.string(), json).optional(),
|
|
42
|
+
version,
|
|
43
|
+
}),
|
|
38
44
|
saveTag: z.strictObject({ name: tagName, version }),
|
|
39
45
|
setFlowTags: z.strictObject({ tagIds, version }),
|
|
40
46
|
updateFlow: z.union([z.strictObject({ name: flowName, tagIds: tagIds.optional(), version }), z.strictObject({ name: flowName.optional(), tagIds, version })]),
|
|
@@ -23,7 +23,7 @@ const nodeKinds = new Set([
|
|
|
23
23
|
'javascript',
|
|
24
24
|
'openapi',
|
|
25
25
|
'http-request',
|
|
26
|
-
'
|
|
26
|
+
'skip-duplicates',
|
|
27
27
|
'llm',
|
|
28
28
|
'value',
|
|
29
29
|
'wait',
|
|
@@ -57,7 +57,7 @@ function nodeKind(
|
|
|
57
57
|
| 'javascript'
|
|
58
58
|
| 'openapi'
|
|
59
59
|
| 'http-request'
|
|
60
|
-
| '
|
|
60
|
+
| 'skip-duplicates'
|
|
61
61
|
| 'llm'
|
|
62
62
|
| 'value'
|
|
63
63
|
| 'wait'
|
|
@@ -78,7 +78,7 @@ function nodeKind(
|
|
|
78
78
|
| 'javascript'
|
|
79
79
|
| 'openapi'
|
|
80
80
|
| 'http-request'
|
|
81
|
-
| '
|
|
81
|
+
| 'skip-duplicates'
|
|
82
82
|
| 'llm'
|
|
83
83
|
| 'value'
|
|
84
84
|
| 'wait'
|
|
@@ -95,6 +95,7 @@ function nodeContext(
|
|
|
95
95
|
event: Record<string, unknown>,
|
|
96
96
|
flows: ReadonlyMap<string, string>,
|
|
97
97
|
platformRunId: string,
|
|
98
|
+
identify: typeof opaqueId,
|
|
98
99
|
): {
|
|
99
100
|
readonly executionId: Promise<string>
|
|
100
101
|
readonly flowId: string
|
|
@@ -105,7 +106,7 @@ function nodeContext(
|
|
|
105
106
|
const flowId = flows.get(rawRunId)
|
|
106
107
|
if (flowId == null) throw new TypeError('Runtime node event preceded its run.started event.')
|
|
107
108
|
return {
|
|
108
|
-
executionId:
|
|
109
|
+
executionId: identify('execution', platformRunId, rawRunId, string(event.jobId, 'Runtime node event jobId')),
|
|
109
110
|
flowId,
|
|
110
111
|
nodeId: string(event.nodeId, 'Runtime node event nodeId'),
|
|
111
112
|
rawRunId,
|
|
@@ -184,6 +185,17 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
184
185
|
const jobs = new Map<string, string>()
|
|
185
186
|
const nodeProgressBuckets = new Map<string, number>()
|
|
186
187
|
const runProgressBuckets = new Map<string, number>()
|
|
188
|
+
// Every event of a scope or execution repeats its identity, so each digest is computed once per projector.
|
|
189
|
+
const identities = new Map<string, Promise<string>>()
|
|
190
|
+
const identify: typeof opaqueId = (prefix, ...parts) => {
|
|
191
|
+
const key = JSON.stringify([prefix, ...parts])
|
|
192
|
+
let identity = identities.get(key)
|
|
193
|
+
if (identity == null) {
|
|
194
|
+
identity = opaqueId(prefix, ...parts)
|
|
195
|
+
identities.set(key, identity)
|
|
196
|
+
}
|
|
197
|
+
return identity
|
|
198
|
+
}
|
|
187
199
|
return async (value) => {
|
|
188
200
|
const event = object(value, 'Runtime event')
|
|
189
201
|
const type = string(event.type, 'Runtime event type')
|
|
@@ -205,8 +217,8 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
205
217
|
kind: 'run.started',
|
|
206
218
|
payload: {
|
|
207
219
|
flowId,
|
|
208
|
-
...(parentRunId === undefined ? {} : { parentScopeId: await
|
|
209
|
-
scopeId: await
|
|
220
|
+
...(parentRunId === undefined ? {} : { parentScopeId: await identify('scope', platformRunId, parentRunId) }),
|
|
221
|
+
scopeId: await identify('scope', platformRunId, rawRunId),
|
|
210
222
|
},
|
|
211
223
|
}
|
|
212
224
|
}
|
|
@@ -226,7 +238,7 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
226
238
|
payload: {
|
|
227
239
|
flowId,
|
|
228
240
|
progress: event.progress,
|
|
229
|
-
scopeId: await
|
|
241
|
+
scopeId: await identify('scope', platformRunId, rawRunId),
|
|
230
242
|
},
|
|
231
243
|
}
|
|
232
244
|
}
|
|
@@ -235,7 +247,7 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
235
247
|
const rawRunId = runId(event)
|
|
236
248
|
if (type == 'run.failed' && rawRunId == platformRunId && event.failure != null) {
|
|
237
249
|
const failure = object(event.failure, 'Runtime terminal failure')
|
|
238
|
-
const context = nodeContext(failure, flows, platformRunId)
|
|
250
|
+
const context = nodeContext(failure, flows, platformRunId, identify)
|
|
239
251
|
return {
|
|
240
252
|
kind: 'node.failed',
|
|
241
253
|
payload: {
|
|
@@ -258,13 +270,13 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
258
270
|
type == 'node.completed' ||
|
|
259
271
|
type == 'node.failed'
|
|
260
272
|
) {
|
|
261
|
-
const context = nodeContext(event, flows, platformRunId)
|
|
273
|
+
const context = nodeContext(event, flows, platformRunId, identify)
|
|
262
274
|
const executionId = await context.executionId
|
|
263
275
|
const payload = {
|
|
264
276
|
executionId,
|
|
265
277
|
flowId: context.flowId,
|
|
266
278
|
nodeId: context.nodeId,
|
|
267
|
-
scopeId: await
|
|
279
|
+
scopeId: await identify('scope', platformRunId, context.rawRunId),
|
|
268
280
|
...(event.systemOutputs === undefined ? {} : { systemOutputs: object(event.systemOutputs, 'Runtime system outputs') }),
|
|
269
281
|
...(event.handled === true ? { handled: true } : {}),
|
|
270
282
|
}
|
|
@@ -13,6 +13,7 @@ import * as Effect from 'effect/Effect'
|
|
|
13
13
|
import * as FiberSet from 'effect/FiberSet'
|
|
14
14
|
import * as Queue from 'effect/Queue'
|
|
15
15
|
import { nanoid } from 'nanoid'
|
|
16
|
+
import { z } from 'zod'
|
|
16
17
|
import { isTriggerNode } from '../../flow/common/change.ts'
|
|
17
18
|
import { nodeInputMappings } from '../../flow/common/condition.ts'
|
|
18
19
|
import { nodeSystemOutputPorts, supportsNodeErrorHandling } from '../../flow/common/errorHandling.ts'
|
|
@@ -70,7 +71,7 @@ export type SchedulerEvent =
|
|
|
70
71
|
| 'javascript'
|
|
71
72
|
| 'openapi'
|
|
72
73
|
| 'http-request'
|
|
73
|
-
| '
|
|
74
|
+
| 'skip-duplicates'
|
|
74
75
|
| 'llm'
|
|
75
76
|
| 'value'
|
|
76
77
|
| 'wait'
|
|
@@ -217,7 +218,7 @@ export type RunLaunch =
|
|
|
217
218
|
readonly resume?: never
|
|
218
219
|
}
|
|
219
220
|
| {
|
|
220
|
-
readonly resume: { readonly checkpoint: unknown }
|
|
221
|
+
readonly resume: { readonly checkpoint: unknown; readonly readyWorkflowRunIds?: readonly string[] }
|
|
221
222
|
readonly bindingValues?: never
|
|
222
223
|
readonly inputs?: never
|
|
223
224
|
readonly trigger?: never
|
|
@@ -250,7 +251,7 @@ function runGraph(
|
|
|
250
251
|
runId: string,
|
|
251
252
|
launchInputs: Readonly<Record<string, Readonly<Record<string, JsonValue>>>> = {},
|
|
252
253
|
trigger?: TriggerSeed,
|
|
253
|
-
resume?: { readonly checkpoint: FlowRunCheckpoint },
|
|
254
|
+
resume?: { readonly checkpoint: FlowRunCheckpoint; readonly readyWorkflowRunIds?: ReadonlySet<string> },
|
|
254
255
|
): Effect.Effect<FlowRunOutcome, Error> {
|
|
255
256
|
return Effect.scoped(
|
|
256
257
|
Effect.gen(function* () {
|
|
@@ -292,6 +293,8 @@ function runGraph(
|
|
|
292
293
|
const changes = yield* Queue.unbounded<void>()
|
|
293
294
|
let remainingMs = context.remainingMs
|
|
294
295
|
let measuredAt = yield* Clock.currentTimeMillis
|
|
296
|
+
let nextCallCheckAt = measuredAt + (resume == null ? 1000 : 0)
|
|
297
|
+
let readyWorkflowRunIds = resume?.readyWorkflowRunIds
|
|
295
298
|
let idleSince: number | undefined
|
|
296
299
|
const agents: Record<string, FlowRunCheckpoint['agents'][string]> = { ...resume?.checkpoint.agents }
|
|
297
300
|
const resolveInputs = (nodeId: string, node: ExecutableNode) => resolveNodeInputs(nodeId, node, dependencies, context.bindingValues, launch[nodeId])
|
|
@@ -546,15 +549,20 @@ function runGraph(
|
|
|
546
549
|
),
|
|
547
550
|
now,
|
|
548
551
|
)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
call.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
552
|
+
if (pendingCalls.size > 0 && now >= nextCallCheckAt) {
|
|
553
|
+
for (const call of pendingCalls.values()) {
|
|
554
|
+
if (readyWorkflowRunIds != null && !readyWorkflowRunIds.has(call.childRunId)) continue
|
|
555
|
+
yield* observe(
|
|
556
|
+
call.nodeId,
|
|
557
|
+
call.jobId,
|
|
558
|
+
Effect.gen(function* () {
|
|
559
|
+
const result = yield* execute(call.nodeId, target.graph.nodes[call.nodeId] as ExecutableNode, call.jobId, call.input)
|
|
560
|
+
yield* settleNode(call.nodeId, call.jobId, result)
|
|
561
|
+
}),
|
|
562
|
+
).pipe(Effect.catch(() => Effect.void))
|
|
563
|
+
}
|
|
564
|
+
readyWorkflowRunIds = undefined
|
|
565
|
+
nextCallCheckAt = (yield* Clock.currentTimeMillis) + 1000
|
|
558
566
|
}
|
|
559
567
|
const ready = dependencies.ready()
|
|
560
568
|
for (const nodeId of ready) executeReady(nodeId)
|
|
@@ -592,7 +600,8 @@ function runGraph(
|
|
|
592
600
|
true,
|
|
593
601
|
)
|
|
594
602
|
let next = Effect.raceFirst(activity, decision)
|
|
595
|
-
if (pendingCalls.size > 0)
|
|
603
|
+
if (pendingCalls.size > 0)
|
|
604
|
+
next = Effect.raceFirst(next, Effect.sleep(Math.max(0, nextCallCheckAt - (yield* Clock.currentTimeMillis))).pipe(Effect.as({})))
|
|
596
605
|
if (left != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, left)).pipe(Effect.as({})))
|
|
597
606
|
const resolutions = yield* next
|
|
598
607
|
applyResolutions(resolutions, yield* Clock.currentTimeMillis)
|
|
@@ -631,6 +640,11 @@ export function runFlow(prepared: PreparedFlow, options: FlowRunOptions): Effect
|
|
|
631
640
|
return yield* Effect.fail(new Error('A resumed Flow Run cannot accept launch inputs, binding values, or a Trigger seed.'))
|
|
632
641
|
}
|
|
633
642
|
const decoded = options.resume == null ? undefined : decodeFlowRunCheckpoint(options.resume.checkpoint)
|
|
643
|
+
const readyWorkflowRunIds =
|
|
644
|
+
options.resume?.readyWorkflowRunIds === undefined ? undefined : new Set(z.array(z.string().min(1)).parse(options.resume.readyWorkflowRunIds))
|
|
645
|
+
if (readyWorkflowRunIds != null && [...readyWorkflowRunIds].some((id) => !decoded?.calls?.some((call) => call.childRunId == id))) {
|
|
646
|
+
return yield* Effect.fail(new Error('Ready workflow Run is not present in the checkpoint.'))
|
|
647
|
+
}
|
|
634
648
|
const outputs = (nodeId: string, value: Readonly<Record<string, JsonValue>>) =>
|
|
635
649
|
isResolutionNode(prepared.graph.nodes[nodeId]) && value.pending != null ? { ...value, pending: withWaitNotificationMessage(value.pending) } : value
|
|
636
650
|
const checkpoint =
|
|
@@ -692,7 +706,7 @@ export function runFlow(prepared: PreparedFlow, options: FlowRunOptions): Effect
|
|
|
692
706
|
options.runId,
|
|
693
707
|
options.inputs,
|
|
694
708
|
trigger,
|
|
695
|
-
checkpoint == null ? undefined : { checkpoint },
|
|
709
|
+
checkpoint == null ? undefined : { checkpoint, readyWorkflowRunIds },
|
|
696
710
|
)
|
|
697
711
|
})
|
|
698
712
|
return program
|
|
@@ -167,7 +167,7 @@ export interface ConditionNode extends GraphNodeBase {
|
|
|
167
167
|
|
|
168
168
|
export type ManagedTaskExecutor =
|
|
169
169
|
| import('../../http/common/httpRequest.ts').HttpRequestExecutor
|
|
170
|
-
| import('../../state/common/state.ts').
|
|
170
|
+
| import('../../state/common/state.ts').SkipDuplicatesExecutor
|
|
171
171
|
| WorkflowExecutor
|
|
172
172
|
| import('../../decision/common/decision.ts').DecisionExecutor
|
|
173
173
|
| import('../../openapi/common/openapi.ts').OpenApiExecutor
|