@mokronos/wfkit 0.1.0
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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/authoring.d.ts +7 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +8 -0
- package/dist/authoring.js.map +1 -0
- package/dist/cli/integrations.d.ts +3 -0
- package/dist/cli/integrations.d.ts.map +1 -0
- package/dist/cli/integrations.js +40 -0
- package/dist/cli/integrations.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +591 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/core.d.ts +198 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1061 -0
- package/dist/core.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +154 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +24 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +3 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +28 -0
- package/dist/replay.js.map +1 -0
- package/dist/runtime.d.ts +70 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +183 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +18 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +19 -0
- package/dist/schema.js.map +1 -0
- package/dist/schemas.d.ts +1198 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +383 -0
- package/dist/schemas.js.map +1 -0
- package/dist/sdk/artifact.d.ts +41 -0
- package/dist/sdk/artifact.d.ts.map +1 -0
- package/dist/sdk/artifact.js +94 -0
- package/dist/sdk/artifact.js.map +1 -0
- package/dist/sdk/graph.d.ts +15 -0
- package/dist/sdk/graph.d.ts.map +1 -0
- package/dist/sdk/graph.js +388 -0
- package/dist/sdk/graph.js.map +1 -0
- package/dist/sdk/index.d.ts +14 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +8 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/integrations.d.ts +18 -0
- package/dist/sdk/integrations.d.ts.map +1 -0
- package/dist/sdk/integrations.js +20 -0
- package/dist/sdk/integrations.js.map +1 -0
- package/dist/sdk/json.d.ts +4 -0
- package/dist/sdk/json.d.ts.map +1 -0
- package/dist/sdk/json.js +31 -0
- package/dist/sdk/json.js.map +1 -0
- package/dist/sdk/loader.d.ts +10 -0
- package/dist/sdk/loader.d.ts.map +1 -0
- package/dist/sdk/loader.js +75 -0
- package/dist/sdk/loader.js.map +1 -0
- package/dist/sdk/sdk.d.ts +102 -0
- package/dist/sdk/sdk.d.ts.map +1 -0
- package/dist/sdk/sdk.js +669 -0
- package/dist/sdk/sdk.js.map +1 -0
- package/dist/sdk/sqlite.d.ts +8 -0
- package/dist/sdk/sqlite.d.ts.map +1 -0
- package/dist/sdk/sqlite.js +288 -0
- package/dist/sdk/sqlite.js.map +1 -0
- package/dist/signal.d.ts +17 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +78 -0
- package/dist/signal.js.map +1 -0
- package/dist/testing/index.d.ts +35 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +244 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +67 -0
- package/src/authoring.ts +32 -0
- package/src/cli/integrations.ts +75 -0
- package/src/cli/main.ts +793 -0
- package/src/core.ts +1515 -0
- package/src/errors.ts +19 -0
- package/src/events.ts +37 -0
- package/src/index.ts +109 -0
- package/src/replay.ts +29 -0
- package/src/runtime.ts +314 -0
- package/src/schema.ts +34 -0
- package/src/schemas.ts +473 -0
- package/src/sdk/artifact.ts +176 -0
- package/src/sdk/graph.ts +486 -0
- package/src/sdk/index.ts +64 -0
- package/src/sdk/integrations.ts +49 -0
- package/src/sdk/json.ts +40 -0
- package/src/sdk/loader.ts +99 -0
- package/src/sdk/sdk.ts +911 -0
- package/src/sdk/sqlite.ts +405 -0
- package/src/signal.ts +116 -0
- package/src/testing/index.ts +341 -0
package/src/sdk/graph.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DefinedWorkflow,
|
|
3
|
+
InMemoryDeterminismState,
|
|
4
|
+
OrchestrationCall,
|
|
5
|
+
SecretResolver,
|
|
6
|
+
Step
|
|
7
|
+
} from "../core.ts"
|
|
8
|
+
import { Schema } from "effect"
|
|
9
|
+
import { createInMemoryDeterminismState } from "../core.ts"
|
|
10
|
+
import type { WorkflowEvent } from "../events.ts"
|
|
11
|
+
import {
|
|
12
|
+
jsonSchemaOf,
|
|
13
|
+
type JsonSchema,
|
|
14
|
+
type WorkflowArtifactGraph,
|
|
15
|
+
type WorkflowGraph,
|
|
16
|
+
type WorkflowGraphEdge,
|
|
17
|
+
type WorkflowGraphNode,
|
|
18
|
+
type WorkflowGraphNodeKind,
|
|
19
|
+
type WorkflowGraphNodeMetadata,
|
|
20
|
+
type WorkflowGraphNodeSchemas,
|
|
21
|
+
type WorkflowGraphSchemas
|
|
22
|
+
} from "../schemas.ts"
|
|
23
|
+
import type { WorkflowArtifact } from "./artifact.ts"
|
|
24
|
+
import { loadWorkflowArtifact } from "./loader.ts"
|
|
25
|
+
|
|
26
|
+
interface SchemaAst {
|
|
27
|
+
readonly _tag?: string
|
|
28
|
+
readonly propertySignatures?: ReadonlyArray<{
|
|
29
|
+
readonly name: PropertyKey
|
|
30
|
+
readonly type?: SchemaAst
|
|
31
|
+
}>
|
|
32
|
+
readonly elements?: ReadonlyArray<SchemaAst>
|
|
33
|
+
readonly rest?: ReadonlyArray<SchemaAst>
|
|
34
|
+
readonly types?: ReadonlyArray<SchemaAst>
|
|
35
|
+
readonly literal?: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type {
|
|
39
|
+
WorkflowArtifactGraph,
|
|
40
|
+
WorkflowGraph,
|
|
41
|
+
WorkflowGraphEdge,
|
|
42
|
+
WorkflowGraphNode,
|
|
43
|
+
WorkflowGraphNodeKind,
|
|
44
|
+
WorkflowGraphNodeMetadata,
|
|
45
|
+
WorkflowGraphNodeSchemas,
|
|
46
|
+
WorkflowGraphSchemas
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WorkflowGraphOptions<I = unknown> {
|
|
50
|
+
readonly input?: I
|
|
51
|
+
readonly maxNodes?: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const emptySecretResolver: SecretResolver = {
|
|
55
|
+
resolve: (name) => `__secret:${name}__`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const nodeId = (kind: WorkflowGraphNodeKind, name: string, invocation: number): string =>
|
|
59
|
+
`${kind}:${name}:${invocation}`
|
|
60
|
+
|
|
61
|
+
const callKey = (call: OrchestrationCall): string =>
|
|
62
|
+
`${call.kind}:${call.name}:${call.counter}`
|
|
63
|
+
|
|
64
|
+
const jsonSchemaFor = jsonSchemaOf
|
|
65
|
+
|
|
66
|
+
const objectWithOptionalSchemas = (schemas: WorkflowGraphNodeSchemas): WorkflowGraphNodeSchemas | undefined =>
|
|
67
|
+
schemas.input === undefined &&
|
|
68
|
+
schemas.output === undefined &&
|
|
69
|
+
schemas.errors === undefined &&
|
|
70
|
+
schemas.signal === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: schemas
|
|
73
|
+
|
|
74
|
+
const workflowSchemas = (workflow: DefinedWorkflow): WorkflowGraphSchemas | undefined => {
|
|
75
|
+
const input = jsonSchemaFor(workflow.input)
|
|
76
|
+
const output = jsonSchemaFor(workflow.output)
|
|
77
|
+
const errors = jsonSchemaFor(workflow.errors)
|
|
78
|
+
return objectWithOptionalSchemas({
|
|
79
|
+
...(input === undefined ? {} : { input }),
|
|
80
|
+
...(output === undefined ? {} : { output }),
|
|
81
|
+
...(errors === undefined ? {} : { errors })
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const stepSchemas = (step: Step<any, any, any>): WorkflowGraphNodeSchemas | undefined => {
|
|
86
|
+
const input = jsonSchemaFor(step.input)
|
|
87
|
+
const output = jsonSchemaFor(step.output)
|
|
88
|
+
const errors = jsonSchemaFor(step.errors)
|
|
89
|
+
return objectWithOptionalSchemas({
|
|
90
|
+
...(input === undefined ? {} : { input }),
|
|
91
|
+
...(output === undefined ? {} : { output }),
|
|
92
|
+
...(errors === undefined ? {} : { errors })
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const describeStep = (step: Step<any, any, any>): WorkflowGraphNodeMetadata => ({
|
|
97
|
+
...(step.retry === undefined ? {} : { retry: step.retry }),
|
|
98
|
+
...(step.concurrency === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: {
|
|
101
|
+
concurrency: {
|
|
102
|
+
limit: step.concurrency.limit,
|
|
103
|
+
keyed: step.concurrency.key !== undefined
|
|
104
|
+
}
|
|
105
|
+
}),
|
|
106
|
+
compensates: step.compensate !== undefined
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const schemaAst = (schema: unknown): SchemaAst | undefined =>
|
|
110
|
+
Schema.isSchema(schema) ? schema.ast as SchemaAst : undefined
|
|
111
|
+
|
|
112
|
+
export const sampleValueForSchema = (schema: unknown): unknown =>
|
|
113
|
+
sampleValueFromAst(schemaAst(schema), new Set())
|
|
114
|
+
|
|
115
|
+
/** Sample value for a JSON Schema document (e.g. PendingSignal.payloadSchema),
|
|
116
|
+
* mirroring the placeholders of sampleValueForSchema. */
|
|
117
|
+
export const sampleValueForJsonSchema = (schema: JsonSchema, depth = 0): unknown => {
|
|
118
|
+
if (depth > 8) {
|
|
119
|
+
return {}
|
|
120
|
+
}
|
|
121
|
+
if (schema.const !== undefined) {
|
|
122
|
+
return schema.const
|
|
123
|
+
}
|
|
124
|
+
if (schema.enum !== undefined && schema.enum.length > 0) {
|
|
125
|
+
return schema.enum[0]
|
|
126
|
+
}
|
|
127
|
+
const alternative = schema.anyOf?.[0] ?? schema.oneOf?.[0]
|
|
128
|
+
if (alternative !== undefined) {
|
|
129
|
+
return sampleValueForJsonSchema(alternative, depth + 1)
|
|
130
|
+
}
|
|
131
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type
|
|
132
|
+
switch (type) {
|
|
133
|
+
case "string":
|
|
134
|
+
return "sample"
|
|
135
|
+
case "number":
|
|
136
|
+
case "integer":
|
|
137
|
+
return 1
|
|
138
|
+
case "boolean":
|
|
139
|
+
return true
|
|
140
|
+
case "null":
|
|
141
|
+
return null
|
|
142
|
+
case "array":
|
|
143
|
+
return schema.items === undefined ? [] : [sampleValueForJsonSchema(schema.items, depth + 1)]
|
|
144
|
+
case "object": {
|
|
145
|
+
const properties = schema.properties ?? {}
|
|
146
|
+
const required = schema.required
|
|
147
|
+
const entries = Object.entries(properties)
|
|
148
|
+
.filter(([key]) => required === undefined || required.includes(key))
|
|
149
|
+
.map(([key, property]) => [key, sampleValueForJsonSchema(property, depth + 1)])
|
|
150
|
+
return Object.fromEntries(entries)
|
|
151
|
+
}
|
|
152
|
+
default:
|
|
153
|
+
return {}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// `seen` holds the current recursion path only: schema AST nodes are shared
|
|
158
|
+
// (t.string is a singleton), so a persistent visited-set would misread the
|
|
159
|
+
// second reference to a node as a cycle.
|
|
160
|
+
const sampleValueFromAst = (ast: SchemaAst | undefined, seen: Set<SchemaAst>): unknown => {
|
|
161
|
+
if (ast === undefined || seen.has(ast)) {
|
|
162
|
+
return {}
|
|
163
|
+
}
|
|
164
|
+
seen.add(ast)
|
|
165
|
+
try {
|
|
166
|
+
return sampleValueFromAstUnguarded(ast, seen)
|
|
167
|
+
} finally {
|
|
168
|
+
seen.delete(ast)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const sampleValueFromAstUnguarded = (ast: SchemaAst, seen: Set<SchemaAst>): unknown => {
|
|
173
|
+
switch (ast._tag) {
|
|
174
|
+
case "String":
|
|
175
|
+
return "sample"
|
|
176
|
+
case "Number":
|
|
177
|
+
return 1
|
|
178
|
+
case "Boolean":
|
|
179
|
+
return true
|
|
180
|
+
case "Void":
|
|
181
|
+
case "Undefined":
|
|
182
|
+
return undefined
|
|
183
|
+
case "Null":
|
|
184
|
+
return null
|
|
185
|
+
case "Date":
|
|
186
|
+
return new Date(0)
|
|
187
|
+
case "Literal":
|
|
188
|
+
return ast.literal
|
|
189
|
+
case "Arrays": {
|
|
190
|
+
const element = ast.rest?.[0] ?? ast.elements?.[0]
|
|
191
|
+
return [sampleValueFromAst(element, seen)]
|
|
192
|
+
}
|
|
193
|
+
case "Union": {
|
|
194
|
+
const candidate = ast.types?.find((item) => item._tag !== "Undefined") ?? ast.types?.[0]
|
|
195
|
+
return sampleValueFromAst(candidate, seen)
|
|
196
|
+
}
|
|
197
|
+
case "Objects": {
|
|
198
|
+
const entries = ast.propertySignatures?.map((property) => [
|
|
199
|
+
property.name,
|
|
200
|
+
sampleValueFromAst(property.type, seen)
|
|
201
|
+
]) ?? []
|
|
202
|
+
return Object.fromEntries(entries)
|
|
203
|
+
}
|
|
204
|
+
case "Unknown":
|
|
205
|
+
case "Any":
|
|
206
|
+
return {}
|
|
207
|
+
case "Never":
|
|
208
|
+
throw new Error("Cannot create a sample value for Schema.Never")
|
|
209
|
+
default:
|
|
210
|
+
return {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const metadataFromEvents = (events: ReadonlyArray<WorkflowEvent>) => {
|
|
215
|
+
const metadata = new Map<string, WorkflowGraphNodeMetadata>()
|
|
216
|
+
|
|
217
|
+
for (const event of events) {
|
|
218
|
+
switch (event.type) {
|
|
219
|
+
case "step.started":
|
|
220
|
+
metadata.set(nodeId("step", event.stepName, event.invocation), {
|
|
221
|
+
input: event.input,
|
|
222
|
+
activityName: event.activityName
|
|
223
|
+
})
|
|
224
|
+
break
|
|
225
|
+
case "sleep.started":
|
|
226
|
+
metadata.set(nodeId("sleep", event.name, event.invocation), {
|
|
227
|
+
duration: event.duration,
|
|
228
|
+
activityName: event.activityName
|
|
229
|
+
})
|
|
230
|
+
break
|
|
231
|
+
case "signal.waiting":
|
|
232
|
+
metadata.set(nodeId("signal", event.name, event.invocation), {
|
|
233
|
+
timeout: event.timeout,
|
|
234
|
+
activityName: event.activityName
|
|
235
|
+
})
|
|
236
|
+
break
|
|
237
|
+
case "code.started":
|
|
238
|
+
metadata.set(nodeId("code", event.name, event.invocation), {
|
|
239
|
+
activityName: event.activityName,
|
|
240
|
+
...(event.reason === undefined ? {} : { reason: event.reason })
|
|
241
|
+
})
|
|
242
|
+
break
|
|
243
|
+
case "all.started":
|
|
244
|
+
metadata.set(nodeId("all", event.name, event.invocation), {
|
|
245
|
+
activityName: event.activityName,
|
|
246
|
+
branches: event.branches
|
|
247
|
+
})
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return metadata
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const graphNodeForCall = (
|
|
256
|
+
call: OrchestrationCall,
|
|
257
|
+
options: {
|
|
258
|
+
readonly eventMetadata: ReadonlyMap<string, WorkflowGraphNodeMetadata>
|
|
259
|
+
readonly steps: ReadonlyMap<string, WorkflowGraphNodeMetadata>
|
|
260
|
+
readonly schemas: ReadonlyMap<string, WorkflowGraphNodeSchemas>
|
|
261
|
+
readonly nameCounts: ReadonlyMap<string, number>
|
|
262
|
+
}
|
|
263
|
+
): WorkflowGraphNode => {
|
|
264
|
+
const id = nodeId(call.kind, call.name, call.counter)
|
|
265
|
+
const schemas = options.schemas.get(id)
|
|
266
|
+
const metadata: WorkflowGraphNodeMetadata = {
|
|
267
|
+
...options.eventMetadata.get(id),
|
|
268
|
+
...options.steps.get(id),
|
|
269
|
+
...(call.branches === undefined ? {} : { branches: call.branches })
|
|
270
|
+
}
|
|
271
|
+
const description = typeof metadata.reason === "string"
|
|
272
|
+
? metadata.reason
|
|
273
|
+
: call.counter > 1
|
|
274
|
+
? `Invocation ${call.counter}`
|
|
275
|
+
: undefined
|
|
276
|
+
return {
|
|
277
|
+
id,
|
|
278
|
+
kind: call.kind,
|
|
279
|
+
label: call.name,
|
|
280
|
+
invocation: call.counter,
|
|
281
|
+
repeated: (options.nameCounts.get(`${call.kind}:${call.name}`) ?? 0) > 1,
|
|
282
|
+
...(description === undefined ? {} : { description }),
|
|
283
|
+
...(schemas === undefined ? {} : { schemas }),
|
|
284
|
+
metadata
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const graphFromTrace = (options: {
|
|
289
|
+
readonly workflow: DefinedWorkflow
|
|
290
|
+
readonly determinism: InMemoryDeterminismState
|
|
291
|
+
readonly events: ReadonlyArray<WorkflowEvent>
|
|
292
|
+
readonly steps: ReadonlyMap<string, WorkflowGraphNodeMetadata>
|
|
293
|
+
readonly schemas: ReadonlyMap<string, WorkflowGraphNodeSchemas>
|
|
294
|
+
readonly diagnostics: ReadonlyArray<string>
|
|
295
|
+
readonly maxNodes: number
|
|
296
|
+
}): WorkflowGraph => {
|
|
297
|
+
const eventMetadata = metadataFromEvents(options.events)
|
|
298
|
+
const nameCounts = new Map<string, number>()
|
|
299
|
+
for (const call of options.determinism.calls) {
|
|
300
|
+
nameCounts.set(`${call.kind}:${call.name}`, (nameCounts.get(`${call.kind}:${call.name}`) ?? 0) + 1)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const calls = options.determinism.calls.slice(0, options.maxNodes)
|
|
304
|
+
const callKeys = new Set(calls.map(callKey))
|
|
305
|
+
const truncated = calls.length < options.determinism.calls.length
|
|
306
|
+
const blocks = options.determinism.blocks
|
|
307
|
+
.filter((block) => callKeys.has(callKey(block.call)))
|
|
308
|
+
.map((block) => ({
|
|
309
|
+
call: block.call,
|
|
310
|
+
branches: block.branches.map((branch) => branch.filter((call) => callKeys.has(callKey(call))))
|
|
311
|
+
}))
|
|
312
|
+
const blockByCall = new Map(blocks.map((block) => [callKey(block.call), block]))
|
|
313
|
+
const branchCallKeys = new Set<string>()
|
|
314
|
+
for (const block of blocks) {
|
|
315
|
+
for (const branch of block.branches) {
|
|
316
|
+
for (const call of branch) {
|
|
317
|
+
branchCallKeys.add(callKey(call))
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const mainCalls = calls.filter((call) => !branchCallKeys.has(callKey(call)))
|
|
322
|
+
const start: WorkflowGraphNode = {
|
|
323
|
+
id: "start",
|
|
324
|
+
kind: "start",
|
|
325
|
+
label: "Start",
|
|
326
|
+
repeated: false,
|
|
327
|
+
metadata: {}
|
|
328
|
+
}
|
|
329
|
+
const end: WorkflowGraphNode = {
|
|
330
|
+
id: "end",
|
|
331
|
+
kind: "end",
|
|
332
|
+
label: truncated ? "Trace truncated" : "End",
|
|
333
|
+
repeated: false,
|
|
334
|
+
metadata: {}
|
|
335
|
+
}
|
|
336
|
+
const nodes: WorkflowGraphNode[] = [start]
|
|
337
|
+
const edges: WorkflowGraphEdge[] = []
|
|
338
|
+
let previousTails: WorkflowGraphNode[] = [start]
|
|
339
|
+
const addEdge = (source: WorkflowGraphNode, target: WorkflowGraphNode, label?: string) => {
|
|
340
|
+
edges.push({
|
|
341
|
+
id: `${source.id}->${target.id}${label === undefined ? "" : `:${label}`}`,
|
|
342
|
+
source: source.id,
|
|
343
|
+
target: target.id,
|
|
344
|
+
...(label === undefined ? (target.repeated ? { label: "repeat" } : {}) : { label })
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
const appendAfterPrevious = (node: WorkflowGraphNode, label?: string) => {
|
|
348
|
+
for (const tail of previousTails) {
|
|
349
|
+
addEdge(tail, node, label)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const call of mainCalls) {
|
|
354
|
+
const node = graphNodeForCall(call, {
|
|
355
|
+
eventMetadata,
|
|
356
|
+
steps: options.steps,
|
|
357
|
+
schemas: options.schemas,
|
|
358
|
+
nameCounts
|
|
359
|
+
})
|
|
360
|
+
nodes.push(node)
|
|
361
|
+
appendAfterPrevious(node)
|
|
362
|
+
|
|
363
|
+
const block = blockByCall.get(callKey(call))
|
|
364
|
+
if (block === undefined) {
|
|
365
|
+
previousTails = [node]
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const branchTails: WorkflowGraphNode[] = []
|
|
370
|
+
block.branches.forEach((branch, branchIndex) => {
|
|
371
|
+
let branchPrevious = node
|
|
372
|
+
if (branch.length === 0) {
|
|
373
|
+
branchTails.push(node)
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
branch.forEach((branchCall, callIndex) => {
|
|
377
|
+
const branchNode = graphNodeForCall(branchCall, {
|
|
378
|
+
eventMetadata,
|
|
379
|
+
steps: options.steps,
|
|
380
|
+
schemas: options.schemas,
|
|
381
|
+
nameCounts
|
|
382
|
+
})
|
|
383
|
+
nodes.push(branchNode)
|
|
384
|
+
addEdge(branchPrevious, branchNode, callIndex === 0 ? `branch ${branchIndex + 1}` : undefined)
|
|
385
|
+
branchPrevious = branchNode
|
|
386
|
+
})
|
|
387
|
+
branchTails.push(branchPrevious)
|
|
388
|
+
})
|
|
389
|
+
previousTails = branchTails.length === 0 ? [node] : branchTails
|
|
390
|
+
}
|
|
391
|
+
nodes.push(end)
|
|
392
|
+
for (const tail of previousTails) {
|
|
393
|
+
addEdge(tail, end)
|
|
394
|
+
}
|
|
395
|
+
const schemas = workflowSchemas(options.workflow)
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
workflowName: options.workflow.name,
|
|
399
|
+
version: options.workflow.version,
|
|
400
|
+
engineName: options.workflow.engineName,
|
|
401
|
+
sourceHash: options.workflow.sourceHash,
|
|
402
|
+
...(schemas === undefined ? {} : { schemas }),
|
|
403
|
+
nodes,
|
|
404
|
+
edges,
|
|
405
|
+
calls,
|
|
406
|
+
diagnostics: truncated
|
|
407
|
+
? [...options.diagnostics, `Trace stopped after ${options.maxNodes} nodes.`]
|
|
408
|
+
: options.diagnostics
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export const workflowToGraph = async <I, O, E>(
|
|
413
|
+
workflow: DefinedWorkflow<I, O, E>,
|
|
414
|
+
options: WorkflowGraphOptions<I> = {}
|
|
415
|
+
): Promise<WorkflowGraph> => {
|
|
416
|
+
const determinism = createInMemoryDeterminismState()
|
|
417
|
+
const events: WorkflowEvent[] = []
|
|
418
|
+
const steps = new Map<string, WorkflowGraphNodeMetadata>()
|
|
419
|
+
const schemas = new Map<string, WorkflowGraphNodeSchemas>()
|
|
420
|
+
const signalCounts = new Map<string, number>()
|
|
421
|
+
const diagnostics: string[] = []
|
|
422
|
+
const input = options.input ?? sampleValueForSchema(workflow.input) as I
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
await workflow.executeInMemory(input, {
|
|
426
|
+
executionId: `graph-${workflow.name}-${workflow.version}`,
|
|
427
|
+
determinism,
|
|
428
|
+
onEvent: (event) => {
|
|
429
|
+
events.push(event)
|
|
430
|
+
},
|
|
431
|
+
stepExecutor: ({ step, invocation }) => {
|
|
432
|
+
const id = nodeId("step", step.name, invocation)
|
|
433
|
+
steps.set(id, describeStep(step))
|
|
434
|
+
const stepNodeSchemas = stepSchemas(step)
|
|
435
|
+
if (stepNodeSchemas !== undefined) {
|
|
436
|
+
schemas.set(id, stepNodeSchemas)
|
|
437
|
+
}
|
|
438
|
+
return sampleValueForSchema(step.output)
|
|
439
|
+
},
|
|
440
|
+
sleep: async () => undefined,
|
|
441
|
+
signalTimeout: async () => undefined,
|
|
442
|
+
signalValue: ({ name, schema }) => {
|
|
443
|
+
const invocation = (signalCounts.get(name) ?? 0) + 1
|
|
444
|
+
signalCounts.set(name, invocation)
|
|
445
|
+
const signalSchema = jsonSchemaFor(schema)
|
|
446
|
+
if (signalSchema !== undefined) {
|
|
447
|
+
schemas.set(nodeId("signal", name, invocation), { signal: signalSchema })
|
|
448
|
+
}
|
|
449
|
+
return sampleValueForSchema(schema)
|
|
450
|
+
},
|
|
451
|
+
secrets: emptySecretResolver
|
|
452
|
+
})
|
|
453
|
+
} catch (error) {
|
|
454
|
+
diagnostics.push(error instanceof Error ? error.message : String(error))
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return graphFromTrace({
|
|
458
|
+
workflow,
|
|
459
|
+
determinism,
|
|
460
|
+
events,
|
|
461
|
+
steps,
|
|
462
|
+
schemas,
|
|
463
|
+
diagnostics,
|
|
464
|
+
maxNodes: options.maxNodes ?? 100
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export const workflowArtifactToGraph = async (
|
|
469
|
+
artifact: WorkflowArtifact,
|
|
470
|
+
options: WorkflowGraphOptions = {}
|
|
471
|
+
): Promise<WorkflowArtifactGraph> => {
|
|
472
|
+
try {
|
|
473
|
+
const loaded = await loadWorkflowArtifact(artifact)
|
|
474
|
+
return {
|
|
475
|
+
artifact,
|
|
476
|
+
exportName: loaded.exportName,
|
|
477
|
+
graph: await workflowToGraph(loaded.workflow, options),
|
|
478
|
+
diagnostics: []
|
|
479
|
+
}
|
|
480
|
+
} catch (error) {
|
|
481
|
+
return {
|
|
482
|
+
artifact,
|
|
483
|
+
diagnostics: [error instanceof Error ? error.message : String(error)]
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
package/src/sdk/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createFileWorkflowStore,
|
|
3
|
+
createMemoryWorkflowStore
|
|
4
|
+
} from "./artifact.ts"
|
|
5
|
+
export type {
|
|
6
|
+
FileWorkflowStoreOptions,
|
|
7
|
+
WorkflowArtifact,
|
|
8
|
+
WorkflowRepository,
|
|
9
|
+
WorkflowRunEventRecord,
|
|
10
|
+
WorkflowRunRecord,
|
|
11
|
+
WorkflowRunStatus,
|
|
12
|
+
WorkflowRunStore,
|
|
13
|
+
WorkflowStore
|
|
14
|
+
} from "./artifact.ts"
|
|
15
|
+
export {
|
|
16
|
+
createSqliteWorkflowRepository,
|
|
17
|
+
seedSqliteWorkflowRepository
|
|
18
|
+
} from "./sqlite.ts"
|
|
19
|
+
export type { SqliteWorkflowRepositoryOptions } from "./sqlite.ts"
|
|
20
|
+
export {
|
|
21
|
+
isDefinedWorkflow,
|
|
22
|
+
loadWorkflowArtifact
|
|
23
|
+
} from "./loader.ts"
|
|
24
|
+
export type { LoadedWorkflow } from "./loader.ts"
|
|
25
|
+
export {
|
|
26
|
+
sampleValueForJsonSchema,
|
|
27
|
+
sampleValueForSchema,
|
|
28
|
+
workflowArtifactToGraph,
|
|
29
|
+
workflowToGraph
|
|
30
|
+
} from "./graph.ts"
|
|
31
|
+
export type {
|
|
32
|
+
WorkflowArtifactGraph,
|
|
33
|
+
WorkflowGraph,
|
|
34
|
+
WorkflowGraphEdge,
|
|
35
|
+
WorkflowGraphNode,
|
|
36
|
+
WorkflowGraphNodeKind,
|
|
37
|
+
WorkflowGraphNodeMetadata,
|
|
38
|
+
WorkflowGraphNodeSchemas,
|
|
39
|
+
WorkflowGraphSchemas,
|
|
40
|
+
WorkflowGraphOptions
|
|
41
|
+
} from "./graph.ts"
|
|
42
|
+
export { parseJsonText, toJsonText } from "./json.ts"
|
|
43
|
+
export { discover } from "./integrations.ts"
|
|
44
|
+
export type {
|
|
45
|
+
DiscoverIntegrationsOptions,
|
|
46
|
+
DiscoverIntegrationsResult,
|
|
47
|
+
IntegrationKind,
|
|
48
|
+
IntegrationSearchResult
|
|
49
|
+
} from "./integrations.ts"
|
|
50
|
+
export { Cancelled, MissingWorkflowVersionError, createWorkflowClient, createWorkflowSdk } from "./sdk.ts"
|
|
51
|
+
export type {
|
|
52
|
+
WorkflowClient,
|
|
53
|
+
WorkflowExecutionHandle,
|
|
54
|
+
WorkflowExecutionStatus,
|
|
55
|
+
WorkflowHistoryEvent,
|
|
56
|
+
WorkflowHistoryRecord,
|
|
57
|
+
WorkflowListResult,
|
|
58
|
+
PendingSignal,
|
|
59
|
+
WorkflowResult,
|
|
60
|
+
RunWorkflowOptions,
|
|
61
|
+
WorkflowRunResult,
|
|
62
|
+
WorkflowSdk,
|
|
63
|
+
WorkflowSdkOptions
|
|
64
|
+
} from "./sdk.ts"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type IntegrationKind = "mcp" | "openapi" | "graphql" | "cli"
|
|
2
|
+
|
|
3
|
+
export interface DiscoverIntegrationsOptions {
|
|
4
|
+
readonly kind?: IntegrationKind
|
|
5
|
+
readonly limit?: number
|
|
6
|
+
readonly baseUrl?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface IntegrationSearchResult {
|
|
10
|
+
readonly domain: string
|
|
11
|
+
readonly name: string
|
|
12
|
+
readonly description: string
|
|
13
|
+
readonly kinds: ReadonlyArray<IntegrationKind>
|
|
14
|
+
readonly url: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DiscoverIntegrationsResult {
|
|
18
|
+
readonly results: ReadonlyArray<IntegrationSearchResult>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const discover = async (
|
|
22
|
+
searchTerm: string,
|
|
23
|
+
options: DiscoverIntegrationsOptions = {}
|
|
24
|
+
): Promise<DiscoverIntegrationsResult> => {
|
|
25
|
+
const query = searchTerm.trim()
|
|
26
|
+
|
|
27
|
+
if (query.length === 0) {
|
|
28
|
+
throw new Error("discover requires a non-empty search term")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const url = new URL("/api/search", options.baseUrl ?? "https://integrations.sh")
|
|
32
|
+
url.searchParams.set("q", query)
|
|
33
|
+
|
|
34
|
+
if (options.kind !== undefined) {
|
|
35
|
+
url.searchParams.set("kind", options.kind)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options.limit !== undefined) {
|
|
39
|
+
url.searchParams.set("limit", String(options.limit))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const response = await fetch(url)
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error(`integrations.sh discover failed: ${response.status} ${response.statusText}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return await response.json() as DiscoverIntegrationsResult
|
|
49
|
+
}
|
package/src/sdk/json.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const stableStringify = (value: unknown): string =>
|
|
2
|
+
JSON.stringify(normalizeJson(value))
|
|
3
|
+
|
|
4
|
+
const normalizeJson = (value: unknown): unknown => {
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
return value.map(normalizeJson)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof value !== "object" || value === null) {
|
|
10
|
+
return value
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const input = value as Record<string, unknown>
|
|
14
|
+
const output: Record<string, unknown> = {}
|
|
15
|
+
|
|
16
|
+
for (const key of Object.keys(input).sort()) {
|
|
17
|
+
output[key] = normalizeJson(input[key])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return output
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Error has no enumerable own properties, so plain JSON.stringify turns a
|
|
24
|
+
// thrown Error into "{}" and history loses the failure message. Keep name,
|
|
25
|
+
// message, and any own enumerable fields (e.g. _tag on tagged errors).
|
|
26
|
+
const jsonReplacer = (_key: string, value: unknown): unknown =>
|
|
27
|
+
value instanceof Error
|
|
28
|
+
? { ...value, name: value.name, message: value.message }
|
|
29
|
+
: value
|
|
30
|
+
|
|
31
|
+
export const toJsonText = (value: unknown): string => {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(value, jsonReplacer) ?? JSON.stringify(String(value))
|
|
34
|
+
} catch {
|
|
35
|
+
return JSON.stringify(String(value))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const parseJsonText = (value: string | null): unknown =>
|
|
40
|
+
value === null ? undefined : JSON.parse(value)
|