@swirls/looms 0.0.2 → 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 (256) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/dist/actor/cell.d.ts +9 -4
  3. package/dist/actor/cell.js +7 -11
  4. package/dist/actor/host.d.ts +7 -3
  5. package/dist/actor/host.js +55 -32
  6. package/dist/actor/index.d.ts +1 -1
  7. package/dist/actor/index.js +1 -1
  8. package/dist/actor/routing.d.ts +12 -1
  9. package/dist/actor/routing.js +57 -51
  10. package/dist/agent/agent-session.d.ts +5 -0
  11. package/dist/agent/agent-session.js +270 -0
  12. package/dist/agent/definitions-store.d.ts +2 -3
  13. package/dist/agent/definitions-store.js +2 -4
  14. package/dist/agent/definitions.d.ts +46 -20
  15. package/dist/agent/definitions.js +36 -8
  16. package/dist/agent/effects.d.ts +359 -2
  17. package/dist/agent/effects.js +60 -34
  18. package/dist/agent/events.d.ts +175 -19
  19. package/dist/agent/events.js +45 -10
  20. package/dist/agent/index.d.ts +1 -0
  21. package/dist/agent/index.js +1 -0
  22. package/dist/agent/llm.d.ts +11 -2
  23. package/dist/agent/llm.js +13 -3
  24. package/dist/agent/module.d.ts +812 -78
  25. package/dist/agent/module.js +16 -4
  26. package/dist/agent/scope.d.ts +174 -18
  27. package/dist/agent/signals.d.ts +24 -1
  28. package/dist/agent/signals.js +77 -0
  29. package/dist/agent/threads.d.ts +3 -33
  30. package/dist/agent/threads.js +20 -25
  31. package/dist/agent/tool-schema.js +0 -1
  32. package/dist/agent/types.d.ts +49 -4
  33. package/dist/agent/types.js +5 -5
  34. package/dist/ai-vercel/index.d.ts +1 -0
  35. package/dist/ai-vercel/index.js +1 -0
  36. package/dist/ai-vercel/messages.js +22 -7
  37. package/dist/ai-vercel/vercel-evaluator.d.ts +23 -0
  38. package/dist/ai-vercel/vercel-evaluator.js +68 -0
  39. package/dist/ai-vercel/vercel-llm.js +64 -44
  40. package/dist/approval/effects.d.ts +89 -0
  41. package/dist/approval/module.d.ts +179 -29
  42. package/dist/approval/projections.d.ts +0 -14
  43. package/dist/approval/projections.js +20 -3
  44. package/dist/approval/signals.d.ts +2 -0
  45. package/dist/approval/signals.js +18 -2
  46. package/dist/cli/cli.js +2 -0
  47. package/dist/cli/commands.js +12 -16
  48. package/dist/client/client.d.ts +1195 -18
  49. package/dist/client/client.js +118 -120
  50. package/dist/client/index.d.ts +1 -1
  51. package/dist/client/index.js +1 -1
  52. package/dist/client/sse.d.ts +13 -0
  53. package/dist/client/sse.js +38 -44
  54. package/dist/cloudflare/alarm-scheduler.d.ts +12 -2
  55. package/dist/cloudflare/alarm-scheduler.js +34 -7
  56. package/dist/cloudflare/durable-object.d.ts +10 -2
  57. package/dist/cloudflare/durable-object.js +59 -28
  58. package/dist/cloudflare/event-store.d.ts +7 -2
  59. package/dist/cloudflare/event-store.js +8 -7
  60. package/dist/cloudflare/index.d.ts +2 -2
  61. package/dist/cloudflare/index.js +2 -2
  62. package/dist/cloudflare/router.js +13 -14
  63. package/dist/core/catalog.d.ts +3 -3
  64. package/dist/core/catalog.js +2 -5
  65. package/dist/core/coalesce.js +5 -3
  66. package/dist/core/compose.d.ts +2 -1
  67. package/dist/core/compose.js +18 -2
  68. package/dist/core/definition-store.d.ts +11 -0
  69. package/dist/core/definition-store.js +6 -0
  70. package/dist/core/effects.d.ts +127 -15
  71. package/dist/core/effects.js +89 -17
  72. package/dist/core/envelope.d.ts +27 -7
  73. package/dist/core/envelope.js +20 -6
  74. package/dist/core/event-codec.d.ts +4 -2
  75. package/dist/core/event-codec.js +10 -5
  76. package/dist/core/event-index.js +1 -3
  77. package/dist/core/fold.d.ts +3 -0
  78. package/dist/core/fold.js +171 -24
  79. package/dist/core/ids.d.ts +5 -0
  80. package/dist/core/ids.js +35 -2
  81. package/dist/core/index.d.ts +5 -1
  82. package/dist/core/index.js +5 -1
  83. package/dist/core/kind.d.ts +22 -0
  84. package/dist/core/kind.js +17 -0
  85. package/dist/core/limits.d.ts +34 -0
  86. package/dist/core/limits.js +46 -0
  87. package/dist/core/match.d.ts +1 -0
  88. package/dist/core/match.js +10 -1
  89. package/dist/core/module-scope.d.ts +22 -20
  90. package/dist/core/module-scope.js +38 -28
  91. package/dist/core/module.d.ts +16 -5
  92. package/dist/core/module.js +4 -0
  93. package/dist/core/projection.d.ts +13 -13
  94. package/dist/core/projection.js +9 -5
  95. package/dist/core/protocol.d.ts +68 -11
  96. package/dist/core/protocol.js +82 -11
  97. package/dist/core/replay.d.ts +312 -3
  98. package/dist/core/replay.js +11 -1
  99. package/dist/core/schema.d.ts +8 -19
  100. package/dist/core/schema.js +25 -80
  101. package/dist/core/serialize.js +2 -4
  102. package/dist/core/snapshot-store.d.ts +2 -1
  103. package/dist/core/snapshot-store.js +8 -2
  104. package/dist/core/snapshots.d.ts +5 -1
  105. package/dist/core/snapshots.js +10 -15
  106. package/dist/core/sqlite-store.js +56 -11
  107. package/dist/core/state.d.ts +142 -4
  108. package/dist/core/state.js +78 -0
  109. package/dist/core/store.d.ts +38 -3
  110. package/dist/core/store.js +77 -22
  111. package/dist/core/thread.d.ts +10 -9
  112. package/dist/core/types.d.ts +5 -8
  113. package/dist/core/types.js +28 -24
  114. package/dist/core/validate-event.js +14 -4
  115. package/dist/core/worker-callback.d.ts +17 -0
  116. package/dist/core/worker-callback.js +1 -0
  117. package/dist/debugger/components/event-stream.js +9 -10
  118. package/dist/debugger/events/protocol-summarize.js +26 -0
  119. package/dist/debugger/lib/event-meta.js +1 -2
  120. package/dist/evaluator/definitions-store.d.ts +11 -0
  121. package/dist/evaluator/definitions-store.js +8 -0
  122. package/dist/evaluator/definitions.d.ts +18 -0
  123. package/dist/evaluator/definitions.js +4 -0
  124. package/dist/evaluator/effects.d.ts +226 -0
  125. package/dist/evaluator/effects.js +132 -0
  126. package/dist/evaluator/evaluator.d.ts +24 -0
  127. package/dist/evaluator/evaluator.js +67 -0
  128. package/dist/evaluator/events.d.ts +59 -0
  129. package/dist/evaluator/events.js +21 -0
  130. package/dist/evaluator/index.d.ts +11 -0
  131. package/dist/evaluator/index.js +11 -0
  132. package/dist/evaluator/module.d.ts +606 -0
  133. package/dist/evaluator/module.js +25 -0
  134. package/dist/evaluator/projections.d.ts +50 -0
  135. package/dist/evaluator/projections.js +20 -0
  136. package/dist/evaluator/scope.d.ts +59 -0
  137. package/dist/evaluator/scope.js +7 -0
  138. package/dist/evaluator/signals.d.ts +3 -0
  139. package/dist/evaluator/signals.js +17 -0
  140. package/dist/evaluator/threads.d.ts +62 -0
  141. package/dist/evaluator/threads.js +117 -0
  142. package/dist/evaluator/types.d.ts +125 -0
  143. package/dist/evaluator/types.js +51 -0
  144. package/dist/projectors/delivery.d.ts +83 -0
  145. package/dist/projectors/delivery.js +182 -0
  146. package/dist/projectors/index-model.d.ts +1 -1
  147. package/dist/projectors/index-model.js +4 -6
  148. package/dist/projectors/index.d.ts +2 -0
  149. package/dist/projectors/index.js +2 -0
  150. package/dist/projectors/memory.d.ts +1 -1
  151. package/dist/projectors/memory.js +10 -8
  152. package/dist/projectors/postgres.js +44 -58
  153. package/dist/projectors/projector.d.ts +17 -0
  154. package/dist/projectors/projector.js +26 -1
  155. package/dist/projectors/sqlite-delivery.d.ts +4 -0
  156. package/dist/projectors/sqlite-delivery.js +147 -0
  157. package/dist/projectors/sqlite.js +23 -23
  158. package/dist/projectors/with-projectors.d.ts +7 -2
  159. package/dist/projectors/with-projectors.js +27 -28
  160. package/dist/react/notify.js +11 -8
  161. package/dist/react/react.d.ts +2 -3
  162. package/dist/react/react.js +14 -30
  163. package/dist/react/sse.d.ts +1 -1
  164. package/dist/react/sse.js +1 -1
  165. package/dist/react/store.d.ts +2 -2
  166. package/dist/react/store.js +109 -119
  167. package/dist/runtime/cancellation.d.ts +16 -0
  168. package/dist/runtime/cancellation.js +129 -0
  169. package/dist/runtime/deadline-transition-planner.d.ts +31 -0
  170. package/dist/runtime/deadline-transition-planner.js +139 -0
  171. package/dist/runtime/dispatch-effect.d.ts +3 -2
  172. package/dist/runtime/dispatch-effect.js +7 -13
  173. package/dist/runtime/errors.d.ts +51 -0
  174. package/dist/runtime/errors.js +39 -0
  175. package/dist/runtime/{runtime-helpers.d.ts → helpers.d.ts} +24 -4
  176. package/dist/runtime/{runtime-helpers.js → helpers.js} +56 -19
  177. package/dist/runtime/index.d.ts +3 -3
  178. package/dist/runtime/index.js +3 -3
  179. package/dist/runtime/ingress.d.ts +15 -0
  180. package/dist/runtime/ingress.js +42 -0
  181. package/dist/runtime/local-effect-executor.d.ts +21 -0
  182. package/dist/runtime/local-effect-executor.js +137 -0
  183. package/dist/runtime/looms.d.ts +22 -4
  184. package/dist/runtime/looms.js +54 -36
  185. package/dist/runtime/observer.d.ts +45 -0
  186. package/dist/runtime/observer.js +11 -0
  187. package/dist/runtime/repository.d.ts +55 -0
  188. package/dist/runtime/repository.js +278 -0
  189. package/dist/runtime/retry-policy.d.ts +16 -0
  190. package/dist/runtime/retry-policy.js +29 -0
  191. package/dist/runtime/run-execution-context.d.ts +30 -0
  192. package/dist/runtime/run-execution-context.js +56 -0
  193. package/dist/runtime/run-start.d.ts +4 -0
  194. package/dist/runtime/run-start.js +44 -0
  195. package/dist/runtime/runtime.d.ts +6 -87
  196. package/dist/runtime/runtime.js +233 -470
  197. package/dist/runtime/server/auth.d.ts +40 -0
  198. package/dist/runtime/server/auth.js +40 -0
  199. package/dist/runtime/server/fetch-handler.d.ts +14 -0
  200. package/dist/runtime/server/fetch-handler.js +51 -0
  201. package/dist/runtime/server/http.d.ts +37 -0
  202. package/dist/runtime/server/http.js +62 -0
  203. package/dist/runtime/server/route-table.d.ts +55 -0
  204. package/dist/runtime/server/route-table.js +74 -0
  205. package/dist/runtime/server/routes/effects.d.ts +3 -0
  206. package/dist/runtime/server/routes/effects.js +59 -0
  207. package/dist/runtime/server/routes/operations.d.ts +3 -0
  208. package/dist/runtime/server/routes/operations.js +14 -0
  209. package/dist/runtime/server/routes/runs.d.ts +3 -0
  210. package/dist/runtime/server/routes/runs.js +192 -0
  211. package/dist/runtime/server.d.ts +2 -8
  212. package/dist/runtime/server.js +4 -211
  213. package/dist/runtime/sse.d.ts +2 -1
  214. package/dist/runtime/sse.js +24 -17
  215. package/dist/runtime/types.d.ts +115 -0
  216. package/dist/runtime/types.js +1 -0
  217. package/dist/runtime/wake-engine.d.ts +20 -0
  218. package/dist/runtime/wake-engine.js +311 -0
  219. package/dist/runtime/wake-scheduler.d.ts +3 -4
  220. package/dist/runtime/wake-scheduler.js +25 -31
  221. package/dist/runtime/worker-callbacks.d.ts +16 -0
  222. package/dist/runtime/worker-callbacks.js +167 -0
  223. package/dist/runtime/worker-effect-dispatch.d.ts +36 -0
  224. package/dist/runtime/worker-effect-dispatch.js +126 -0
  225. package/dist/s2/config.d.ts +1 -1
  226. package/dist/s2/config.js +1 -1
  227. package/dist/s2/lite.js +70 -82
  228. package/dist/s2/pagination.d.ts +15 -0
  229. package/dist/s2/pagination.js +36 -0
  230. package/dist/s2/projector.js +6 -5
  231. package/dist/s2/snapshot-store.js +148 -163
  232. package/dist/s2/store.d.ts +3 -21
  233. package/dist/s2/store.js +71 -165
  234. package/dist/testing/index.d.ts +15 -0
  235. package/dist/testing/index.js +53 -16
  236. package/dist/workflow/definitions-store.d.ts +2 -3
  237. package/dist/workflow/definitions-store.js +2 -2
  238. package/dist/workflow/definitions.d.ts +78 -22
  239. package/dist/workflow/definitions.js +80 -14
  240. package/dist/workflow/effects.d.ts +453 -107
  241. package/dist/workflow/effects.js +214 -115
  242. package/dist/workflow/events.d.ts +149 -5
  243. package/dist/workflow/events.js +27 -5
  244. package/dist/workflow/graph.d.ts +56 -0
  245. package/dist/workflow/graph.js +207 -0
  246. package/dist/workflow/index.d.ts +1 -0
  247. package/dist/workflow/index.js +1 -0
  248. package/dist/workflow/module.d.ts +1047 -266
  249. package/dist/workflow/projections.d.ts +14 -12
  250. package/dist/workflow/projections.js +30 -8
  251. package/dist/workflow/scope.d.ts +146 -4
  252. package/dist/workflow/threads.d.ts +383 -155
  253. package/dist/workflow/threads.js +308 -110
  254. package/package.json +7 -2
  255. package/dist/runtime/events-api.d.ts +0 -4
  256. package/dist/runtime/events-api.js +0 -84
