@oomol-lab/open-flow 0.1.0-beta.85 → 0.1.0-beta.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/browser/{FlowCanvas-CZoHhB1y.js → FlowCanvas-CHIaMaBv.js} +3 -3
  2. package/dist/browser/{createResourceDialog-Bi4fUnbD.js → createResourceDialog-C0E5yPe6.js} +3 -3
  3. package/dist/browser/{dialog-BzoSxhXQ.js → dialog-b3hsdu8i.js} +291 -143
  4. package/dist/browser/{exportPng-CXVWVlLc.js → exportPng-DGHcejgy.js} +2 -2
  5. package/dist/browser/{flowChanges-C567g-Pc.js → flowChanges-BvSWezEb.js} +375 -83
  6. package/dist/browser/{flowWorkspace-BNdBKtla.js → flowWorkspace-BKnMRClO.js} +1619 -475
  7. package/dist/browser/fr-CNTSedYU.js +593 -0
  8. package/dist/browser/ja-CxQBHr2J.js +593 -0
  9. package/dist/browser/ko-D1Exiv1h.js +593 -0
  10. package/dist/browser/licenses.md +1 -1
  11. package/dist/browser/{markdownContent-DF39sd9T.js → markdownContent-BR9-1ikU.js} +1 -1
  12. package/dist/browser/preview.css +181 -181
  13. package/dist/browser/preview.js +1994 -623
  14. package/dist/browser/ru-BzLZv0B7.js +593 -0
  15. package/dist/browser/{tooltip-DGj5pudg.js → tooltip-DSBF3bRw.js} +55 -23
  16. package/dist/browser/workbench.css +181 -181
  17. package/dist/browser/workbench.js +86 -17
  18. package/dist/browser/{workbenchSelect-BIaEVTx6.js → workbenchSelect-Di1Bm_-d.js} +2 -2
  19. package/dist/browser/{wrapLayout-C4moMKgb.js → wrapLayout-B0AT8_-i.js} +1 -1
  20. package/dist/browser/zh-CN-Dg_xSGde.js +593 -0
  21. package/dist/browser/zh-TW-oxuLwsV0.js +593 -0
  22. package/dist/command/index.js +61 -57
  23. package/dist/command/licenses.md +28 -4
  24. package/dist/common/authoring.d.ts +5 -5
  25. package/dist/common/authoringExamples.d.ts +3 -2
  26. package/dist/common/authoringSchema.d.ts +41 -4
  27. package/dist/common/catalogCache.d.ts +46 -0
  28. package/dist/common/connector-action.js +13 -10
  29. package/dist/common/connector-proxy.d.ts +96 -0
  30. package/dist/common/connector-proxy.js +5209 -0
  31. package/dist/common/control-api-conformance.js +312 -46
  32. package/dist/common/control-api.d.ts +12 -1
  33. package/dist/common/control-api.js +303 -41
  34. package/dist/common/control-requests.d.ts +4 -3
  35. package/dist/common/control-requests.js +14111 -171
  36. package/dist/common/engine-contract.d.ts +3 -2
  37. package/dist/common/event-source.js +55 -23
  38. package/dist/common/flow-authoring-node.d.ts +2 -0
  39. package/dist/common/flow-authoring.js +6936 -6781
  40. package/dist/common/flow-change-schema.d.ts +1 -1
  41. package/dist/common/flow-change.d.ts +2 -2
  42. package/dist/common/flow-change.js +91 -36
  43. package/dist/common/flow-connectionUsage.d.ts +1 -1
  44. package/dist/common/flow-encoding.js +93 -38
  45. package/dist/common/flow-requestParameters.d.ts +34 -0
  46. package/dist/common/flow-semantics.js +9429 -9236
  47. package/dist/common/flow-state.js +55 -23
  48. package/dist/common/flow-workflowFile.d.ts +3 -1
  49. package/dist/common/http-request.d.ts +6 -11
  50. package/dist/common/http-request.js +192 -141
  51. package/dist/common/licenses.md +1 -1
  52. package/dist/common/mcp.d.ts +3 -3
  53. package/dist/common/mcp.js +303 -41
  54. package/dist/common/run-events.js +1 -0
  55. package/dist/common/runtime-contract.js +10 -4
  56. package/dist/common/runtime-nodejs-compat.js +55 -23
  57. package/dist/common/scheduler-checkpoint.d.ts +3 -3
  58. package/dist/common/scheduler-dependencies.d.ts +22 -28
  59. package/dist/common/scheduler-node-execution.d.ts +1 -0
  60. package/dist/common/scheduler.d.ts +8 -2
  61. package/dist/common/scheduler.js +274 -95
  62. package/dist/common/workflow.js +51 -19
  63. package/dist/common/workflowAuthoring.d.ts +411 -0
  64. package/dist/render/browser/licenses.md +1 -1
  65. package/dist/render/browser/runtime.js +180 -48
  66. package/dist/render/node/index.js +85 -30
  67. package/dist/render/node/licenses.md +1 -1
  68. package/package.json +1 -1
  69. package/skills/open-flow/references/flow-authoring.md +14 -13
  70. package/src/base/common/jsonSchema.ts +11 -0
  71. package/src/connector/common/actionSchema.ts +4 -11
  72. package/src/connector/common/proxy.ts +152 -0
  73. package/src/control/common/api.ts +99 -3
  74. package/src/control/common/authoring.ts +23 -9
  75. package/src/control/common/authoringExamples.ts +9 -0
  76. package/src/control/common/authoringSchema.ts +21 -12
  77. package/src/control/common/catalogCache.ts +145 -0
  78. package/src/control/common/conformance.ts +9 -3
  79. package/src/control/common/connectorQueries.ts +17 -2
  80. package/src/control/common/requests.ts +1 -0
  81. package/src/control/common/workflowAuthoring.ts +120 -0
  82. package/src/execution/common/checkpoint.ts +18 -12
  83. package/src/execution/common/dependencies.ts +132 -15
  84. package/src/execution/common/events.ts +3 -0
  85. package/src/execution/common/node-execution.ts +22 -15
  86. package/src/execution/common/scheduler.ts +20 -31
  87. package/src/flow/common/change.ts +2 -1
  88. package/src/flow/common/changeSchema.ts +11 -1
  89. package/src/flow/common/connectionUsage.ts +4 -2
  90. package/src/flow/common/errorHandling.ts +3 -1
  91. package/src/flow/common/nodeChanges.ts +35 -18
  92. package/src/flow/common/requestParameters.ts +120 -0
  93. package/src/flow/common/semantics.ts +51 -1
  94. package/src/flow/common/sourceField.ts +3 -2
  95. package/src/flow/common/workflowFile.ts +2 -2
  96. package/src/http/common/httpRequest.ts +28 -78
  97. package/src/runtime/common/engineContract.ts +6 -3
