@oomol-lab/open-flow 0.1.0-beta.76 → 0.1.0-beta.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/browser/{ContentIcon-Cm1hbVzW.js → ContentIcon-CMn0c-kl.js} +99 -6
- package/dist/browser/{FlowCanvas-NzKtla_c.js → FlowCanvas-BQGBcfoL.js} +1709 -613
- package/dist/browser/{createResourceDialog-BgdedQqu.js → createResourceDialog-DCGxfKFM.js} +63 -57
- package/dist/browser/{dialog-aSElFFw2.js → dialog-WzR4CRFH.js} +76 -77
- package/dist/browser/{exportSvg-Dcz4HTo_.js → exportSvg-CYwyYiuH.js} +2 -2
- package/dist/browser/{flowChanges-NZyS7HSp.js → flowChanges-Bz4dbDN_.js} +6875 -11091
- package/dist/browser/{flowWorkspace-Bl0SDehy.js → flowWorkspace-BI6L6a4r.js} +1415 -3037
- package/dist/browser/licenses.md +1 -1
- package/dist/browser/preview.css +619 -462
- package/dist/browser/preview.js +9016 -14956
- package/dist/browser/theme.css +4 -2
- package/dist/browser/{tooltip-3XMK9jkr.js → tooltip-BDcVTNpu.js} +65 -2
- package/dist/browser/{typeScriptSession-C9sBGOKZ.js → typeScriptSession-CRoezJPN.js} +1 -1
- package/dist/browser/ui.css +112 -24
- package/dist/browser/ui.js +3 -2
- package/dist/browser/workbench.css +10020 -9842
- package/dist/browser/workbench.d.ts +5 -2
- package/dist/browser/workbench.js +2382 -2668
- package/dist/browser/{workbenchSelect-DBGP7Y4Z.js → workbenchSelect-DtAd0zc4.js} +2 -2
- package/dist/command/index.js +48 -48
- package/dist/common/authoring.d.ts +4 -4
- package/dist/common/authoringExamples.d.ts +3 -2
- package/dist/common/authoringSchema.d.ts +15 -4
- package/dist/common/control-api-conformance.js +155 -15
- package/dist/common/control-api-errors.d.ts +12 -0
- package/dist/common/control-api.d.ts +39 -2
- package/dist/common/control-api.js +180 -37
- package/dist/common/control-requests.d.ts +23 -3
- package/dist/common/control-requests.js +141 -14
- package/dist/common/engine-contract.d.ts +3 -2
- package/dist/common/flow-authoring.js +47 -7
- package/dist/common/flow-change-schema.d.ts +1 -1
- package/dist/common/flow-change.d.ts +2 -2
- package/dist/common/flow-change.js +48 -8
- package/dist/common/flow-encoding.js +47 -7
- package/dist/common/flow-semantics.js +86 -8
- package/dist/common/flow-state.d.ts +100 -0
- package/dist/common/flow-state.js +5794 -0
- package/dist/common/mcp.d.ts +96 -3
- package/dist/common/mcp.js +158 -13
- package/dist/common/run-events.js +19 -8
- package/dist/common/runtime-contract.d.ts +14 -0
- package/dist/common/runtime-contract.js +124 -17
- package/dist/common/runtime-nodejs-compat.js +11 -12
- package/dist/common/scheduler-node-execution.d.ts +1 -0
- package/dist/common/scheduler.d.ts +2 -1
- package/dist/common/scheduler.js +95 -40
- package/dist/common/state-conformance.d.ts +16 -0
- package/dist/common/tags.d.ts +4 -0
- package/dist/index.d.ts +11 -0
- package/package.json +5 -1
- package/src/control/common/api.ts +101 -11
- package/src/control/common/authoring.ts +7 -1
- package/src/control/common/authoringExamples.ts +9 -0
- package/src/control/common/authoringSchema.ts +18 -12
- package/src/control/common/errors.ts +6 -0
- package/src/control/common/flowDecoders.ts +1 -0
- package/src/control/common/mcp.ts +16 -0
- package/src/control/common/requests.ts +16 -2
- package/src/control/common/tags.ts +20 -0
- package/src/execution/common/events.ts +22 -7
- package/src/execution/common/node-execution.ts +3 -0
- package/src/execution/common/scheduler.ts +29 -13
- package/src/flow/common/change.ts +2 -1
- package/src/flow/common/changeSchema.ts +11 -1
- package/src/flow/common/errorHandling.ts +1 -1
- package/src/flow/common/semantics.ts +27 -1
- package/src/runtime/common/engineContract.ts +6 -3
- package/src/runtime/common/nodejsGuest.generated.json +11 -12
- package/src/runtime/common/runtime.ts +122 -7
- package/src/state/common/conformance.ts +81 -0
- package/src/state/common/state.ts +141 -0
- package/src/types/index.ts +11 -0
|
@@ -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,17 @@ 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({
|
|
36
|
+
importFlow: z.strictObject({ file: json, name: flowName.optional(), teamId: id.optional(), tagIds: tagIds.optional(), version }),
|
|
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
|
+
}),
|
|
44
|
+
saveTag: z.strictObject({ name: tagName, version }),
|
|
45
|
+
setFlowTags: z.strictObject({ tagIds, version }),
|
|
46
|
+
updateFlow: z.union([z.strictObject({ name: flowName, tagIds: tagIds.optional(), version }), z.strictObject({ name: flowName.optional(), tagIds, version })]),
|
|
36
47
|
renameFlow: z.strictObject({ name: flowName, version }),
|
|
37
48
|
changeDraft: z.strictObject({ expectedRevisionId: id, operations: z.array(json).min(1), version }),
|
|
38
49
|
repairDraft: z.strictObject({ expectedRevisionId: id, version }),
|
|
@@ -79,6 +90,9 @@ export const controlRequests = {
|
|
|
79
90
|
eventSourceRevision: decoder(schemas.eventSourceRevision),
|
|
80
91
|
importFlow: decoder(schemas.importFlow),
|
|
81
92
|
createFlow: decoder(schemas.createFlow),
|
|
93
|
+
saveTag: decoder(schemas.saveTag),
|
|
94
|
+
setFlowTags: decoder(schemas.setFlowTags),
|
|
95
|
+
updateFlow: decoder(schemas.updateFlow),
|
|
82
96
|
renameFlow: decoder(schemas.renameFlow),
|
|
83
97
|
changeDraft(value: unknown): { expectedRevisionId: string; operations: readonly DraftOperation[]; version: 1 } {
|
|
84
98
|
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
|
+
'skip-duplicates',
|
|
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
|
+
| 'skip-duplicates'
|
|
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
|
+
| 'skip-duplicates'
|
|
79
82
|
| 'llm'
|
|
80
83
|
| 'value'
|
|
81
84
|
| 'wait'
|
|
@@ -92,6 +95,7 @@ function nodeContext(
|
|
|
92
95
|
event: Record<string, unknown>,
|
|
93
96
|
flows: ReadonlyMap<string, string>,
|
|
94
97
|
platformRunId: string,
|
|
98
|
+
identify: typeof opaqueId,
|
|
95
99
|
): {
|
|
96
100
|
readonly executionId: Promise<string>
|
|
97
101
|
readonly flowId: string
|
|
@@ -102,7 +106,7 @@ function nodeContext(
|
|
|
102
106
|
const flowId = flows.get(rawRunId)
|
|
103
107
|
if (flowId == null) throw new TypeError('Runtime node event preceded its run.started event.')
|
|
104
108
|
return {
|
|
105
|
-
executionId:
|
|
109
|
+
executionId: identify('execution', platformRunId, rawRunId, string(event.jobId, 'Runtime node event jobId')),
|
|
106
110
|
flowId,
|
|
107
111
|
nodeId: string(event.nodeId, 'Runtime node event nodeId'),
|
|
108
112
|
rawRunId,
|
|
@@ -181,6 +185,17 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
181
185
|
const jobs = new Map<string, string>()
|
|
182
186
|
const nodeProgressBuckets = new Map<string, number>()
|
|
183
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
|
+
}
|
|
184
199
|
return async (value) => {
|
|
185
200
|
const event = object(value, 'Runtime event')
|
|
186
201
|
const type = string(event.type, 'Runtime event type')
|
|
@@ -202,8 +217,8 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
202
217
|
kind: 'run.started',
|
|
203
218
|
payload: {
|
|
204
219
|
flowId,
|
|
205
|
-
...(parentRunId === undefined ? {} : { parentScopeId: await
|
|
206
|
-
scopeId: await
|
|
220
|
+
...(parentRunId === undefined ? {} : { parentScopeId: await identify('scope', platformRunId, parentRunId) }),
|
|
221
|
+
scopeId: await identify('scope', platformRunId, rawRunId),
|
|
207
222
|
},
|
|
208
223
|
}
|
|
209
224
|
}
|
|
@@ -223,7 +238,7 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
223
238
|
payload: {
|
|
224
239
|
flowId,
|
|
225
240
|
progress: event.progress,
|
|
226
|
-
scopeId: await
|
|
241
|
+
scopeId: await identify('scope', platformRunId, rawRunId),
|
|
227
242
|
},
|
|
228
243
|
}
|
|
229
244
|
}
|
|
@@ -232,7 +247,7 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
232
247
|
const rawRunId = runId(event)
|
|
233
248
|
if (type == 'run.failed' && rawRunId == platformRunId && event.failure != null) {
|
|
234
249
|
const failure = object(event.failure, 'Runtime terminal failure')
|
|
235
|
-
const context = nodeContext(failure, flows, platformRunId)
|
|
250
|
+
const context = nodeContext(failure, flows, platformRunId, identify)
|
|
236
251
|
return {
|
|
237
252
|
kind: 'node.failed',
|
|
238
253
|
payload: {
|
|
@@ -255,13 +270,13 @@ export function createEventProjector(platformRunId: string, nodeFailureCodes: Re
|
|
|
255
270
|
type == 'node.completed' ||
|
|
256
271
|
type == 'node.failed'
|
|
257
272
|
) {
|
|
258
|
-
const context = nodeContext(event, flows, platformRunId)
|
|
273
|
+
const context = nodeContext(event, flows, platformRunId, identify)
|
|
259
274
|
const executionId = await context.executionId
|
|
260
275
|
const payload = {
|
|
261
276
|
executionId,
|
|
262
277
|
flowId: context.flowId,
|
|
263
278
|
nodeId: context.nodeId,
|
|
264
|
-
scopeId: await
|
|
279
|
+
scopeId: await identify('scope', platformRunId, context.rawRunId),
|
|
265
280
|
...(event.systemOutputs === undefined ? {} : { systemOutputs: object(event.systemOutputs, 'Runtime system outputs') }),
|
|
266
281
|
...(event.handled === true ? { handled: true } : {}),
|
|
267
282
|
}
|
|
@@ -109,6 +109,7 @@ function nodeKind(
|
|
|
109
109
|
| 'javascript'
|
|
110
110
|
| 'openapi'
|
|
111
111
|
| 'http-request'
|
|
112
|
+
| 'skip-duplicates'
|
|
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' &&
|
|
@@ -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,6 +71,7 @@ export type SchedulerEvent =
|
|
|
70
71
|
| 'javascript'
|
|
71
72
|
| 'openapi'
|
|
72
73
|
| 'http-request'
|
|
74
|
+
| 'skip-duplicates'
|
|
73
75
|
| 'llm'
|
|
74
76
|
| 'value'
|
|
75
77
|
| 'wait'
|
|
@@ -216,7 +218,7 @@ export type RunLaunch =
|
|
|
216
218
|
readonly resume?: never
|
|
217
219
|
}
|
|
218
220
|
| {
|
|
219
|
-
readonly resume: { readonly checkpoint: unknown }
|
|
221
|
+
readonly resume: { readonly checkpoint: unknown; readonly readyWorkflowRunIds?: readonly string[] }
|
|
220
222
|
readonly bindingValues?: never
|
|
221
223
|
readonly inputs?: never
|
|
222
224
|
readonly trigger?: never
|
|
@@ -249,7 +251,7 @@ function runGraph(
|
|
|
249
251
|
runId: string,
|
|
250
252
|
launchInputs: Readonly<Record<string, Readonly<Record<string, JsonValue>>>> = {},
|
|
251
253
|
trigger?: TriggerSeed,
|
|
252
|
-
resume?: { readonly checkpoint: FlowRunCheckpoint },
|
|
254
|
+
resume?: { readonly checkpoint: FlowRunCheckpoint; readonly readyWorkflowRunIds?: ReadonlySet<string> },
|
|
253
255
|
): Effect.Effect<FlowRunOutcome, Error> {
|
|
254
256
|
return Effect.scoped(
|
|
255
257
|
Effect.gen(function* () {
|
|
@@ -291,6 +293,8 @@ function runGraph(
|
|
|
291
293
|
const changes = yield* Queue.unbounded<void>()
|
|
292
294
|
let remainingMs = context.remainingMs
|
|
293
295
|
let measuredAt = yield* Clock.currentTimeMillis
|
|
296
|
+
let nextCallCheckAt = measuredAt + (resume == null ? 1000 : 0)
|
|
297
|
+
let readyWorkflowRunIds = resume?.readyWorkflowRunIds
|
|
294
298
|
let idleSince: number | undefined
|
|
295
299
|
const agents: Record<string, FlowRunCheckpoint['agents'][string]> = { ...resume?.checkpoint.agents }
|
|
296
300
|
const resolveInputs = (nodeId: string, node: ExecutableNode) => resolveNodeInputs(nodeId, node, dependencies, context.bindingValues, launch[nodeId])
|
|
@@ -344,6 +348,7 @@ function runGraph(
|
|
|
344
348
|
node,
|
|
345
349
|
jobId,
|
|
346
350
|
nodeInputs,
|
|
351
|
+
launchInputs: launch[nodeId],
|
|
347
352
|
resolution,
|
|
348
353
|
flowId: target.flowId,
|
|
349
354
|
runId,
|
|
@@ -544,15 +549,20 @@ function runGraph(
|
|
|
544
549
|
),
|
|
545
550
|
now,
|
|
546
551
|
)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
call.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
|
556
566
|
}
|
|
557
567
|
const ready = dependencies.ready()
|
|
558
568
|
for (const nodeId of ready) executeReady(nodeId)
|
|
@@ -590,7 +600,8 @@ function runGraph(
|
|
|
590
600
|
true,
|
|
591
601
|
)
|
|
592
602
|
let next = Effect.raceFirst(activity, decision)
|
|
593
|
-
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({})))
|
|
594
605
|
if (left != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, left)).pipe(Effect.as({})))
|
|
595
606
|
const resolutions = yield* next
|
|
596
607
|
applyResolutions(resolutions, yield* Clock.currentTimeMillis)
|
|
@@ -629,6 +640,11 @@ export function runFlow(prepared: PreparedFlow, options: FlowRunOptions): Effect
|
|
|
629
640
|
return yield* Effect.fail(new Error('A resumed Flow Run cannot accept launch inputs, binding values, or a Trigger seed.'))
|
|
630
641
|
}
|
|
631
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
|
+
}
|
|
632
648
|
const outputs = (nodeId: string, value: Readonly<Record<string, JsonValue>>) =>
|
|
633
649
|
isResolutionNode(prepared.graph.nodes[nodeId]) && value.pending != null ? { ...value, pending: withWaitNotificationMessage(value.pending) } : value
|
|
634
650
|
const checkpoint =
|
|
@@ -690,7 +706,7 @@ export function runFlow(prepared: PreparedFlow, options: FlowRunOptions): Effect
|
|
|
690
706
|
options.runId,
|
|
691
707
|
options.inputs,
|
|
692
708
|
trigger,
|
|
693
|
-
checkpoint == null ? undefined : { checkpoint },
|
|
709
|
+
checkpoint == null ? undefined : { checkpoint, readyWorkflowRunIds },
|
|
694
710
|
)
|
|
695
711
|
})
|
|
696
712
|
return program
|
|
@@ -167,6 +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').SkipDuplicatesExecutor
|
|
170
171
|
| WorkflowExecutor
|
|
171
172
|
| import('../../decision/common/decision.ts').DecisionExecutor
|
|
172
173
|
| import('../../openapi/common/openapi.ts').OpenApiExecutor
|
|
@@ -397,7 +398,7 @@ export interface FlowDocument {
|
|
|
397
398
|
|
|
398
399
|
export interface RevisionContent {
|
|
399
400
|
readonly document: FlowDocument
|
|
400
|
-
readonly modelVersion: 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | typeof currentFlowModelVersion
|
|
401
|
+
readonly modelVersion: 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | typeof currentFlowModelVersion
|
|
401
402
|
readonly modules: Readonly<Record<string, CodeModule>>
|
|
402
403
|
}
|
|
403
404
|
|
|
@@ -6,6 +6,7 @@ import { limitDecisionTask } from '../../decision/common/decision.ts'
|
|
|
6
6
|
import { assertHttpAuthBindings } from '../../http/common/authBindings.ts'
|
|
7
7
|
import { isHttpHost, maxHttpHosts } from '../../http/common/hosts.ts'
|
|
8
8
|
import { httpRequestExecutorSchema } from '../../http/common/httpRequest.ts'
|
|
9
|
+
import { skipDuplicatesExecutorSchema } from '../../state/common/state.ts'
|
|
9
10
|
import { checkJsonDepth } from './json.ts'
|
|
10
11
|
import { retryConfigurationSchema } from './retry.ts'
|
|
11
12
|
import { triggerScheduleSchema } from './triggerScheduleSchema.ts'
|
|
@@ -13,7 +14,7 @@ import { waitAutoContinueDraftSchema, waitAutoContinueSchema } from './wait.ts'
|
|
|
13
14
|
import { webhookMethods } from './webhookMethod.ts'
|
|
14
15
|
import { webhookResponseTimeout } from './webhookResponse.ts'
|
|
15
16
|
|
|
16
|
-
export const currentFlowModelVersion =
|
|
17
|
+
export const currentFlowModelVersion = 13
|
|
17
18
|
const text = z.string()
|
|
18
19
|
const actionConnection = text.describe(
|
|
19
20
|
'Account selection: omit while unconfigured; use an account ID to select it; use "" to explicitly defer configuration. Deferred Actions fail if called; deferred Agent tools are excluded from the callable tool set. Not supported by Triggers.',
|
|
@@ -129,6 +130,7 @@ const managedFields = z.object({
|
|
|
129
130
|
}),
|
|
130
131
|
z.strictObject({ kind: z.literal('workflow'), flowId: text, revisionId: text }),
|
|
131
132
|
httpRequestExecutorSchema,
|
|
133
|
+
skipDuplicatesExecutorSchema,
|
|
132
134
|
]),
|
|
133
135
|
})
|
|
134
136
|
|
|
@@ -318,6 +320,7 @@ const revision = z.object({
|
|
|
318
320
|
z.literal(9),
|
|
319
321
|
z.literal(10),
|
|
320
322
|
z.literal(11),
|
|
323
|
+
z.literal(12),
|
|
321
324
|
z.literal(currentFlowModelVersion),
|
|
322
325
|
]),
|
|
323
326
|
document,
|
|
@@ -723,6 +726,13 @@ export function changeOperationsSchema(kind?: string): JsonValue {
|
|
|
723
726
|
}
|
|
724
727
|
|
|
725
728
|
function checkModelVersion(content: RevisionContent): void {
|
|
729
|
+
if (
|
|
730
|
+
content.modelVersion < 13 &&
|
|
731
|
+
Object.values(content.document.graph.nodes).some(
|
|
732
|
+
(candidateNode) => candidateNode.kind === 'task' && 'executor' in candidateNode.task && candidateNode.task.executor.kind === 'skip-duplicates',
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
throw new Error('Skip Duplicates requires Flow model v13.')
|
|
726
736
|
if (
|
|
727
737
|
content.modelVersion < 12 &&
|
|
728
738
|
Object.values(content.document.graph.nodes).some(
|
|
@@ -26,7 +26,7 @@ export const nodeErrorSchema = {
|
|
|
26
26
|
} as const
|
|
27
27
|
|
|
28
28
|
export function supportsNodeErrorHandling(node: GraphNode): boolean {
|
|
29
|
-
return supportsNodeRetry(node)
|
|
29
|
+
return supportsNodeRetry(node) || (node.kind === 'task' && 'executor' in node.task && node.task.executor.kind === 'skip-duplicates')
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function nodeSystemOutputPorts(node: GraphNode): Readonly<Record<string, PortDefinition>> {
|
|
@@ -2,6 +2,7 @@ import { dequal } from 'dequal/lite'
|
|
|
2
2
|
import { decisionTaskIssues } from '../../decision/common/decision.ts'
|
|
3
3
|
import { httpRequestIssues, httpAuthHandles, httpRequestTask, httpRequestUrl } from '../../http/common/httpRequest.ts'
|
|
4
4
|
import { openApiIssues, authHandles } from '../../openapi/common/openapi.ts'
|
|
5
|
+
import { skipDuplicatesTask } from '../../state/common/state.ts'
|
|
5
6
|
import { isTriggerNode } from './change.ts'
|
|
6
7
|
import { nodeWorkflowReferences } from './workflowReferences.ts'
|
|
7
8
|
export { renderPrompt } from './promptTemplate.ts'
|
|
@@ -273,6 +274,20 @@ export async function validateFlow(revision: RevisionContent, engine: EngineCont
|
|
|
273
274
|
})
|
|
274
275
|
for (const message of openApiIssues(task))
|
|
275
276
|
checked.diagnostics.push({ code: 'openapi.config-invalid', column: 0, line: 1, message, path: `/document/graph/nodes/${nodeId}/task/executor` })
|
|
277
|
+
if (task.executor.kind === 'skip-duplicates') {
|
|
278
|
+
const expected = skipDuplicatesTask(task.executor)
|
|
279
|
+
if (
|
|
280
|
+
!dequal(httpPortContracts(task.inputs), httpPortContracts(expected.inputs)) ||
|
|
281
|
+
!dequal(httpPortContracts(task.outputs), httpPortContracts(expected.outputs))
|
|
282
|
+
)
|
|
283
|
+
checked.diagnostics.push({
|
|
284
|
+
code: 'skip-duplicates.ports-invalid',
|
|
285
|
+
column: 0,
|
|
286
|
+
line: 1,
|
|
287
|
+
message: 'Deduplication ports must match the fixed contract.',
|
|
288
|
+
path: `/document/graph/nodes/${nodeId}/task`,
|
|
289
|
+
})
|
|
290
|
+
}
|
|
276
291
|
if (task.executor.kind == 'http-request') {
|
|
277
292
|
for (const issue of httpRequestIssues(task.executor))
|
|
278
293
|
checked.diagnostics.push({
|
|
@@ -440,6 +455,13 @@ const httpPortContracts = (ports: import('./change.ts').ManagedTaskDefinition['i
|
|
|
440
455
|
export async function prepareFlow(revision: RevisionContent, engineContract: string, triggerId?: string): Promise<PrepareFlowResult> {
|
|
441
456
|
const engine = findEngineContract(engineContract)
|
|
442
457
|
if (engine == null) return { kind: 'engine-unsupported' }
|
|
458
|
+
if (
|
|
459
|
+
!engine.flowState &&
|
|
460
|
+
Object.values(revision.document.graph.nodes).some(
|
|
461
|
+
(node) => node.kind === 'task' && 'executor' in node.task && node.task.executor.kind === 'skip-duplicates',
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
return { kind: 'engine-unsupported' }
|
|
443
465
|
if (
|
|
444
466
|
!engine.httpRequests &&
|
|
445
467
|
Object.values(revision.document.graph.nodes).some((node) => node.kind == 'task' && 'executor' in node.task && node.task.executor.kind == 'http-request')
|
|
@@ -507,11 +529,15 @@ export async function prepareFlow(revision: RevisionContent, engineContract: str
|
|
|
507
529
|
|
|
508
530
|
export function createRuntimeProgram(prepared: PreparedFlow, entryModuleId: string, engineDigest: string): RuntimeProgram | undefined {
|
|
509
531
|
if (!Object.hasOwn(prepared.modules, entryModuleId)) return
|
|
532
|
+
const modules = new Set([entryModuleId])
|
|
533
|
+
for (const id of modules) {
|
|
534
|
+
for (const imported of prepared.modules[id]!.imports) modules.add(imported)
|
|
535
|
+
}
|
|
510
536
|
return {
|
|
511
537
|
engineContract: prepared.engineContract,
|
|
512
538
|
engineDigest,
|
|
513
539
|
entryModuleId,
|
|
514
|
-
modules: prepared.modules,
|
|
540
|
+
modules: Object.fromEntries([...modules].map((id) => [id, prepared.modules[id]!])),
|
|
515
541
|
}
|
|
516
542
|
}
|
|
517
543
|
|
|
@@ -9,10 +9,11 @@ export interface EngineContract {
|
|
|
9
9
|
readonly httpRequests?: boolean
|
|
10
10
|
/** Code may call `context.fetch` for hosts declared by its `http` Capability. */
|
|
11
11
|
readonly codeFetch?: boolean
|
|
12
|
+
readonly flowState?: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
export const currentEngineContract = 'open-flow-engine/
|
|
15
|
+
/** v10 adds persistent Flow State to the published v9 Node compatibility and fetch contract. */
|
|
16
|
+
export const currentEngineContract = 'open-flow-engine/v10'
|
|
16
17
|
/** The published v8 Node compatibility contract, retained for existing Publications and Runs. */
|
|
17
18
|
export const nodejsEngineContract = 'open-flow-engine/v8/nodejs-compat-v1'
|
|
18
19
|
|
|
@@ -63,10 +64,12 @@ const nodejsContract: EngineContract = {
|
|
|
63
64
|
platformSource: platformSource(nodejsEngineContract),
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
const
|
|
67
|
+
const v9Contract: EngineContract = { ...nodejsContract, codeFetch: true, platformSource: platformSource('open-flow-engine/v9') }
|
|
68
|
+
const currentContract: EngineContract = { ...v9Contract, flowState: true, platformSource: platformSource(currentEngineContract) }
|
|
67
69
|
|
|
68
70
|
export function findEngineContract(contract: string): EngineContract | undefined {
|
|
69
71
|
if (contract == currentEngineContract) return currentContract
|
|
72
|
+
if (contract == 'open-flow-engine/v9') return v9Contract
|
|
70
73
|
if (contract == 'open-flow-engine/v8') return v8Contract
|
|
71
74
|
if (contract == nodejsEngineContract) return nodejsContract
|
|
72
75
|
if (contract == 'open-flow-engine/v7' || contract == 'open-flow-engine/v7/nodejs-compat-v1')
|