@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/authoring.d.ts +7 -0
  4. package/dist/authoring.d.ts.map +1 -0
  5. package/dist/authoring.js +8 -0
  6. package/dist/authoring.js.map +1 -0
  7. package/dist/cli/integrations.d.ts +3 -0
  8. package/dist/cli/integrations.d.ts.map +1 -0
  9. package/dist/cli/integrations.js +40 -0
  10. package/dist/cli/integrations.js.map +1 -0
  11. package/dist/cli/main.d.ts +3 -0
  12. package/dist/cli/main.d.ts.map +1 -0
  13. package/dist/cli/main.js +591 -0
  14. package/dist/cli/main.js.map +1 -0
  15. package/dist/core.d.ts +198 -0
  16. package/dist/core.d.ts.map +1 -0
  17. package/dist/core.js +1061 -0
  18. package/dist/core.js.map +1 -0
  19. package/dist/errors.d.ts +3 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +13 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/events.d.ts +154 -0
  24. package/dist/events.d.ts.map +1 -0
  25. package/dist/events.js +24 -0
  26. package/dist/events.js.map +1 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +11 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/replay.d.ts +3 -0
  32. package/dist/replay.d.ts.map +1 -0
  33. package/dist/replay.js +28 -0
  34. package/dist/replay.js.map +1 -0
  35. package/dist/runtime.d.ts +70 -0
  36. package/dist/runtime.d.ts.map +1 -0
  37. package/dist/runtime.js +183 -0
  38. package/dist/runtime.js.map +1 -0
  39. package/dist/schema.d.ts +18 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +19 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/schemas.d.ts +1198 -0
  44. package/dist/schemas.d.ts.map +1 -0
  45. package/dist/schemas.js +383 -0
  46. package/dist/schemas.js.map +1 -0
  47. package/dist/sdk/artifact.d.ts +41 -0
  48. package/dist/sdk/artifact.d.ts.map +1 -0
  49. package/dist/sdk/artifact.js +94 -0
  50. package/dist/sdk/artifact.js.map +1 -0
  51. package/dist/sdk/graph.d.ts +15 -0
  52. package/dist/sdk/graph.d.ts.map +1 -0
  53. package/dist/sdk/graph.js +388 -0
  54. package/dist/sdk/graph.js.map +1 -0
  55. package/dist/sdk/index.d.ts +14 -0
  56. package/dist/sdk/index.d.ts.map +1 -0
  57. package/dist/sdk/index.js +8 -0
  58. package/dist/sdk/index.js.map +1 -0
  59. package/dist/sdk/integrations.d.ts +18 -0
  60. package/dist/sdk/integrations.d.ts.map +1 -0
  61. package/dist/sdk/integrations.js +20 -0
  62. package/dist/sdk/integrations.js.map +1 -0
  63. package/dist/sdk/json.d.ts +4 -0
  64. package/dist/sdk/json.d.ts.map +1 -0
  65. package/dist/sdk/json.js +31 -0
  66. package/dist/sdk/json.js.map +1 -0
  67. package/dist/sdk/loader.d.ts +10 -0
  68. package/dist/sdk/loader.d.ts.map +1 -0
  69. package/dist/sdk/loader.js +75 -0
  70. package/dist/sdk/loader.js.map +1 -0
  71. package/dist/sdk/sdk.d.ts +102 -0
  72. package/dist/sdk/sdk.d.ts.map +1 -0
  73. package/dist/sdk/sdk.js +669 -0
  74. package/dist/sdk/sdk.js.map +1 -0
  75. package/dist/sdk/sqlite.d.ts +8 -0
  76. package/dist/sdk/sqlite.d.ts.map +1 -0
  77. package/dist/sdk/sqlite.js +288 -0
  78. package/dist/sdk/sqlite.js.map +1 -0
  79. package/dist/signal.d.ts +17 -0
  80. package/dist/signal.d.ts.map +1 -0
  81. package/dist/signal.js +78 -0
  82. package/dist/signal.js.map +1 -0
  83. package/dist/testing/index.d.ts +35 -0
  84. package/dist/testing/index.d.ts.map +1 -0
  85. package/dist/testing/index.js +244 -0
  86. package/dist/testing/index.js.map +1 -0
  87. package/package.json +67 -0
  88. package/src/authoring.ts +32 -0
  89. package/src/cli/integrations.ts +75 -0
  90. package/src/cli/main.ts +793 -0
  91. package/src/core.ts +1515 -0
  92. package/src/errors.ts +19 -0
  93. package/src/events.ts +37 -0
  94. package/src/index.ts +109 -0
  95. package/src/replay.ts +29 -0
  96. package/src/runtime.ts +314 -0
  97. package/src/schema.ts +34 -0
  98. package/src/schemas.ts +473 -0
  99. package/src/sdk/artifact.ts +176 -0
  100. package/src/sdk/graph.ts +486 -0
  101. package/src/sdk/index.ts +64 -0
  102. package/src/sdk/integrations.ts +49 -0
  103. package/src/sdk/json.ts +40 -0
  104. package/src/sdk/loader.ts +99 -0
  105. package/src/sdk/sdk.ts +911 -0
  106. package/src/sdk/sqlite.ts +405 -0
  107. package/src/signal.ts +116 -0
  108. package/src/testing/index.ts +341 -0