@@ -1,35 +1,235 @@
1
- import { Predicate, Schema } from 'effect';
2
- import { asJson, createWaitId, emit, fail, invoke, isWithdrawnError, spawn, wait, } from '@swirls/looms/core';
3
- import {} from './definitions.js';
1
+ import { Effect, Predicate, Schema } from 'effect';
2
+ import { cancel, createWaitId, DEFAULT_DEFINITION_VERSION, EventInputSchema, emit, fail, invoke, isJsonObject, isWithdrawnError, RuntimeEffectSchema, spawn, stableEffectId, wait, utf8JsonBytes, waitOutcome, } from '@swirls/looms/core';
3
+ import { NodeStateSchema } from './definitions.js';
4
4
  import { runNodeEffect, scheduleEffect } from './effects.js';
5
+ import { iterationChildWorkflowId, nodeRunKey, WorkflowValueTooLargeError } from './graph.js';
5
6
  import { workflowModule } from './scope.js';
6
7
  export { NodeStateSchema, NodeStatusSchema } from './definitions.js';
7
- // SAFETY: WorkflowState is folded by reducers; Schema.Unknown is a typed placeholder, not a decoder.
8
- export const WorkflowStateSchema = Schema.Unknown;
8
+ const attemptNumber = Schema.Finite.pipe(Schema.withDecodingDefaultKey(Effect.succeed(1)));
9
+ export const WorkflowPendingSpawnSchema = Schema.Struct({
10
+ childThreadId: Schema.String,
11
+ kind: Schema.String,
12
+ definitionName: Schema.String,
13
+ definitionVersion: Schema.String,
14
+ nodeId: Schema.String,
15
+ attempt: attemptNumber,
16
+ input: Schema.Json,
17
+ loopIndex: Schema.NullOr(Schema.Finite).pipe(Schema.withDecodingDefaultKey(Effect.succeed(null))),
18
+ });
19
+ export const WorkflowPendingMapSchema = Schema.Struct({
20
+ nodeId: Schema.String,
21
+ attempt: attemptNumber,
22
+ kind: Schema.String,
23
+ definitionName: Schema.String,
24
+ definitionVersion: Schema.String,
25
+ items: Schema.Array(Schema.Json),
26
+ concurrency: Schema.Finite,
27
+ limitBytes: Schema.Finite,
28
+ /** Items before this index have been started; later ones wait for a free slot. */
29
+ nextIndex: Schema.Finite,
30
+ outputs: Schema.Record(Schema.String, Schema.Json),
31
+ });
32
+ export const WorkflowPendingSleepSchema = Schema.Struct({
33
+ waitId: Schema.String,
34
+ wakeAt: Schema.Finite,
35
+ nodeId: Schema.String,
36
+ });
37
+ export const WorkflowPendingEffectsSchema = Schema.Struct({
38
+ nodeId: Schema.String,
39
+ effects: Schema.mutable(Schema.Array(RuntimeEffectSchema)),
40
+ });
41
+ export const WorkflowPendingEmitSchema = Schema.Struct({
42
+ id: Schema.String,
43
+ event: EventInputSchema,
44
+ });
45
+ export const WorkflowStateSchema = Schema.Struct({
46
+ definitionName: Schema.optional(Schema.String),
47
+ definitionVersion: Schema.optional(Schema.String),
48
+ nodes: Schema.Record(Schema.String, NodeStateSchema),
49
+ concurrency: Schema.Finite,
50
+ input: Schema.Json,
51
+ nodeIds: Schema.mutable(Schema.Array(Schema.String)),
52
+ needsSchedule: Schema.Boolean,
53
+ runningNodes: Schema.mutable(Schema.Array(Schema.String)),
54
+ pendingSpawns: Schema.mutable(Schema.Array(WorkflowPendingSpawnSchema)),
55
+ pendingMaps: Schema.mutable(Schema.Array(WorkflowPendingMapSchema)).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
56
+ pendingSleeps: Schema.mutable(Schema.Array(WorkflowPendingSleepSchema)),
57
+ pendingEffects: Schema.mutable(Schema.Array(WorkflowPendingEffectsSchema)),
58
+ pendingEmits: Schema.mutable(Schema.Array(WorkflowPendingEmitSchema)),
59
+ /** Children of stopped nodes that were still running; each is cancelled once. */
60
+ pendingCancels: Schema.mutable(Schema.Array(Schema.String)).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
61
+ status: Schema.Union([
62
+ Schema.Literal('running'),
63
+ Schema.Literal('completed'),
64
+ Schema.Literal('failed'),
65
+ ]),
66
+ error: Schema.NullOr(Schema.String),
67
+ });
68
+ const initialWorkflowStatus = 'running';
9
69
  function scheduleInvocation(state) {
10
70
  return invoke(scheduleEffect, {
11
71
  definitionName: state.definitionName,
72
+ definitionVersion: state.definitionVersion ?? DEFAULT_DEFINITION_VERSION,
12
73
  nodes: state.nodes,
13
74
  input: state.input,
14
75
  });
15
76
  }