@@ -0,0 +1,120 @@
1
+ import type { JsonValue } from '../../flow/common/change.ts'
2
+ import type { WorkflowFile } from '../../flow/common/workflowFile.ts'
3
+ import type { ControlClient } from './api.ts'
4
+ import type { AuthoringHost, AuthoringRead, AuthoringRequest, AuthoringSearch, AuthoringType } from './authoring.ts'
5
+
6
+ import { currentFlowModelVersion } from '../../flow/common/changeSchema.ts'
7
+ import { canonicalJsonBytes, digestBytes } from '../../flow/common/encoding.ts'
8
+ import { validateFlow } from '../../flow/common/semantics.ts'
9
+ import { decodeWorkflowFile } from '../../flow/common/workflowFile.ts'
10
+ import { currentEngineContract, findEngineContract } from '../../runtime/common/engineContract.ts'
11
+ import { workflowCandidate } from '../../workflow/common/workflow.ts'
12
+ import { AuthoringError, authoringDiagnostics, compileAuthoring, readAuthoring, searchAuthoring } from './authoring.ts'
13
+ import { authoringRequestSchema, authoringSchema, authoringSchemaQuery } from './authoringSchema.ts'
14
+
15
+ export interface WorkflowAuthoringOptions {
16
+ /** Flow API metadata is already normalized; no Connector catalog adapter runs here. */
17
+ readonly client?: ControlClient
18
+ readonly locale?: string
19
+ readonly engineContract?: string
20
+ /** Override only for callers that own their deterministic node/module identity scope. */
21
+ readonly id?: AuthoringHost['id']
22
+ }
23
+
24
+ async function revision(file: WorkflowFile): Promise<string> {
25
+ return digestBytes(canonicalJsonBytes(JSON.parse(JSON.stringify(file)) as JsonValue))
26
+ }
27
+
28
+ async function checkedRevision(file: WorkflowFile, expected?: string): Promise<string> {
29
+ const actual = await revision(file)
30
+ if (expected != null && expected != actual)
31
+ throw new AuthoringError('flow.revision-conflict', 'The workflow file changed. Read it before submitting another edit.', { revision: actual })
32
+ return actual
33
+ }
34
+
35
+ /** File objects in, file objects out. Persistence and transport belong to the caller. */
36
+ export function createWorkflowAuthoring(options: WorkflowAuthoringOptions = {}) {
37
+ function client(): ControlClient {
38
+ if (options.client == null) throw new AuthoringError('catalog.unavailable', 'A Control client is required to discover this capability.')
39
+ return options.client
40
+ }
41
+ const engineContract = options.engineContract ?? currentEngineContract
42
+ const engine = findEngineContract(engineContract)
43
+ if (engine == null) throw new AuthoringError('engine.unsupported', 'The Engine contract is not supported.', { engineContract })
44
+
45
+ async function check(file: WorkflowFile) {
46
+ const decoded = decodeWorkflowFile(file)
47
+ const validation = await validateFlow(decoded.content, engine!)
48
+ return {
49
+ revision: await revision(decoded),
50
+ valid: validation.valid,
51
+ diagnostics: authoringDiagnostics(decoded.content, validation.diagnostics),
52
+ }
53
+ }
54
+
55
+ return {
56
+ create(metadata: Omit<WorkflowFile, 'kind' | 'version' | 'content'> = {}): WorkflowFile {
57
+ return decodeWorkflowFile({
58
+ ...metadata,
59
+ kind: 'open-flow-workflow',
60
+ version: 1,
61
+ content: { modelVersion: currentFlowModelVersion, document: { graph: { nodes: {}, edges: [] }, bindings: {} }, modules: {} },
62
+ })
63
+ },
64
+
65
+ async read(file: WorkflowFile, query: AuthoringRead = {}) {
66
+ const decoded = decodeWorkflowFile(file)
67
+ return { revision: await checkedRevision(decoded, query.revision), data: readAuthoring(decoded.content, query) }
68
+ },
69
+
70
+ async search(file: WorkflowFile, query: AuthoringSearch) {
71
+ const decoded = decodeWorkflowFile(file)
72
+ return { revision: await checkedRevision(decoded, query.revision), data: searchAuthoring(decoded.content, query) }
73
+ },
74
+
75
+ async schema(query: { readonly type?: AuthoringType; readonly action?: string } = {}) {
76
+ const parsed = authoringSchemaQuery.parse(query)
77
+ if (parsed.action != null) {
78
+ const action = await client().getConnectorActionMetadata(parsed.action, undefined, undefined, options.locale ?? 'en')
79
+ return { action: action.actionId, name: action.name, inputs: action.inputs, outputs: action.outputs, version: 1 as const }
80
+ }
81
+ return { ...authoringSchema(parsed.type), version: 1 as const }
82
+ },
83
+
84
+ async edit(file: WorkflowFile, request: AuthoringRequest) {
85
+ const decoded = decodeWorkflowFile(file)
86
+ const parsed = authoringRequestSchema.parse(request)
87
+ const baseRevision = await checkedRevision(decoded, parsed.baseRevision)
88
+ const prefix = (await digestBytes(canonicalJsonBytes([baseRevision, parsed.requestId]))).slice(7, 31)
89
+ const ids = new Map<string, string>()
90
+ const compiled = await compileAuthoring(decoded.content, parsed, {
91
+ id: (label) => {
92
+ if (options.id != null) return options.id(label)
93
+ let id = ids.get(label)
94
+ if (id == null) {
95
+ id = `${prefix}_${ids.size}`
96
+ ids.set(label, id)
97
+ }
98
+ return id
99
+ },
100
+ action: (action) => client().getConnectorActionMetadata(action, undefined, undefined, options.locale ?? 'en'),
101
+ trigger: (key) => client().getTriggerKey(key),
102
+ openapi: (url) => client().loadOpenApiDocument(url),
103
+ workflow: async (flowId) => {
104
+ const candidate = workflowCandidate(await client().getFlow(flowId))
105
+ return candidate.problem == null ? client().getWorkflowContract(flowId) : candidate
106
+ },
107
+ })
108
+ const next = { ...decoded, content: compiled.content }
109
+ const validation = await check(next)
110
+ return {
111
+ file: next,
112
+ revision: validation.revision,
113
+ nodes: compiled.nodes,
114
+ validation: { status: validation.valid ? ('valid' as const) : ('invalid' as const), diagnostics: validation.diagnostics },
115
+ }
116
+ },
117
+
118
+ check,
119
+ }
120
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Graph, JsonValue } from '../../flow/common/change.ts'
2
2
  import type { AgentCheckpoint } from '../../runtime/common/runtime.ts'