package/src/errors.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect"
2
+
3
+ // A typed, tagged workflow error. The returned value is both:
4
+ // - a class you `throw new MyError({...})` inside a step's `run`
5
+ // - a Schema the engine uses to (de)serialize the error across replays
6
+ //
7
+ // Because errors cross the durable boundary they MUST be declared this way
8
+ // (not plain `Error`), so the engine can persist and rehydrate them.
9
+ export const defineError = <
10
+ Tag extends string,
11
+ Fields extends Schema.Struct.Fields
12
+ >(
13
+ tag: Tag,
14
+ fields: Fields
15
+ ) => {
16
+ // The self-referential generic of `Schema.TaggedErrorClass` is awkward to thread
17
+ // through a generic factory; the cast is purely to satisfy that phantom type.
18
+ return Schema.TaggedErrorClass<any>()(tag, fields) as any
19
+ }
package/src/events.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { Context, Effect } from "effect"
2
+ import { WorkflowEvent as WorkflowEventSchema, isWorkflowEvent } from "./schemas.ts"
3
+
4
+ export { isWorkflowEvent }
5
+ export const WorkflowEvent = WorkflowEventSchema
6
+ export type WorkflowEvent = typeof WorkflowEventSchema.Type
7
+
8
+ export type WorkflowEventSink = (event: WorkflowEvent) => void | Promise<void>
9
+
10
+ export const currentWorkflowEventSink = Context.Reference<WorkflowEventSink | undefined>(
11
+ "wf/currentWorkflowEventSink",
12
+ { defaultValue: () => undefined }
13
+ )
14
+
15
+ // Durable-engine executions run on entity fibers created when the engine
16
+ // layer is built, so they do not inherit the caller's context reference. Events that
17
+ // carry an executionId are routed through this registry instead.
18
+ const executionEventSinks = new Map<string, WorkflowEventSink>()
19
+
20
+ export const setExecutionEventSink = (executionId: string, sink: WorkflowEventSink): void => {
21
+ executionEventSinks.set(executionId, sink)
22
+ }
23
+
24
+ export const removeExecutionEventSink = (executionId: string): void => {
25
+ executionEventSinks.delete(executionId)
26
+ }
27
+
28
+ export const emitWorkflowEvent = (event: WorkflowEvent): Effect.Effect<void> =>
29
+ Effect.flatMap(currentWorkflowEventSink, (fiberSink) => {
30
+ const executionId = (event as { readonly executionId?: string }).executionId
31
+ const sink = (executionId !== undefined ? executionEventSinks.get(executionId) : undefined) ?? fiberSink
32
+ if (sink === undefined) {
33
+ return Effect.void
34
+ }
35
+
36
+ return Effect.promise(() => Promise.resolve(sink(event)))
37
+ })
package/src/index.ts ADDED
@@ -0,0 +1,109 @@
1
+ // The workflow authoring surface. An authored workflow imports ONLY from here
2
+ // (plus pure helper functions it defines) — never `effect` or `@effect/*`.
3
+ export { createInMemoryDeterminismState, defineStep, defineWorkflow, envSecretResolver, isSecretRef, NonDeterminismError, secret } from "./core.ts"
4
+ export type {
5
+ DefinedWorkflow,
6
+ InMemoryDeterminismState,
7
+ OrchestrationCall,
8
+ OrchestrationKind,
9
+ SecretRef,
10
+ SecretResolver,
11
+ SignalOutcome,
12
+ Step,
13
+ StepConcurrency,
14
+ StepContext,
15
+ StepRetryPolicy,
16
+ TerminalFailure,
17
+ WorkflowContext,
18
+ WorkflowValue
19
+ } from "./core.ts"
20
+ export { defineError } from "./errors.ts"
21
+ export type { WorkflowEvent, WorkflowEventSink } from "./events.ts"
22
+ export {
23
+ ExecutionId,
24
+ JsonSchema,
25
+ RunEventsResponse,
26
+ RunsResponse,
27
+ WorkflowArtifact as WorkflowArtifactSchema,
28
+ WorkflowArtifactGraph as WorkflowArtifactGraphSchema,
29
+ WorkflowEvent as WorkflowEventSchema,
30
+ WorkflowGraph as WorkflowGraphSchema,
31
+ WorkflowGraphEdge as WorkflowGraphEdgeSchema,
32
+ WorkflowGraphNode as WorkflowGraphNodeSchema,
33
+ WorkflowGraphNodeMetadata as WorkflowGraphNodeMetadataSchema,
34
+ WorkflowGraphNodeSchemas as WorkflowGraphNodeSchemasSchema,
35
+ WorkflowGraphSchemas as WorkflowGraphSchemasSchema,
36
+ WorkflowHistoryEvent as WorkflowHistoryEventSchema,
37
+ WorkflowManifest,
38
+ WorkflowManifestEntry,
39
+ WorkflowRunEventRecord as WorkflowRunEventRecordSchema,
40
+ WorkflowRunRecord as WorkflowRunRecordSchema,
41
+ WorkflowRunStatus as WorkflowRunStatusSchema,
42
+ WorkflowsResponse,
43
+ decodeRunEventsResponse,
44
+ decodeRunsResponse,
45
+ decodeWorkflowsResponse,
46
+ decodeJsonSchema,
47
+ isWorkflowEvent
48
+ } from "./schemas.ts"
49
+ export type { JsonSchema as JsonSchemaDocument, ExecutionId as ExecutionIdValue } from "./schemas.ts"
50
+ export { deliverSignal, SignalDeliveryError } from "./signal.ts"
51
+ export { t } from "./schema.ts"
52
+ export { createWorkflowRuntime, engineLayer, executeWorkflow, makeEngineLayer, makeWorkflowEffect, run, WorkflowVersionConflictError } from "./runtime.ts"
53
+ export type { ExecuteWorkflowOptions, WorkflowRuntime, WorkflowRuntimeOptions } from "./runtime.ts"
54
+ export type {
55
+ FileWorkflowStoreOptions,
56
+ LoadedWorkflow,
57
+ RunWorkflowOptions,
58
+ WorkflowArtifact,
59
+ WorkflowRunResult,
60
+ WorkflowSdk,
61
+ WorkflowSdkOptions,
62
+ WorkflowStore,
63
+ WorkflowRepository,
64
+ WorkflowClient,
65
+ WorkflowExecutionHandle,
66
+ WorkflowExecutionStatus,
67
+ WorkflowHistoryEvent,
68
+ WorkflowHistoryRecord,
69
+ WorkflowListResult,
70
+ PendingSignal,
71
+ WorkflowRunEventRecord,
72
+ WorkflowRunRecord,
73
+ WorkflowRunStatus,
74
+ WorkflowRunStore,
75
+ SqliteWorkflowRepositoryOptions,
76
+ WorkflowArtifactGraph,
77
+ WorkflowGraph,
78
+ WorkflowGraphEdge,
79
+ WorkflowGraphNode,
80
+ WorkflowGraphNodeSchemas,
81
+ WorkflowGraphNodeMetadata,
82
+ WorkflowGraphSchemas,
83
+ WorkflowGraphNodeKind,
84
+ WorkflowGraphOptions,
85
+ DiscoverIntegrationsOptions,
86
+ DiscoverIntegrationsResult,
87
+ IntegrationKind,
88
+ IntegrationSearchResult
89
+ } from "./sdk/index.ts"
90
+ export {
91
+ Cancelled,
92
+ MissingWorkflowVersionError,
93
+ createFileWorkflowStore,
94
+ createMemoryWorkflowStore,
95
+ createWorkflowClient,
96
+ createSqliteWorkflowRepository,
97
+ seedSqliteWorkflowRepository,
98
+ loadWorkflowArtifact,
99
+ createWorkflowSdk,
100
+ parseJsonText,
101
+ toJsonText,
102
+ sampleValueForJsonSchema,
103
+ sampleValueForSchema,
104
+ workflowArtifactToGraph,
105
+ workflowToGraph,
106
+ discover
107
+ } from "./sdk/index.ts"
108
+ export { createTestRuntime } from "./testing/index.ts"
109
+ export type { CompensationRecorder, TestRuntime, TestRuntimeOptions } from "./testing/index.ts"
package/src/replay.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { WorkflowHistoryEvent } from "./schemas.ts"
2
+
3
+ // Events emitted from workflow-position code (as opposed to inside an
4
+ // activity) fire again every time the durable engine replays an execution —
5
+ // e.g. when a suspended run resumes after a signal, possibly in a fresh
6
+ // process. History sinks use this identity to record each such event once.
7
+ // Step, code, and compensation events are emitted inside activities or
8
+ // finalizers, never replayed, and legitimately repeat across retry attempts,
9
+ // so they have no dedupe identity.
10
+ export const replayDedupeKey = (event: WorkflowHistoryEvent): string | undefined => {
11
+ switch (event.type) {
12
+ case "workflow.started":
13
+ case "workflow.completed":
14
+ case "workflow.failed":
15
+ case "cancellation.received":
16
+ return event.type
17
+ case "sleep.started":
18
+ case "sleep.completed":
19
+ case "signal.waiting":
20
+ case "signal.received":
21
+ case "signal.timeout":
22
+ case "all.started":
23
+ case "all.completed":
24
+ case "all.failed":
25
+ return `${event.type}:${event.activityName}`
26
+ default:
27
+ return undefined
28
+ }
29
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,314 @@
1
+ import { mkdirSync } from "node:fs"
2
+ import path from "node:path"
3
+ import { NodeRuntime } from "@effect/platform-node"
4
+ import { SqliteClient } from "@effect/sql-sqlite-bun"
5
+ import { Effect, Exit, Layer, ManagedRuntime, Schema } from "effect"
6
+ import { ClusterWorkflowEngine, SingleRunner } from "effect/unstable/cluster"
7
+ import { SqlClient } from "effect/unstable/sql"
8
+ import { DurableDeferred, WorkflowEngine } from "effect/unstable/workflow"
9
+ import { currentSecretResolver, removeExecutionSecretResolver, setExecutionSecretResolver } from "./core.ts"
10
+ import type { DefinedWorkflow, SecretResolver } from "./core.ts"
11
+ import {
12
+ currentWorkflowEventSink,
13
+ emitWorkflowEvent,
14
+ removeExecutionEventSink,
15
+ setExecutionEventSink
16
+ } from "./events.ts"
17
+ import type { WorkflowEventSink } from "./events.ts"
18
+
19
+ export interface ExecuteWorkflowOptions {
20
+ readonly onEvent?: WorkflowEventSink
21
+ readonly engineDatabasePath?: string
22
+ }
23
+
24
+ export interface WorkflowRuntimeOptions {
25
+ readonly backend: "memory" | "sqlite"
26
+ readonly databasePath?: string
27
+ /** Resolves SecretRef inputs to their values at step execution time.
28
+ * Only the reference string is ever persisted. */
29
+ readonly secrets?: SecretResolver
30
+ readonly sqliteBusyTimeoutMs?: number
31
+ /** How often the engine polls storage for due timers and undelivered
32
+ * messages. Durable timers (signal timeouts, long sleeps) can fire up to
33
+ * one interval late. Defaults to 250ms. */
34
+ readonly timerPollIntervalMs?: number
35
+ }
36
+
37
+ export interface WorkflowRuntime {
38
+ readonly backend: "memory" | "sqlite"
39
+ readonly databasePath?: string
40
+ readonly secrets?: SecretResolver
41
+ register(workflows: ReadonlyArray<any>): void
42
+ getWorkflow(name: string, version: number): DefinedWorkflow | undefined
43
+ getLatestWorkflow(name: string): DefinedWorkflow | undefined
44
+ listWorkflows(name?: string): ReadonlyArray<DefinedWorkflow>
45
+ execute(options: {
46
+ readonly workflow: DefinedWorkflow
47
+ readonly payload: unknown
48
+ readonly executionId: string
49
+ readonly onEvent?: WorkflowEventSink
50
+ }): Promise<unknown>
51
+ deliverSignal(options: {
52
+ readonly workflow: DefinedWorkflow
53
+ readonly executionId: string
54
+ readonly deferredName: string
55
+ readonly payload: unknown
56
+ readonly onEvent?: WorkflowEventSink
57
+ }): Promise<void>
58
+ interrupt(options: {
59
+ readonly workflow: DefinedWorkflow
60
+ readonly executionId: string
61
+ }): Promise<void>
62
+ /** Wake a suspended execution so it replays to its suspension point.
63
+ * No-op unless the run is recorded as suspended. */
64
+ resume(options: {
65
+ readonly workflow: DefinedWorkflow
66
+ readonly executionId: string
67
+ }): Promise<void>
68
+ }
69
+
70
+ export class WorkflowVersionConflictError extends Error {
71
+ readonly _tag = "WorkflowVersionConflictError"
72
+
73
+ constructor(options: { readonly name: string; readonly version: number }) {
74
+ super(
75
+ `Workflow ${options.name}@v${options.version} is already registered with different source; register a new version instead`
76
+ )
77
+ this.name = "WorkflowVersionConflictError"
78
+ }
79
+ }
80
+
81
+ const defaultEngineDatabasePath = () => path.join(process.cwd(), ".wf", "engine.sqlite")
82
+
83
+ // All durable-execution plumbing lives here so authored workflows never touch
84
+ // the cluster engine, the runner, or the backing store.
85
+ export const makeEngineLayer = (options: {
86
+ readonly databasePath?: string
87
+ readonly sqliteBusyTimeoutMs?: number
88
+ readonly timerPollIntervalMs?: number
89
+ } = {}) => {
90
+ const databasePath = path.resolve(options.databasePath ?? defaultEngineDatabasePath())
91
+ const sqliteBusyTimeoutMs = Math.max(0, Math.trunc(options.sqliteBusyTimeoutMs ?? 5000))
92
+ // The cluster default is 10 seconds, which delays every durable timer
93
+ // (signal timeout, long sleep) by up to that long on a single-node engine.
94
+ const timerPollIntervalMs = Math.max(10, Math.trunc(options.timerPollIntervalMs ?? 250))
95
+ mkdirSync(path.dirname(databasePath), { recursive: true })
96
+ const sqliteLayer = SqliteClient.layer({ filename: databasePath })
97
+ const configuredSqliteLayer = Layer.effectDiscard(Effect.gen(function* () {
98
+ const sql = yield* SqlClient.SqlClient
99
+ yield* sql.unsafe(`PRAGMA busy_timeout = ${sqliteBusyTimeoutMs}`)
100
+ })).pipe(Layer.provideMerge(sqliteLayer))
101
+
102
+ return ClusterWorkflowEngine.layer.pipe(
103
+ Layer.provideMerge(SingleRunner.layer({
104
+ shardingConfig: { entityMessagePollInterval: timerPollIntervalMs }
105
+ })),
106
+ Layer.provide(configuredSqliteLayer)
107
+ )
108
+ }
109
+
110
+ export const engineLayer = makeEngineLayer()
111
+
112
+ export const createWorkflowRuntime = (options: WorkflowRuntimeOptions): WorkflowRuntime => {
113
+ const workflows = new Map<string, DefinedWorkflow>()
114
+ const databasePath = options.databasePath
115
+
116
+ const env = () => {
117
+ const workflowLayers = Array.from(workflows.values()).map((workflow) => workflow.layer)
118
+ const base =
119
+ options.backend === "sqlite"
120
+ ? makeEngineLayer({
121
+ ...(databasePath === undefined ? {} : { databasePath }),
122
+ ...(options.sqliteBusyTimeoutMs === undefined ? {} : { sqliteBusyTimeoutMs: options.sqliteBusyTimeoutMs }),
123
+ ...(options.timerPollIntervalMs === undefined ? {} : { timerPollIntervalMs: options.timerPollIntervalMs })
124
+ })
125
+ : WorkflowEngine.layerMemory
126
+ return workflowLayers.reduce((layer, workflowLayer) => Layer.provideMerge(workflowLayer, layer), base)
127
+ }
128
+
129
+ // ONE engine per runtime. Building a fresh cluster node per call (execute,
130
+ // signal, ...) puts several SingleRunner nodes on the same store, and they
131
+ // fight over shard ownership — message processing becomes arbitrarily late.
132
+ // Rebuilt only when the registered workflow set changes.
133
+ let managed: { readonly signature: string; readonly runtime: ManagedRuntime.ManagedRuntime<any, unknown> } | undefined
134
+
135
+ const getManagedRuntime = () => {
136
+ const signature = Array.from(workflows.keys()).sort().join(",")
137
+ if (managed === undefined || managed.signature !== signature) {
138
+ if (managed !== undefined) {
139
+ void managed.runtime.dispose()
140
+ }
141
+ managed = { signature, runtime: ManagedRuntime.make(env()) }
142
+ }
143
+ return managed.runtime
144
+ }
145
+
146
+ const runEffect = <A>(effect: Effect.Effect<A, unknown, any>, onEvent?: WorkflowEventSink) =>
147
+ getManagedRuntime().runPromise(
148
+ effect.pipe(
149
+ Effect.provideService(currentWorkflowEventSink, onEvent),
150
+ Effect.provideService(currentSecretResolver, options.secrets)
151
+ ) as Effect.Effect<A, unknown, never>
152
+ )
153
+
154
+ return {
155
+ backend: options.backend,
156
+ ...(databasePath === undefined ? {} : { databasePath }),
157
+ ...(options.secrets === undefined ? {} : { secrets: options.secrets }),
158
+
159
+ register(registered) {
160
+ for (const workflow of registered) {
161
+ const key = `${workflow.name}@${workflow.version}`
162
+ const existing = workflows.get(key)
163
+ if (existing !== undefined && existing.sourceHash !== workflow.sourceHash) {
164
+ throw new WorkflowVersionConflictError({
165
+ name: workflow.name,
166
+ version: workflow.version
167
+ })
168
+ }
169
+ workflows.set(key, workflow)
170
+ }
171
+ },
172
+
173
+ getWorkflow(name, version) {
174
+ return workflows.get(`${name}@${version}`)
175
+ },
176
+
177
+ getLatestWorkflow(name) {
178
+ return Array.from(workflows.values())
179
+ .filter((workflow) => workflow.name === name)
180
+ .sort((left, right) => right.version - left.version)[0]
181
+ },
182
+
183
+ listWorkflows(name) {
184
+ return Array.from(workflows.values())
185
+ .filter((workflow) => name === undefined || workflow.name === name)
186
+ .sort((left, right) => left.name.localeCompare(right.name) || left.version - right.version)
187
+ },
188
+
189
+ execute({ workflow, payload, executionId, onEvent }) {
190
+ const workflowName = String(workflow.workflow._tag ?? workflow.engineName)
191
+ if (onEvent !== undefined) {
192
+ setExecutionEventSink(executionId, onEvent)
193
+ }
194
+ if (options.secrets !== undefined) {
195
+ setExecutionSecretResolver(executionId, options.secrets)
196
+ }
197
+ const effect = Effect.gen(function* () {
198
+ const engine = yield* WorkflowEngine.WorkflowEngine
199
+ yield* emitWorkflowEvent({ type: "workflow.started", workflowName, payload })
200
+ const result = yield* engine.execute(workflow.workflow, {
201
+ executionId,
202
+ payload: payload as any
203
+ }).pipe(
204
+ Effect.tap((result: unknown) =>
205
+ emitWorkflowEvent({ type: "workflow.completed", workflowName, result })
206
+ ),
207
+ Effect.tapError((error: unknown) =>
208
+ emitWorkflowEvent({ type: "workflow.failed", workflowName, error })
209
+ )
210
+ )
211
+ return result
212
+ })
213
+ return runEffect(effect, onEvent).finally(() => {
214
+ removeExecutionEventSink(executionId)
215
+ removeExecutionSecretResolver(executionId)
216
+ })
217
+ },
218
+
219
+ deliverSignal({ workflow, executionId, deferredName, payload, onEvent }) {
220
+ if (onEvent !== undefined) {
221
+ setExecutionEventSink(executionId, onEvent)
222
+ }
223
+ // The resumed replay may execute steps in THIS process (see the resume
224
+ // note below), so it needs the secret resolver just like execute().
225
+ if (options.secrets !== undefined) {
226
+ setExecutionSecretResolver(executionId, options.secrets)
227
+ }
228
+ const effect = Effect.gen(function* () {
229
+ const engine = yield* WorkflowEngine.WorkflowEngine
230
+ const deferred = DurableDeferred.make(deferredName, { success: Schema.Unknown })
231
+ yield* engine.deferredDone(deferred, {
232
+ workflowName: workflow.workflow._tag,
233
+ executionId,
234
+ deferredName,
235
+ exit: Exit.succeed(payload)
236
+ })
237
+ // deferredDone only resumes a run whose Suspended reply is already
238
+ // persisted. A delivery racing the suspension write would otherwise
239
+ // sit unnoticed until another wake-up, so nudge resume a few times
240
+ // (resume is a no-op unless the run is recorded as suspended).
241
+ for (let attempt = 0; attempt < 5; attempt++) {
242
+ yield* Effect.sleep("100 millis")
243
+ yield* engine.resume(workflow.workflow, executionId)
244
+ }
245
+ })
246
+ // The resumed replay may run inside THIS call's engine environment, so
247
+ // it needs the same event sink as the original execute to record
248
+ // history (compensations, cancellation, signal receipt).
249
+ return runEffect(effect, onEvent)
250
+ },
251
+
252
+ interrupt({ workflow, executionId }) {
253
+ const effect = Effect.gen(function* () {
254
+ const engine = yield* WorkflowEngine.WorkflowEngine
255
+ yield* engine.interrupt(workflow.workflow, executionId)
256
+ })
257
+ return runEffect(effect)
258
+ },
259
+
260
+ resume({ workflow, executionId }) {
261
+ const effect = Effect.gen(function* () {
262
+ const engine = yield* WorkflowEngine.WorkflowEngine
263
+ yield* engine.resume(workflow.workflow, executionId)
264
+ })
265
+ return runEffect(effect)
266
+ }
267
+ }
268
+ }
269
+
270
+ export const makeWorkflowEffect = (
271
+ wf: DefinedWorkflow,
272
+ payload: unknown,
273
+ options: ExecuteWorkflowOptions = {}
274
+ ) => {
275
+ const env = wf.layer.pipe(
276
+ Layer.provideMerge(makeEngineLayer(
277
+ options.engineDatabasePath === undefined ? {} : { databasePath: options.engineDatabasePath }
278
+ ))
279
+ )
280
+ const workflowName = String(wf.workflow._tag ?? wf.engineName ?? "Workflow")
281
+ const execution = Effect.gen(function* () {
282
+ yield* emitWorkflowEvent({ type: "workflow.started", workflowName, payload })
283
+ const result = yield* wf.workflow.execute(payload).pipe(
284
+ Effect.tap((result: unknown) =>
285
+ emitWorkflowEvent({ type: "workflow.completed", workflowName, result })
286
+ ),
287
+ Effect.tapError((error: unknown) =>
288
+ emitWorkflowEvent({ type: "workflow.failed", workflowName, error })
289
+ )
290
+ )
291
+ return result
292
+ })
293
+
294
+ return execution.pipe(
295
+ Effect.provide(env),
296
+ Effect.provideService(currentWorkflowEventSink, options.onEvent)
297
+ )
298
+ }
299
+
300
+ export const executeWorkflow = (
301
+ wf: DefinedWorkflow,
302
+ payload: unknown,
303
+ options: ExecuteWorkflowOptions = {}
304
+ ) =>
305
+ Effect.runPromise(
306
+ makeWorkflowEffect(wf, payload, options) as Effect.Effect<unknown, unknown, never>
307
+ )
308
+
309
+ // Execute a workflow to completion as a standalone program.
310
+ export const run = (wf: DefinedWorkflow, payload: unknown) => {
311
+ return (makeWorkflowEffect(wf, payload) as Effect.Effect<unknown, unknown, never>).pipe(
312
+ NodeRuntime.runMain
313
+ )
314
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { Schema } from "effect"
2
+
3
+ interface SchemaVocabulary {
4
+ readonly string: typeof Schema.String
5
+ readonly number: typeof Schema.Number
6
+ readonly boolean: typeof Schema.Boolean
7
+ readonly void: typeof Schema.Void
8
+ readonly date: typeof Schema.Date
9
+ readonly struct: typeof Schema.Struct
10
+ readonly array: <S extends Schema.Constraint>(schema: S) => Schema.$Array<S>
11
+ readonly literal: typeof Schema.Literal
12
+ readonly taggedStruct: typeof Schema.TaggedStruct
13
+ readonly optional: <S extends Schema.Constraint>(schema: S) => Schema.optional<S>
14
+ readonly union: typeof Schema.Union
15
+ readonly unknown: typeof Schema.Unknown
16
+ }
17
+
18
+ // `t` is the LLM-facing schema vocabulary. We re-export a small, lowercase
19
+ // subset of Effect's `Schema` so authored workflows never import `effect`
20
+ // directly. Add primitives here as workflows need them — keep it small.
21
+ export const t: SchemaVocabulary = {
22
+ string: Schema.String,
23
+ number: Schema.Number,
24
+ boolean: Schema.Boolean,
25
+ void: Schema.Void,
26
+ date: Schema.Date,
27
+ struct: Schema.Struct,
28
+ array: Schema.Array,
29
+ literal: Schema.Literal,
30
+ taggedStruct: Schema.TaggedStruct,
31
+ optional: Schema.optional,
32
+ union: Schema.Union,
33
+ unknown: Schema.Unknown
34
+ } as const