16
77
  function runNodeInvocation(state, nodeId) {
17
- const results = {};
18
- for (const [id, node] of Object.entries(state.nodes)) {
19
- results[id] = node.result;
20
- }
78
+ const node = state.nodes[nodeId];
79
+ const attempts = node?.attempts ?? 1;
21
80
  return invoke(runNodeEffect, {
22
81
  definitionName: state.definitionName,
82
+ definitionVersion: state.definitionVersion ?? DEFAULT_DEFINITION_VERSION,
23
83
  nodeId,
84
+ attempt: attempts,
85
+ iteration: node?.iteration,
24
86
  input: state.input,
25
- results,
26
- }, `runNode_${nodeId}`);
87
+ nodes: state.nodes,
88
+ }, `runNode_${nodeRunKey(nodeId, attempts, node?.iteration?.index)}`);
89
+ }
90
+ function spawnAndWait(threadId, child, childThreadId, input, tag) {
91
+ return [
92
+ spawn({
93
+ childThreadId,
94
+ kind: child.kind,
95
+ definitionName: child.definitionName,
96
+ definitionVersion: child.definitionVersion,
97
+ input,
98
+ }),
99
+ wait({
100
+ waitId: createWaitId(threadId, `spawn_${childThreadId}`),
101
+ on: {
102
+ type: ['runtime.thread.completed', 'runtime.thread.failed'],
103
+ match: { threadId: childThreadId },
104
+ },
105
+ tag,
106
+ }),
107
+ ];
108
+ }
109
+ function mapChildThreadId(threadId, map, index) {
110
+ return iterationChildWorkflowId(threadId, map.nodeId, 'map', map.attempt, index);
111
+ }
112
+ /** Every effect a node currently needs, attributed to that node. */
113
+ function nodeEffects(state, threadId) {
114
+ const effects = [];
115
+ const add = (nodeId, items) => {
116
+ for (const effect of items) {
117
+ effects.push({ nodeId, effect });
118
+ }
119
+ };
120
+ for (const nodeId of state.runningNodes) {
121
+ add(nodeId, [runNodeInvocation(state, nodeId)]);
122
+ }
123
+ for (const item of state.pendingSpawns) {
124
+ const tag = { nodeId: item.nodeId, attempt: item.attempt };
125
+ add(item.nodeId, spawnAndWait(threadId, item, item.childThreadId, item.input, {
126
+ ...tag,
127
+ loopIndex: item.loopIndex,
128
+ }));
129
+ }
130
+ for (const map of state.pendingMaps) {
131
+ for (let index = 0; index < map.nextIndex; index++) {
132
+ if (!(String(index) in map.outputs)) {
133
+ add(map.nodeId, spawnAndWait(threadId, map, mapChildThreadId(threadId, map, index), map.items[index] ?? null, { nodeId: map.nodeId, attempt: map.attempt, index }));
134
+ }
135
+ }
136
+ }
137
+ for (const sleep of state.pendingSleeps) {
138
+ add(sleep.nodeId, [
139
+ wait({ waitId: sleep.waitId, on: { timerAt: sleep.wakeAt }, tag: { nodeId: sleep.nodeId } }),
140
+ ]);
141
+ }
142
+ for (const pending of state.pendingEffects) {
143
+ add(pending.nodeId, pending.effects);
144
+ }
145
+ return effects;
146
+ }
147
+ function nodeIdForEffect(state, threadId, effectId) {
148
+ const match = nodeEffects(state, threadId).find((item) => stableEffectId(threadId, item.effect) === effectId);
149
+ return match?.nodeId ?? null;
150
+ }
151
+ /** Children a node started that have not reported back yet. */
152
+ function unsettledChildren(state, nodeId, threadId) {
153
+ const children = state.pendingSpawns
154
+ .filter((item) => item.nodeId === nodeId)
155
+ .map((item) => item.childThreadId);
156
+ for (const map of state.pendingMaps) {
157
+ if (map.nodeId !== nodeId) {
158
+ continue;
159
+ }
160
+ for (let index = 0; index < map.nextIndex; index++) {
161
+ if (!(String(index) in map.outputs)) {
162
+ children.push(mapChildThreadId(threadId, map, index));
163
+ }
164
+ }
165
+ }
166
+ return children;
167
+ }
168
+ /**
169
+ * Drops a node's pending work and cancels the children it leaves running. `settled` is the child
170
+ * whose result is finishing the node, which must not be cancelled.
171
+ */
172
+ function withoutPendingWork(state, nodeId, threadId, settled = null) {
173
+ const cancels = unsettledChildren(state, nodeId, threadId).filter((childThreadId) => childThreadId !== settled && !state.pendingCancels.includes(childThreadId));
174
+ return {
175
+ ...state,
176
+ pendingCancels: cancels.length === 0 ? state.pendingCancels : [...state.pendingCancels, ...cancels],
177
+ pendingSpawns: state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
178
+ pendingMaps: state.pendingMaps.filter((item) => item.nodeId !== nodeId),
179
+ pendingSleeps: state.pendingSleeps.filter((item) => item.nodeId !== nodeId),
180
+ pendingEffects: state.pendingEffects.filter((item) => item.nodeId !== nodeId),
181
+ };
182
+ }
183
+ function stopNode(state, nodeId, threadId) {
184
+ return {
185
+ ...withoutPendingWork(state, nodeId, threadId),
186
+ runningNodes: state.runningNodes.filter((id) => id !== nodeId),
187
+ };
188
+ }
189
+ /** Drops a node's pending work and emits the event that durably finishes it. */
190
+ function finishNode(state, nodeId, result, error, threadId, settled = null) {
191
+ const id = `finished_${nodeId}`;
192
+ const event = {
193
+ type: 'workflow.node.finished',
194
+ payload: { nodeId, result, error },
195
+ threadId,
196
+ };
197
+ return {
198
+ ...withoutPendingWork(state, nodeId, threadId, settled),
199
+ pendingEmits: [...state.pendingEmits.filter((item) => item.id !== id), { id, event }],
200
+ };
201
+ }
202
+ function recordMapOutput(state, map, index, output, threadId) {
203
+ if (String(index) in map.outputs) {
204
+ return state;
205
+ }
206
+ const settled = mapChildThreadId(threadId, map, index);
207
+ const outputs = { ...map.outputs, [String(index)]: output };
208
+ const size = utf8JsonBytes(outputs);
209
+ if (size > map.limitBytes) {
210
+ const error = new WorkflowValueTooLargeError(`Node ${map.nodeId} outputs`, size, map.limitBytes);
211
+ return finishNode(state, map.nodeId, null, error.message, threadId, settled);
212
+ }
213
+ if (Object.keys(outputs).length === map.items.length) {
214
+ const ordered = map.items.map((_, i) => outputs[String(i)] ?? null);
215
+ return finishNode(state, map.nodeId, ordered, null, threadId, settled);
216
+ }
217
+ const nextIndex = Math.min(map.items.length, map.nextIndex + 1);
218
+ return {
219
+ ...state,
220
+ pendingMaps: state.pendingMaps.map((item) => item.nodeId === map.nodeId ? { ...item, outputs, nextIndex } : item),
221
+ };
222
+ }
223
+ function isCurrentAttempt(state, nodeId, attempt) {
224
+ const node = state.nodes[nodeId];
225
+ return node?.status === 'running' && (attempt === undefined || attempt === node.attempts);
27
226
  }