3
- import type { DependenciesCheckpoint } from './dependencies.ts'
3
+ import type { Dependencies, DependenciesCheckpoint } from './dependencies.ts'
4
4
  import type { ExecutableNode } from './node-execution.ts'
5
5
 
6
6
  import { z } from 'zod'
@@ -9,7 +9,7 @@ import { nodeErrorSchema, nodeSystemOutputPorts } from '../../flow/common/errorH
9
9
  import { nodeInputPorts as nodePorts, isResolutionNode, resolutionActions } from '../../flow/common/graph.ts'
10
10
  import { matchesSchema } from '../../flow/common/schema.ts'
11
11
  import { matchesTriggerOutputs } from '../../trigger/common/contract.ts'
12
- import { dependenciesSchema, dependencyValues } from './dependencies.ts'
12
+ import { decodeDependenciesCheckpoint } from './dependencies.ts'
13
13
  import { agentCheckpointSchema, agentConfig, agentApproval, jsonEqual, jsonValue, outputRecord, usesPending, validateOutputs } from './node-execution.ts'
14
14
 
15
15
  export interface PendingWait {
@@ -44,7 +44,7 @@ export interface FlowRunCheckpoint {
44
44
  >
45
45
  readonly counts: Readonly<Record<string, Readonly<Record<string, number>>>>
46
46
  readonly dependencies: DependenciesCheckpoint
47
- readonly version: 6
47
+ readonly version: 7
48
48
  readonly waits: readonly PendingWait[]
49
49
  readonly agents: Readonly<
50
50
  Record<
@@ -81,7 +81,8 @@ export function decodeFlowRunCheckpoint(input: unknown): FlowRunCheckpoint {
81
81
  ['agents', 'bindingValues', 'counts', 'dependencies', 'inputs', 'results', 'version', 'waits', ...(source.calls === undefined ? [] : ['calls'])],
82
82
  'Flow Run checkpoint',
83
83
  )
84
- if (source.version != 6) throw new Error('Flow Run checkpoint version is unsupported.')
84
+ const version = source.version
85
+ if (version !== 6 && version !== 7) throw new Error('Flow Run checkpoint version is unsupported.')
85
86
  const bindingValues = Object.fromEntries(
86
87
  Object.entries(checkpointRecord(source.bindingValues, 'Checkpoint bindings')).map(([id, value]) => {
87
88
  if (typeof value != 'string') throw new Error('Checkpoint binding must be a string.')
@@ -119,7 +120,7 @@ export function decodeFlowRunCheckpoint(input: unknown): FlowRunCheckpoint {
119
120
  }),
120
121
  )
121
122
  const counts = z.record(z.string(), z.record(z.string(), z.number().int().positive().max(Number.MAX_SAFE_INTEGER))).parse(source.counts)
122
- const dependencies = dependenciesSchema.parse(source.dependencies)
123
+ const dependencies = decodeDependenciesCheckpoint(source.dependencies, version)
123
124
  const savedWaitFields = {
124
125
  jobId: z.string().min(1),
125
126
  nodeId: z.string().min(1),
@@ -171,12 +172,13 @@ export function decodeFlowRunCheckpoint(input: unknown): FlowRunCheckpoint {
171
172
  if (Object.values(dependencies.nodes).some((state) => state.status == 'running' && !jobs.has(state.jobId!)))
172
173
  throw new Error('Checkpoint dependency has no waiting execution.')
173
174
  if (Object.keys(agents).some((id) => !jobs.has(id))) throw new Error('Checkpoint Agent has no waiting execution.')
174
- return { agents, bindingValues, counts, dependencies, inputs, results, version: 6, waits: pendingWaits, ...(source.calls === undefined ? {} : { calls }) }
175
+ return { agents, bindingValues, counts, dependencies, inputs, results, version: 7, waits: pendingWaits, ...(source.calls === undefined ? {} : { calls }) }
175
176
  }
176
177
 
177
178
  export function validateCheckpoint(
178
179
  target: { readonly graph: Graph },
179
180
  checkpoint: FlowRunCheckpoint,
181
+ dependencies: Dependencies,
180
182
  resolveInputs: (nodeId: string, node: ExecutableNode) => Readonly<Record<string, JsonValue>>,
181
183
  ): void {
182
184
  const launch = checkpoint.inputs
@@ -249,12 +251,16 @@ export function validateCheckpoint(
249
251
  throw new Error('Checkpoint launch inputs do not match the prepared graph.')
250
252
  }
251
253
 
252
- for (const [nodeId, state] of Object.entries(checkpoint.dependencies.nodes)) {
253
- const frame = dependencyValues(state.input)
254
- validateResults(frame, true, state.input)
254
+ for (const result of checkpoint.dependencies.results) validateResults({ [result.nodeId]: result.outputs }, true, { [result.nodeId]: result })
255
+ const graphTriggers = Object.keys(target.graph.nodes).filter((id) => isTriggerNode(target.graph.nodes[id]!))
256
+ for (const [nodeId, state] of Object.entries(dependencies.state.nodes)) {
255
257
  if (!isTriggerNode(target.graph.nodes[nodeId]!) && (state.status == 'running' || state.status == 'completed')) {
256
- const triggers = Object.keys(frame).filter((id) => isTriggerNode(target.graph.nodes[id]!))
257
- if (triggers.length != 1 || triggers[0] != triggerIds[0] || !jsonEqual(frame[triggers[0]!]!, checkpoint.results[triggers[0]!]!.outputs))
258
+ const triggers = graphTriggers.filter((id) => dependencies.input(nodeId, id) != null)
259
+ if (
260
+ triggers.length != 1 ||
261
+ triggers[0] != triggerIds[0] ||
262
+ !jsonEqual(dependencies.input(nodeId, triggers[0]!)!.outputs, checkpoint.results[triggers[0]!]!.outputs)
263
+ )
258
264
  throw new Error('Checkpoint input Trigger does not match the Run.')
259
265
  }
260
266
  if (state.output != null) validateResults({ [nodeId]: state.output }, state.status == 'running')
@@ -284,7 +290,7 @@ export function validateCheckpoint(
284
290
  for (const waiting of checkpoint.waits) {
285
291
  const node = target.graph.nodes[waiting.nodeId]
286
292
  if (node == null || isTriggerNode(node)) throw new Error('Checkpoint waiting node is missing.')
287
- const state = checkpoint.dependencies.nodes[waiting.nodeId]!
293
+ const state = dependencies.state.nodes[waiting.nodeId]!
288
294
  const expectedOutput = waiting.pending == null ? undefined : { pending: waiting.pending }
289
295
  if ((state.output == null) != (expectedOutput == null) || (state.output != null && !jsonEqual(state.output, expectedOutput!)))
290
296
  throw new Error('Checkpoint waiting dependency output is inconsistent.')
@@ -1,9 +1,23 @@
1
1
  import type { Graph, JsonValue } from '../../flow/common/change.ts'
2
2
 
3
+ import { dequal } from 'dequal/lite'
3
4
  import { z } from 'zod'
4
5
  import { isTriggerNode } from '../../flow/common/change.ts'
5
6
 
6
7
  export interface DependenciesCheckpoint {
8
+ nextId: number
9
+ iterations: Record<string, number>
10
+ nodes: Record<string, Omit<DependencyState, 'input' | 'output' | 'systemOutputs'> & { input: number; output?: number }>
11
+ frames: readonly { readonly parent?: number; readonly input: Readonly<Record<string, number>> }[]
12
+ results: readonly {
13
+ readonly nodeId: string
14
+ readonly execution: number
15
+ readonly outputs: Readonly<Record<string, JsonValue>>
16
+ readonly systemOutputs?: Readonly<Record<string, JsonValue>>
17
+ }[]
18
+ }
19
+
20
+ interface DependenciesState {
7
21
  nextId: number
8
22
  iterations: Record<string, number>
9
23
  nodes: Record<string, DependencyState>
@@ -29,15 +43,11 @@ const inputSchema = z.record(
29
43
  z.string(),
30
44
  z.strictObject({ execution: z.number().int().nonnegative(), outputs: outputsSchema, systemOutputs: outputsSchema.optional() }),
31
45
  )
32
- type InputSnapshot = DependenciesCheckpoint['nodes'][string]['input']
46
+ type InputSnapshot = DependencyState['input']
33
47
  type InputFrame =
34
48
  | { values: InputSnapshot }
35
49
  | { values?: InputSnapshot; source: string; result: InputSnapshot[string]; parent: InputFrame; cache: Map<string, InputSnapshot[string] | undefined> }
36
50
 
37
- export function dependencyValues(input: InputSnapshot): Readonly<Record<string, Readonly<Record<string, JsonValue>>>> {
38
- return Object.fromEntries(Object.entries(input).map(([id, result]) => [id, result.outputs]))
39
- }
40
-
41
51
  const stateSchema = z.strictObject({
42
52
  id: z.number().int().nonnegative(),
43
53
  status: z.enum(['pending', 'running', 'completed', 'skipped']),
@@ -49,11 +59,97 @@ const stateSchema = z.strictObject({
49
59
  jobId: z.string().optional(),
50
60
  seeded: z.boolean(),
51
61
  })
52
- export const dependenciesSchema = z.strictObject({
62
+ const legacyDependenciesSchema = z.strictObject({
53
63
  nextId: z.number().int().nonnegative(),
54
64
  iterations: z.record(z.string(), z.number().int().nonnegative()),
55
65
  nodes: z.record(z.string(), stateSchema),
56
66
  })
67
+
68
+ const referenceSchema = z.number().int().nonnegative()
69
+ const dependenciesSchema = z.strictObject({
70
+ nextId: referenceSchema,
71
+ iterations: z.record(z.string(), referenceSchema),
72
+ nodes: z.record(
73
+ z.string(),
74
+ stateSchema.omit({ input: true, output: true, systemOutputs: true }).extend({ input: referenceSchema, output: referenceSchema.optional() }),
75
+ ),
76
+ frames: z.array(z.strictObject({ parent: referenceSchema.optional(), input: z.record(z.string(), referenceSchema) })),
77
+ results: z.array(z.strictObject({ nodeId: z.string(), execution: referenceSchema, outputs: outputsSchema, systemOutputs: outputsSchema.optional() })),
78
+ })
79
+
80
+ export function decodeDependenciesCheckpoint(input: unknown, version: 6 | 7): DependenciesCheckpoint {
81
+ const saved = version == 6 ? saveDependencies(legacyDependenciesSchema.parse(input)) : dependenciesSchema.parse(input)
82
+ for (const [index, frame] of saved.frames.entries()) {
83
+ if (frame.parent != null && (frame.parent >= index || Object.keys(frame.input).length != 1)) throw new Error('Checkpoint input frame parent is invalid.')
84
+ for (const [nodeId, reference] of Object.entries(frame.input)) {
85
+ const result = saved.results[reference]
86
+ if (result == null || result.nodeId != nodeId) throw new Error('Checkpoint input result reference is invalid.')
87
+ }
88
+ }
89
+ for (const [nodeId, state] of Object.entries(saved.nodes)) {
90
+ if (saved.frames[state.input] == null) throw new Error('Checkpoint input frame reference is invalid.')
91
+ if (state.output != null) {
92
+ const result = saved.results[state.output]
93
+ if (result == null || result.nodeId != nodeId || result.execution != state.id) throw new Error('Checkpoint output result reference is invalid.')
94
+ }
95
+ }
96
+ for (const result of saved.results) {
97
+ if (saved.nodes[result.nodeId] == null || result.execution > saved.nodes[result.nodeId]!.id || result.execution >= saved.nextId)
98
+ throw new Error('Checkpoint dependency result identity is invalid.')
99
+ }
100
+ return saved
101
+ }
102
+
103
+ function saveDependencies(state: DependenciesState, frames?: ReadonlyMap<string, InputFrame>): DependenciesCheckpoint {
104
+ const results: DependenciesCheckpoint['results'][number][] = []
105
+ const references = new Map<string, number>()
106
+ const saveResult = (nodeId: string, result: InputSnapshot[string]) => {
107
+ const key = JSON.stringify([nodeId, result.execution, Object.keys(result.outputs).toSorted()])
108
+ let reference = references.get(key)
109
+ if (reference == null) {
110
+ reference = results.length
111
+ references.set(key, reference)
112
+ results.push({ nodeId, ...result })
113
+ } else if (!dequal(results[reference]!.outputs, result.outputs) || !dequal(results[reference]!.systemOutputs, result.systemOutputs))
114
+ throw new Error('Checkpoint invocation results conflict.')
115
+ return reference
116
+ }
117
+ const savedFrames: DependenciesCheckpoint['frames'][number][] = []
118
+ const frameIds = new Map<InputFrame, number>()
119
+ const saveFrame = (frame: InputFrame): number => {
120
+ const existing = frameIds.get(frame)
121
+ if (existing != null) return existing
122
+ const saved =
123
+ 'source' in frame
124
+ ? { parent: saveFrame(frame.parent), input: { [frame.source]: saveResult(frame.source, frame.result) } }
125
+ : { input: Object.fromEntries(Object.entries(frame.values).map(([id, result]) => [id, saveResult(id, result)])) }
126
+ const id = savedFrames.length
127
+ savedFrames.push(saved)
128
+ frameIds.set(frame, id)
129
+ return id
130
+ }
131
+ const nodes = Object.fromEntries(
132
+ Object.entries(state.nodes).map(([id, current]) => {
133
+ const { id: execution, status, previous, released, jobId, seeded, output, systemOutputs } = current
134
+ return [
135
+ id,
136
+ {
137
+ id: execution,
138
+ status,
139
+ previous,
140
+ released,
141
+ seeded,
142
+ ...(jobId == null ? {} : { jobId }),
143
+ input: saveFrame(frames?.get(id) ?? { values: current.input }),
144
+ ...(output == null
145
+ ? {}
146
+ : { output: saveResult(id, { execution: current.id, outputs: output, ...(systemOutputs == null ? {} : { systemOutputs }) }) }),
147
+ },
148
+ ]
149
+ }),
150
+ )
151
+ return { nextId: state.nextId, iterations: { ...state.iterations }, nodes, frames: savedFrames, results }
152
+ }
57
153
  type Edge = Graph['edges'][number]
58
154
  interface Loop {
59
155
  readonly header: string
@@ -64,7 +160,7 @@ interface Loop {
64
160
 
65
161
  /** Dependency completion belongs to an invocation, never to a Run-wide latest-result table. */
66
162
  export class Dependencies {
67
- readonly state: DependenciesCheckpoint
163
+ readonly state: DependenciesState
68
164
  private readonly forward: Edge[] = []
69
165
  private readonly loops = new Map<string, Loop>()
70
166
  private readonly innerLoopsFirst: readonly Loop[]
@@ -135,15 +231,40 @@ export class Dependencies {
135
231
  this.loops.set(header, { header, feedback: edges, body, continuation: descendants })
136
232
  }
137
233
  this.innerLoopsFirst = [...this.loops.values()].toSorted((a, b) => a.body.size - b.body.size || a.header.localeCompare(b.header))
138
- this.state = saved == null ? { nextId: 0, iterations: {}, nodes: {} } : structuredClone(saved)
234
+ this.state = { nextId: saved?.nextId ?? 0, iterations: { ...saved?.iterations }, nodes: {} }
139
235
  if (saved == null) for (const id of ids) this.state.nodes[id] = this.fresh()
140
- else this.validate()
236
+ else {
237
+ const frames: InputFrame[] = []
238
+ for (const frame of saved.frames) {
239
+ if (frame.parent == null)
240
+ frames.push({ values: Object.fromEntries(Object.entries(frame.input).map(([id, reference]) => [id, saved.results[reference]!])) })
241
+ else {
242
+ const [source, reference] = Object.entries(frame.input)[0]!
243
+ frames.push({ source, result: saved.results[reference]!, parent: frames[frame.parent]!, cache: new Map() })
244
+ }
245
+ }
246
+ for (const [id, current] of Object.entries(saved.nodes)) {
247
+ const { input, output, ...node } = current
248
+ const result = output == null ? undefined : saved.results[output]!
249
+ const frame = frames[input]!
250
+ this.frames.set(id, frame)
251
+ const restored: DependencyState = { ...node, input: {}, ...(result == null ? {} : { output: result.outputs, systemOutputs: result.systemOutputs }) }
252
+ if (this.loops.size == 0) Object.defineProperty(restored, 'input', { enumerable: true, configurable: true, get: () => this.snapshot(frame) })
253
+ else restored.input = this.snapshot(frame)
254
+ this.state.nodes[id] = restored
255
+ }
256
+ this.validate()
257
+ }
141
258
  for (const id of ids) {
142
259
  if (this.state.nodes[id]!.status == 'pending') this.candidates.add(id)
143
- this.frames.set(id, { values: this.state.nodes[id]!.input })
260
+ if (saved == null) this.frames.set(id, { values: this.state.nodes[id]!.input })
144
261
  }
145
262
  }
146
263
 
264
+ checkpoint(): DependenciesCheckpoint {
265
+ return saveDependencies(this.state, this.loops.size == 0 ? this.frames : undefined)
266
+ }
267
+
147
268
  private fresh(): DependencyState {
148
269
  return { id: this.state.nextId++, status: 'pending', input: {}, previous: [], released: [], seeded: false }
149
270
  }
@@ -170,11 +291,7 @@ export class Dependencies {
170
291
  throw new Error('Checkpoint predecessor does not match the graph.')
171
292
  if (state.previous.length != new Set(state.previous).size) throw new Error('Checkpoint predecessors are duplicated.')
172
293
  if (state.status == 'completed' && state.output == null) throw new Error('Checkpoint completed dependency has no output.')
173
- if (state.previous.some((source) => state.input[source] == null)) throw new Error('Checkpoint predecessor has no input snapshot.')
174
- for (const [source, result] of Object.entries(state.input)) {
175
- if (nodes[source] == null || result.execution > nodes[source]!.id || result.execution >= nextId)
176
- throw new Error('Checkpoint dependency result identity is invalid.')
177
- }
294
+ if (state.previous.some((source) => this.input(id, source) == null)) throw new Error('Checkpoint predecessor has no input snapshot.')
178
295
  }
179
296
  if (Object.keys(iterations).some((id) => !this.loops.has(id))) throw new Error('Checkpoint loop scope is invalid.')
180
297
  }
@@ -19,6 +19,7 @@ const nodeKinds = new Set([
19
19
  'approval',
20
20
  'condition',
21
21
  'connector',
22
+ 'connector-proxy',
22
23
  'decision',
23
24
  'javascript',
24
25
  'openapi',
@@ -53,6 +54,7 @@ function nodeKind(
53
54
  | 'approval'
54
55
  | 'condition'
55
56
  | 'connector'
57
+ | 'connector-proxy'
56
58
  | 'decision'
57
59
  | 'javascript'
58
60
  | 'openapi'
@@ -74,6 +76,7 @@ function nodeKind(
74
76
  | 'approval'
75
77
  | 'condition'
76
78
  | 'connector'
79
+ | 'connector-proxy'
77
80
  | 'decision'
78
81
  | 'javascript'
79
82
  | 'openapi'
@@ -20,6 +20,7 @@ interface NodeExecutionContext {
20
20
  readonly createId: FlowRunOptions['createId']
21
21
  readonly emit: (event: SchedulerEvent) => Effect.Effect<void, Error>
22
22
  readonly invokeTask: FlowRunOptions['invokeTask']
23
+ readonly admitTask?: FlowRunOptions['admitTask']
23
24
  readonly projectFailure: FlowRunOptions['projectFailure']
24
25
  }
25
26
 
@@ -105,6 +106,7 @@ function nodeKind(
105
106
  | 'approval'
106
107
  | 'condition'
107
108
  | 'connector'
109
+ | 'connector-proxy'
108
110
  | 'decision'
109
111
  | 'javascript'
110
112
  | 'openapi'
@@ -217,7 +219,11 @@ export function validateOutputs(nodeId: string, node: ExecutableNode, value: unk
217
219
  : {}
218
220
  const outputs = outputRecord(
219
221
  jsonValue(
220
- Object.fromEntries(Object.entries(raw).map(([handle, item]) => [handle, Object.hasOwn(ports, handle) && item === undefined ? null : item])),
222
+ Object.fromEntries(
223
+ Object.entries(raw)
224
+ .filter(([handle]) => !discardUndeclared || Object.hasOwn(ports, handle))
225
+ .map(([handle, item]) => [handle, Object.hasOwn(ports, handle) && item === undefined ? null : item]),
226
+ ),
221
227
  `Node "${nodeId}" outputs`,
222
228
  ),
223
229
  nodeId,
@@ -227,11 +233,7 @@ export function validateOutputs(nodeId: string, node: ExecutableNode, value: unk
227
233
  const validated: [string, JsonValue][] = []
228
234
  for (const [handle, output] of Object.entries(complete)) {
229
235
  const port = ports[handle]
230
- if (port == null) {
231
- if (discardUndeclared) continue
232
- throw new PortValidationError(`Node "${nodeId}" output "${handle}" does not match its declaration.`)
233
- }
234
- if (!(output === null && port.nullable) && !matchesSchema(output, port.jsonSchema))
236
+ if (port == null || (!(output === null && port.nullable) && !matchesSchema(output, port.jsonSchema)))
235
237
  throw new PortValidationError(`Node "${nodeId}" output "${handle}" does not match its declaration.`)
236
238
  validated.push([handle, output])
237
239
  }
@@ -316,8 +318,9 @@ export function executeNode(
316
318
  const { nodeId, node, jobId, nodeInputs, flowId, runId, graph, dependencies, previousIds, saved, workflowRunId, resolution } = input
317
319
  const config = agentConfig(node)
318
320
  const nodeRemainingMs = saved?.remainingMs ?? node.timeoutMs
319
- const startedAt = performance.now()
321
+ let startedAt = 0
320
322
  const execution = Effect.gen(function* () {
323
+ startedAt = performance.now()
321
324
  const kind = nodeKind(node)
322
325
  const title = nodeTitle(node)
323
326
  const projectedInputs = Object.fromEntries(
@@ -461,12 +464,16 @@ export function executeNode(
461
464
  catch: (error) => new NodeValidationError('node.output-invalid', error),
462
465
  })
463
466
  })
464
- return nodeRemainingMs == null
465
- ? execution
466
- : execution.pipe(
467
- Effect.timeoutOrElse({
468
- duration: nodeRemainingMs,
469
- orElse: () => Effect.fail(new Error(`Node "${nodeId}" timed out.`)),
470
- }),
471
- )
467
+ const timed =
468
+ nodeRemainingMs == null
469
+ ? execution
470
+ : execution.pipe(
471
+ Effect.timeoutOrElse({
472
+ duration: nodeRemainingMs,
473
+ orElse: () => Effect.fail(new Error(`Node "${nodeId}" timed out.`)),
474
+ }),
475
+ )
476
+ // Waiting for deployment admission is queueing, not node execution time.
477
+ // Pending workflow calls only read child state and do not consume Task capacity.
478
+ return node.kind == 'task' && workflowRunId == null && context.admitTask != null ? Effect.andThen(context.admitTask({ runId, nodeId, jobId }), timed) : timed
472
479
  }
@@ -67,6 +67,7 @@ export type SchedulerEvent =
67
67
  | 'approval'
68
68
  | 'condition'
69
69
  | 'connector'
70
+ | 'connector-proxy'
70
71
  | 'decision'
71
72
  | 'javascript'
72
73
  | 'openapi'
@@ -157,8 +158,6 @@ export interface WaitHost {
157
158
  readonly resolutions: (waitIds: readonly string[], block: boolean) => Effect.Effect<Readonly<Record<string, WaitResolution>>, Error>
158
159
  }
159
160
 
160
- export const waitRetentionMs = 120_000
161
-
162
161
  export type FlowRunOutcome =
163
162
  | FlowRunResult
164
163
  | {
@@ -199,11 +198,19 @@ export interface SchedulerFailure {
199
198
  readonly message: string
200
199
  }
201
200
 
201
+ export interface TaskAdmission {
202
+ readonly runId: string
203
+ readonly nodeId: string
204
+ readonly jobId: string
205
+ }
206
+
202
207
  export type FlowRunOptions = {
203
208
  readonly createId: () => string
204
209
  readonly emit?: (event: SchedulerEvent) => Effect.Effect<void, Error>
205
210
  readonly flowId: string
206
211
  readonly invokeTask: (invocation: TaskInvocation) => Effect.Effect<unknown, Error>
212
+ /** Deployment admission for each Task invocation; it completes before the node timeout starts. */
213
+ readonly admitTask?: (admission: TaskAdmission) => Effect.Effect<void, Error>
207
214
  readonly projectFailure?: (error: unknown) => SchedulerFailure
208
215
  readonly runId: string
209
216
  readonly waits?: WaitHost
@@ -239,6 +246,7 @@ interface RunContext {
239
246
  readonly createId: FlowRunOptions['createId']
240
247
  readonly emit: (event: SchedulerEvent) => Effect.Effect<void, Error>
241
248
  readonly invokeTask: FlowRunOptions['invokeTask']
249
+ readonly admitTask: FlowRunOptions['admitTask']
242
250
  readonly prepared: PreparedFlow
243
251
  readonly projectFailure: FlowRunOptions['projectFailure']
244
252
  readonly waits: WaitHost | undefined
@@ -295,7 +303,6 @@ function runGraph(
295
303
  let measuredAt = yield* Clock.currentTimeMillis
296
304
  let nextCallCheckAt = measuredAt + (resume == null ? 1000 : 0)
297
305
  let readyWorkflowRunIds = resume?.readyWorkflowRunIds
298
- let idleSince: number | undefined
299
306
  const agents: Record<string, FlowRunCheckpoint['agents'][string]> = { ...resume?.checkpoint.agents }
300
307
  const resolveInputs = (nodeId: string, node: ExecutableNode) => resolveNodeInputs(nodeId, node, dependencies, context.bindingValues, launch[nodeId])
301
308
  const commit = (
@@ -479,7 +486,7 @@ function runGraph(
479
486
  }
480
487
  if (resume != null) {
481
488
  yield* Effect.try({
482
- try: () => validateCheckpoint(target, resume.checkpoint, resolveInputs),
489
+ try: () => validateCheckpoint(target, resume.checkpoint, dependencies, resolveInputs),
483
490
  catch: (error) => (error instanceof Error ? error : new Error(String(error))),
484
491
  })
485
492
  }
@@ -487,15 +494,11 @@ function runGraph(
487
494
  if (trigger != null) dependencies.start(trigger.nodeId, trigger.outputs)
488
495
  }
489
496
 
490
- const applyResolutions = (resolutions: Readonly<Record<string, WaitResolution>>, now: number) => {
497
+ const applyResolutions = (resolutions: Readonly<Record<string, WaitResolution>>) => {
491
498
  for (const saved of pendingWaits.values()) {
492
499
  const resolution = resolutions[saved.waitId]
493
500
  if (resolution == null) continue
494
501
  const { action, resolvedAt, comment } = resolution
495
- if (idleSince != null) {
496
- idleSince = undefined
497
- measuredAt = now
498
- }
499
502
  pendingWaits.delete(saved.jobId)
500
503
  const node = target.graph.nodes[saved.nodeId] as ExecutableNode
501
504
  runNode(
@@ -526,7 +529,7 @@ function runGraph(
526
529
  }
527
530
  while (true) {
528
531
  const now = yield* Clock.currentTimeMillis
529
- if (idleSince == null && remainingMs != null) remainingMs -= now - measuredAt
532
+ if (remainingMs != null) remainingMs -= now - measuredAt
530
533
  measuredAt = now
531
534
  if (remainingMs != null && remainingMs <= 0) return yield* Effect.fail(new Error('Run exceeded its execution deadline.'))
532
535
  if (firstCause != null) {
@@ -547,7 +550,6 @@ function runGraph(
547
550
  [...pendingWaits.values()].map((wait) => wait.waitId),
548
551
  false,
549
552
  ),
550
- now,
551
553
  )
552
554
  if (pendingCalls.size > 0 && now >= nextCallCheckAt) {
553
555
  for (const call of pendingCalls.values()) {
@@ -572,17 +574,14 @@ function runGraph(
572
574
  if (dependencies.incomplete) return yield* Effect.fail(new Error('Execution graph contains unresolved dependencies.'))
573
575
  break
574
576
  }
575
- if (running > 0 || ready.length > 0) idleSince = undefined
576
- else idleSince ??= now
577
- const left = idleSince == null ? remainingMs : waitRetentionMs - (now - idleSince)
578
- if (idleSince != null && (left! <= 0 || pendingCalls.size > 0)) {
577
+ if (running == 0 && ready.length == 0) {
579
578
  const checkpoint: FlowRunCheckpoint = {
580
579
  bindingValues: context.bindingValues,
581
580
  inputs: launch,
582
581
  results: Object.fromEntries(completed),
583
582
  counts: context.counts,
584
- dependencies: dependencies.state,
585
- version: 6,
583
+ dependencies: dependencies.checkpoint(),
584
+ version: 7,
586
585
  agents,
587
586
  waits: [...pendingWaits.values()],
588
587
  ...(pendingCalls.size == 0 ? {} : { calls: [...pendingCalls.values()] }),
@@ -602,9 +601,9 @@ function runGraph(
602
601
  let next = Effect.raceFirst(activity, decision)
603
602
  if (pendingCalls.size > 0)
604
603
  next = Effect.raceFirst(next, Effect.sleep(Math.max(0, nextCallCheckAt - (yield* Clock.currentTimeMillis))).pipe(Effect.as({})))
605
- if (left != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, left)).pipe(Effect.as({})))
604
+ if (remainingMs != null) next = Effect.raceFirst(next, Effect.sleep(Math.max(0, remainingMs)).pipe(Effect.as({})))
606
605
  const resolutions = yield* next
607
- applyResolutions(resolutions, yield* Clock.currentTimeMillis)
606
+ applyResolutions(resolutions)
608
607
  }
609
608
  yield* context.emit({ progress: 100, runId, type: 'run.progress' })
610
609
  const outputNodeId = order.find((nodeId) => target.graph.nodes[nodeId]?.kind == 'outputs')
@@ -655,18 +654,7 @@ export function runFlow(prepared: PreparedFlow, options: FlowRunOptions): Effect
655
654
  results: Object.fromEntries(Object.entries(decoded.results).map(([id, result]) => [id, { ...result, outputs: outputs(id, result.outputs) }])),
656
655
  dependencies: {
657
656
  ...decoded.dependencies,
658
- nodes: Object.fromEntries(
659
- Object.entries(decoded.dependencies.nodes).map(([id, state]) => [
660
- id,
661
- {
662
- ...state,
663
- ...(state.output == null ? {} : { output: outputs(id, state.output) }),
664
- input: Object.fromEntries(
665
- Object.entries(state.input).map(([source, result]) => [source, { ...result, outputs: outputs(source, result.outputs) }]),
666
- ),
667
- },
668
- ]),
669
- ),
657
+ results: decoded.dependencies.results.map((result) => ({ ...result, outputs: outputs(result.nodeId, result.outputs) })),
670
658
  },
671
659
  waits: decoded.waits.map((wait) => (wait.pending == null ? wait : Object.assign({}, wait, { pending: withWaitNotificationMessage(wait.pending) }))),
672
660
  }
@@ -691,6 +679,7 @@ export function runFlow(prepared: PreparedFlow, options: FlowRunOptions): Effect
691
679
  createId: options.createId,
692
680
  emit,
693
681
  invokeTask: options.invokeTask,
682
+ admitTask: options.admitTask,
694
683
  prepared,
695
684
  projectFailure: options.projectFailure,
696
685
  waits: options.waits,