28
227
  export const workflowThread = workflowModule.thread({
29
228
  kind: 'workflow',
30
229
  shape: WorkflowStateSchema,
31
230
  initialState: (ctx) => ({
32
231
  definitionName: ctx.definitionName,
232
+ definitionVersion: ctx.definitionVersion,
33
233
  nodes: {},
34
234
  concurrency: 8,
35
235
  input: ctx.input,
@@ -37,10 +237,12 @@ export const workflowThread = workflowModule.thread({
37
237
  needsSchedule: false,
38
238
  runningNodes: [],
39
239
  pendingSpawns: [],
240
+ pendingMaps: [],
40
241
  pendingSleeps: [],
41
242
  pendingEffects: [],
42
243
  pendingEmits: [],
43
- status: 'running',
244
+ pendingCancels: [],
245
+ status: initialWorkflowStatus,
44
246
  error: null,
45
247
  }),
46
248
  step(state, event, ctx) {
@@ -48,6 +250,7 @@ export const workflowThread = workflowModule.thread({
48
250
  case 'runtime.thread.started': {
49
251
  const payload = event.payload;
50
252
  const defName = payload.definitionName || state.definitionName;
253
+ const defVersion = payload.definitionVersion ?? state.definitionVersion ?? DEFAULT_DEFINITION_VERSION;
51
254
  const inputObj = Predicate.isObject(payload.input) ? payload.input : {};
52
255
  const nodeIds = Array.isArray(inputObj.nodeIds)
53
256
  ? inputObj.nodeIds.filter(Predicate.isString)
@@ -59,6 +262,7 @@ export const workflowThread = workflowModule.thread({
59
262
  return {
60
263
  ...state,
61
264
  definitionName: defName,
265
+ definitionVersion: defVersion,
62
266
  nodes,
63
267
  nodeIds,
64
268
  input: payload.input ?? state.input,
@@ -74,7 +278,12 @@ export const workflowThread = workflowModule.thread({
74
278
  ...state,
75
279
  nodes: {
76
280
  ...state.nodes,
77
- [nodeId]: { status: 'running', result: null, error: null },
281
+ [nodeId]: {
282
+ status: 'running',
283
+ result: null,
284
+ error: null,
285
+ attempts: (state.nodes[nodeId]?.attempts ?? 0) + 1,
286
+ },
78
287
  },
79
288
  runningNodes: state.runningNodes.includes(nodeId)
80
289
  ? state.runningNodes
@@ -83,46 +292,71 @@ export const workflowThread = workflowModule.thread({
83
292
  };
84
293
  }
85
294
  case 'workflow.node.finished': {
86
- const { nodeId, result, error } = event.payload;
295
+ const { nodeId, result, error, branch } = event.payload;
87
296
  if (!nodeId) {
88
297
  return state;
89
298
  }
90
299
  const failed = Predicate.isString(error) && error.length > 0;
91
300
  return {
92
- ...state,
301
+ ...stopNode(state, nodeId, ctx.threadId),
93
302
  nodes: {
94
303
  ...state.nodes,
95
304
  [nodeId]: {
96
305
  status: failed ? 'failed' : 'completed',
97
306
  result: result ?? null,
98
307
  error: failed ? error : null,
308
+ attempts: state.nodes[nodeId]?.attempts ?? 1,
309
+ branch,
99
310
  },
100
311
  },
101
- runningNodes: state.runningNodes.filter((id) => id !== nodeId),
102
- pendingSpawns: state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
103
- pendingSleeps: state.pendingSleeps.filter((item) => item.nodeId !== nodeId),
104
- pendingEffects: state.pendingEffects.filter((item) => item.nodeId !== nodeId),
105
312
  pendingEmits: state.pendingEmits.filter((item) => item.id !== `finished_${nodeId}`),
106
- status: failed ? 'failed' : state.status,
107
313
  error: failed ? error : state.error,
108
- needsSchedule: !failed,
314
+ needsSchedule: true,
315
+ };
316
+ }
317
+ case 'workflow.node.skipped': {
318
+ const { nodeId, reason, cause } = event.payload;
319
+ return {
320
+ ...stopNode(state, nodeId, ctx.threadId),
321
+ nodes: {
322
+ ...state.nodes,
323
+ [nodeId]: {
324
+ status: 'skipped',
325
+ result: null,
326
+ error: reason,
327
+ attempts: state.nodes[nodeId]?.attempts ?? 0,
328
+ skipCause: cause,
329
+ },
330
+ },
331
+ needsSchedule: true,
109
332
  };
110
333
  }
111
334
  case 'workflow.spawn.requested': {
112
- const { childThreadId, kind, definitionName, nodeId, input } = event.payload;
113
- if (!childThreadId || !kind || !definitionName || !nodeId) {
335
+ const { nodeId } = event.payload;
336
+ if (!isCurrentAttempt(state, nodeId, event.payload.attempt)) {
114
337
  return state;
115
338
  }
116
339
  return {
117
340
  ...state,
118
341
  pendingSpawns: [
119
342
  ...state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
343
+ event.payload,
344
+ ],
345
+ };
346
+ }
347
+ case 'workflow.map.requested': {
348
+ const { nodeId } = event.payload;
349
+ if (!isCurrentAttempt(state, nodeId, event.payload.attempt)) {
350
+ return state;
351
+ }
352
+ return {
353
+ ...state,
354
+ pendingMaps: [
355
+ ...state.pendingMaps.filter((item) => item.nodeId !== nodeId),
120
356
  {
121
- childThreadId,
122
- kind,
123
- definitionName,
124
- nodeId,
125
- input: input ?? null,
357
+ ...event.payload,
358
+ nextIndex: Math.min(event.payload.concurrency, event.payload.items.length),
359
+ outputs: {},
126
360
  },
127
361
  ],
128
362
  };
@@ -146,13 +380,16 @@ export const workflowThread = workflowModule.thread({
146
380
  if (!nodeId) {
147
381
  return state;
148
382
  }
383
+ const attempts = state.nodes[nodeId]?.attempts ?? 1;
384
+ const key = nodeRunKey(nodeId, attempts, state.nodes[nodeId]?.iteration?.index);
149
385
  const taggedEffects = effects.map((effect, idx) => {
150
386
  if (effect.type === 'runtime.wait' && 'on' in effect) {
151
- const tag = Predicate.isObject(effect.tag) ? { ...effect.tag, nodeId } : { nodeId };
387
+ const scope = { nodeId, attempt: attempts };
388
+ const tag = Predicate.isObject(effect.tag) ? { ...effect.tag, ...scope } : scope;
152
389
  return { ...effect, tag };
153
390
  }
154
391
  if (!('tag' in effect && Predicate.isString(effect.tag))) {
155
- return { ...effect, tag: `${nodeId}_${effect.type}_${idx}` };
392
+ return { ...effect, tag: `${key}_${effect.type}_${idx}` };
156
393
  }
157
394
  return effect;
158
395
  });
@@ -169,72 +406,58 @@ export const workflowThread = workflowModule.thread({
169
406
  if (isWithdrawnError(error)) {
170
407
  return state;
171
408
  }
172
- const running = Object.entries(state.nodes).find(([, node]) => node.status === 'running');
173
- if (!running) {
409
+ const effectId = event.payload.effectId;
410
+ const nodeId = Predicate.isString(effectId)
411
+ ? nodeIdForEffect(state, ctx.threadId, effectId)
412
+ : null;
413
+ if (!nodeId) {
174
414
  return state;
175
415
  }
176
- const nodeId = running[0];
177
416
  const message = `Node ${nodeId} failed: ${error}`;
178
417
  return {
179
- ...state,
418
+ ...finishNode(stopNode(state, nodeId, ctx.threadId), nodeId, null, message, ctx.threadId),
180
419
  nodes: {
181
420
  ...state.nodes,
182
- [nodeId]: { status: 'failed', result: null, error: message },
183
- },
184
- runningNodes: state.runningNodes.filter((id) => id !== nodeId),
185
- pendingSpawns: state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
186
- pendingSleeps: state.pendingSleeps.filter((item) => item.nodeId !== nodeId),
187
- pendingEffects: state.pendingEffects.filter((item) => item.nodeId !== nodeId),
188
- pendingEmits: [
189
- ...state.pendingEmits.filter((item) => item.id !== `finished_${nodeId}`),
190
- {
191
- id: `finished_${nodeId}`,
192
- event: {
193
- type: 'workflow.node.finished',
194
- payload: { nodeId, result: null, error: message },
195
- threadId: ctx.threadId,
196
- },
421
+ [nodeId]: {
422
+ status: 'failed',
423
+ result: null,
424
+ error: message,
425
+ attempts: state.nodes[nodeId]?.attempts,
197
426
  },
198
- ],
427
+ },
199
428
  };
200
429
  }
201
430
  case 'runtime.wait.satisfied': {
202
431
  const tag = event.payload.tag;
203
- if (!Predicate.isObject(tag)) {
432
+ if (!isJsonObject(tag) || !Predicate.isString(tag.nodeId)) {
204
433
  return state;
205
434
  }
206
- const nodeId = Predicate.isString(tag.nodeId) ? tag.nodeId : undefined;
207
- if (!nodeId) {
435
+ const nodeId = tag.nodeId;
436
+ if (!isCurrentAttempt(state, nodeId, tag.attempt)) {
208
437
  return state;
209
438
  }
210
- const embedded = event.payload.event;
211
- const embeddedObj = Predicate.isObject(embedded) && Predicate.isObject(embedded.payload)
212
- ? embedded.payload
213
- : {};
214
- const error = Predicate.isString(embeddedObj.error) ? embeddedObj.error : null;
215
- const result = embeddedObj.output ?? (Predicate.isObject(embedded) ? embedded.payload : { waited: true });
216
- return {
217
- ...state,
218
- pendingSpawns: state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
219
- pendingSleeps: state.pendingSleeps.filter((item) => item.nodeId !== nodeId),
220
- pendingEffects: state.pendingEffects.filter((item) => item.nodeId !== nodeId),
221
- pendingEmits: [
222
- ...state.pendingEmits.filter((item) => item.id !== `finished_${nodeId}`),
223
- {
224
- id: `finished_${nodeId}`,
225
- event: {
226
- type: 'workflow.node.finished',
227
- payload: {
228
- nodeId,
229
- // SAFETY: result is serialized to a JSON-compatible node execution result.
230
- result: asJson(result) ?? null,
231
- error,
232
- },
233
- threadId: ctx.threadId,
234
- },
439
+ const { result, error } = waitOutcome(event.payload.event, tag, { waited: true });
440
+ const map = state.pendingMaps.find((item) => item.nodeId === nodeId);
441
+ if (error === null && map && Predicate.isNumber(tag.index)) {
442
+ return recordMapOutput(state, map, tag.index, result, ctx.threadId);
443
+ }
444
+ const node = state.nodes[nodeId];
445
+ if (error === null && node && Predicate.isNumber(tag.loopIndex)) {
446
+ return {
447
+ ...state,
448
+ nodes: {
449
+ ...state.nodes,
450
+ [nodeId]: { ...node, iteration: { index: tag.loopIndex + 1, previous: result } },
235
451
  },
236
- ],
237
- };
452
+ pendingSpawns: state.pendingSpawns.filter((item) => item.nodeId !== nodeId),
453
+ };
454
+ }
455
+ const satisfied = event.payload.event;
456
+ const satisfiedPayload = Predicate.isObject(satisfied) ? satisfied.payload : undefined;
457
+ const settled = Predicate.isObject(satisfiedPayload) && Predicate.isString(satisfiedPayload.threadId)
458
+ ? satisfiedPayload.threadId
459
+ : null;
460
+ return finishNode(state, nodeId, result, error, ctx.threadId, settled);
238
461
  }
239
462
  default:
240
463
  return state;
@@ -245,40 +468,15 @@ export const workflowThread = workflowModule.thread({
245
468
  if (state.needsSchedule) {
246
469
  effects.push(scheduleInvocation(state));
247
470
  }
248
- for (const nodeId of state.runningNodes) {
249
- effects.push(runNodeInvocation(state, nodeId));
250
- }
251
- for (const spawnReq of state.pendingSpawns) {
252
- const waitId = createWaitId(ctx.threadId, `spawn_${spawnReq.childThreadId}`);
253
- effects.push(spawn({
254
- childThreadId: spawnReq.childThreadId,
255
- kind: spawnReq.kind,
256
- definitionName: spawnReq.definitionName,
257
- input: spawnReq.input,
258
- }), wait({
259
- waitId,
260
- on: {
261
- type: ['runtime.thread.completed', 'runtime.thread.failed'],
262
- match: { threadId: spawnReq.childThreadId },
263
- },
264
- tag: { nodeId: spawnReq.nodeId },
265
- }));
266
- }
267
- for (const sleepReq of state.pendingSleeps) {
268
- effects.push(wait({
269
- waitId: sleepReq.waitId,
270
- on: { timerAt: sleepReq.wakeAt },
271
- tag: { nodeId: sleepReq.nodeId },
272
- }));
273
- }
274
- for (const effectReq of state.pendingEffects) {
275
- for (const eff of effectReq.effects) {
276
- effects.push(eff);
277
- }
471
+ for (const item of nodeEffects(state, ctx.threadId)) {
472
+ effects.push(item.effect);
278
473
  }
279
474
  for (const emitReq of state.pendingEmits) {
280
475
  effects.push(emit(emitReq.event));
281
476
  }
477
+ for (const childThreadId of state.pendingCancels) {
478
+ effects.push(cancel(childThreadId));
479
+ }
282
480
  if (state.status === 'failed') {
283
481
  effects.push(fail(state.error ?? 'Workflow execution failed'));
284
482
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swirls/looms",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "Pluggable event-sourced runtime for durable agents and workflows",
5
5
  "homepage": "https://github.com/ByteSliceHQ/looms#readme",
6
6
  "bugs": {
@@ -65,6 +65,10 @@
65
65
  "types": "./dist/debugger/index.d.ts",
66
66
  "import": "./dist/debugger/index.js"
67
67
  },
68
+ "./evaluator": {
69
+ "types": "./dist/evaluator/index.d.ts",
70
+ "import": "./dist/evaluator/index.js"
71
+ },
68
72
  "./projectors": {
69
73
  "types": "./dist/projectors/index.d.ts",
70
74
  "import": "./dist/projectors/index.js"
@@ -144,6 +148,7 @@
144
148
  "@looms/cloudflare": "workspace:*",
145
149
  "@looms/core": "workspace:*",
146
150
  "@looms/debugger": "workspace:*",
151
+ "@looms/evaluator": "workspace:*",
147
152
  "@looms/projectors": "workspace:*",
148
153
  "@looms/react": "workspace:*",
149
154
  "@looms/runtime": "workspace:*",
@@ -151,7 +156,7 @@
151
156
  "@looms/testing": "workspace:*",
152
157
  "@looms/workflow": "workspace:*",
153
158
  "@types/bun": "^1.3.9",
154
- "ai": "^7.0.92",
159
+ "ai": "^7.0.105",
155
160
  "postgres": "^3.4.0",
156
161
  "react": "19",
157
162
  "react-dom": "19",
@@ -1,4 +0,0 @@
1
- import { encodeLoomsEvent, type EncodedLoomsEvent, type EventStore } from '@swirls/looms/core';
2
- import type { LoomsRuntime } from './runtime';
3
- export { encodeLoomsEvent, type EncodedLoomsEvent };
4
- export declare function handleEventsApi(req: Request, runtime: LoomsRuntime, store: EventStore): Promise<Response>;
@@ -1,84 +0,0 @@
1
- import { Effect, Predicate } from 'effect';
2
- import { decodeAppendableEvent, encodeLoomsEvent, EventStoreConflictError, EventStoreTag, } from '@swirls/looms/core';
3
- import { createEventStreamResponse } from './sse.js';
4
- export { encodeLoomsEvent };
5
- function readCursor(req, url) {
6
- const lastEventId = req.headers.get('last-event-id');
7
- const raw = lastEventId !== null && lastEventId !== ''
8
- ? lastEventId
9
- : (url.searchParams.get('cursor') ?? '0');
10
- const parsed = Number(raw);
11
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
12
- }
13
- function readRunId(url) {
14
- return url.searchParams.get('runId');
15
- }
16
- export async function handleEventsApi(req, runtime, store) {
17
- const url = new URL(req.url);
18
- if (req.method === 'HEAD') {
19
- return new Response(null, { status: 200 });
20
- }
21
- if (req.method === 'GET') {
22
- const runId = readRunId(url);
23
- if (!runId) {
24
- return Response.json({ error: 'runId required' }, { status: 400 });
25
- }
26
- const cursor = readCursor(req, url);
27
- const acceptHeader = req.headers.get('accept') ?? '';
28
- const isLive = url.searchParams.get('live') === 'true' ||
29
- url.searchParams.get('live') === '1' ||
30
- acceptHeader.includes('text/event-stream');
31
- if (!isLive) {
32
- const events = await Effect.runPromise(store.read(runId, { fromSeq: cursor + 1 }));
33
- const bounds = store.bounds
34
- ? await Effect.runPromise(store.bounds(runId))
35
- : { head: events[0]?.seq ?? 1, tail: await Effect.runPromise(store.tail(runId)) };
36
- return Response.json({
37
- batch: events.map(encodeLoomsEvent),
38
- head: bounds.head,
39
- tail: bounds.tail,
40
- });
41
- }
42
- return createEventStreamResponse({
43
- signal: req.signal,
44
- request: req,
45
- store,
46
- runId,
47
- fromSeq: cursor + 1,
48
- });
49
- }
50
- if (req.method === 'POST') {
51
- const body = await req.json();
52
- const runId = readRunId(url) ??
53
- (Predicate.isReadonlyObject(body) && Predicate.isString(body.runId) ? body.runId : null);
54
- if (!runId) {
55
- return Response.json({ error: 'runId required' }, { status: 400 });
56
- }
57
- const batch = Predicate.isReadonlyObject(body) && Array.isArray(body.batch) ? body.batch : [];
58
- const expectedTail = Predicate.isReadonlyObject(body) && Predicate.isNumber(body.expectedTail)
59
- ? body.expectedTail
60
- : 0;
61
- const tail = await Effect.runPromise(store.tail(runId));
62
- if (expectedTail !== tail) {
63
- return Response.json({ _tag: 'ServerAheadError', expected: expectedTail, actual: tail }, { status: 409 });
64
- }
65
- const appendable = batch
66
- .map((item) => {
67
- // SAFETY: push batch items are host-encoded envelopes.
68
- return decodeAppendableEvent(item, runId);
69
- })
70
- .filter((item) => item !== undefined);
71
- try {
72
- await Effect.runPromise(store.append(runId, appendable, { expectedTail }));
73
- }
74
- catch (err) {
75
- if (err instanceof EventStoreConflictError) {
76
- return Response.json({ _tag: 'ServerAheadError', expected: err.expectedTail, actual: err.actualTail }, { status: 409 });
77
- }
78
- throw err;
79
- }
80
- await Effect.runPromise(Effect.provideService(runtime.wake(runId), EventStoreTag, store));
81
- return Response.json({ ok: true });
82
- }
83
- return new Response('Method Not Allowed', { status: 405 });
84
- }