@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
@@ -7,65 +7,17 @@ export class InvalidInputError extends Data.TaggedError('InvalidInputError') {
7
7
  this.issues = issues;
8
8
  }
9
9
  }
10
- function toStandard(candidate) {
11
- if (!candidate) {
12
- return undefined;
13
- }
14
- if (Schema.isSchema(candidate)) {
15
- // SAFETY: Effect Schema converts to StandardSchemaV1 via official helper.
16
- return Schema.toStandardSchemaV1(candidate);
17
- }
18
- if (Predicate.isObject(candidate) && '~standard' in candidate) {
19
- // SAFETY: verified '~standard' property exists on object.
20
- return candidate;
10
+ function isStandardSchema(candidate) {
11
+ if (!Predicate.isReadonlyObject(candidate) || !('~standard' in candidate)) {
12
+ return false;
21
13
  }
22
- if (Predicate.isObject(candidate) && 'safeParse' in candidate) {
23
- // SAFETY: candidate has safeParse property verified by key check.
24
- const safeParse = candidate.safeParse;
25
- if (Predicate.isFunction(safeParse)) {
26
- // SAFETY: verified safeParse is a function matching the Zod safeParse contract.
27
- const zodSchema = candidate;
28
- return {
29
- '~standard': {
30
- version: 1,
31
- vendor: 'zod-compat',
32
- validate(raw) {
33
- const res = zodSchema.safeParse(raw);
34
- if (res.success) {
35
- return { value: res.data };
36
- }
37
- return {
38
- issues: res.error.issues.map((issue) => ({
39
- message: issue.message,
40
- path: issue.path,
41
- })),
42
- };
43
- },
44
- },
45
- };
46
- }
47
- }
48
- return undefined;
14
+ const standard = candidate['~standard'];
15
+ return (Predicate.isReadonlyObject(standard) &&
16
+ 'validate' in standard &&
17
+ Predicate.isFunction(standard.validate));
49
18
  }
50
- function extractSchema(schemaOrDef) {
51
- const direct = toStandard(schemaOrDef);
52
- if (direct) {
53
- return direct;
54
- }
55
- if (schemaOrDef && Predicate.isObject(schemaOrDef)) {
56
- if ('schema' in schemaOrDef) {
57
- // SAFETY: verified schemaOrDef has schema property.
58
- const fromSchema = toStandard(schemaOrDef.schema);
59
- if (fromSchema) {
60
- return fromSchema;
61
- }
62
- }
63
- if ('input' in schemaOrDef) {
64
- // SAFETY: Verified schemaOrDef has input property.
65
- return toStandard(schemaOrDef.input);
66
- }
67
- }
68
- return undefined;
19
+ function toStandard(candidate) {
20
+ return isStandardSchema(candidate) ? candidate : Schema.toStandardSchemaV1(candidate);
69
21
  }
70
22
  function formatIssue(issue) {
71
23
  if (!issue.path || issue.path.length === 0) {
@@ -85,39 +37,33 @@ function formatIssue(issue) {
85
37
  .join('.');
86
38
  return `${pathStr}: ${issue.message}`;
87
39
  }
88
- export function validateInputEffect(schemaOrDef, raw) {
89
- const schema = extractSchema(schemaOrDef);
40
+ function decodeInput(schema, raw) {
90
41
  if (!schema) {
91
- // SAFETY: when no schema is provided, raw JsonValue is accepted as TInput.
92
42
  return Effect.succeed(raw);
93
43
  }
94
- return Effect.gen(function* () {
95
- let result = schema['~standard'].validate(raw);
96
- if (result instanceof Promise) {
97
- result = yield* Effect.tryPromise({
98
- // SAFETY: Standard Schema validate() returns this Promise when async.
99
- try: () => result,
100
- catch: (cause) => new InvalidInputError(`Schema validation failed: ${cause instanceof Error ? cause.message : String(cause)}`),
101
- });
102
- }
44
+ const standard = toStandard(schema);
45
+ return Effect.tryPromise({
46
+ try: () => Promise.resolve(standard['~standard'].validate(raw)),
47
+ catch: (cause) => new InvalidInputError(`Schema validation failed: ${cause instanceof Error ? cause.message : String(cause)}`),
48
+ }).pipe(Effect.flatMap((result) => {
103
49
  if (result.issues !== undefined) {
104
50
  const text = result.issues.map(formatIssue).join(', ');
105
- return yield* Effect.fail(new InvalidInputError(`Invalid input: ${text}`, result.issues));
51
+ return new InvalidInputError(`Invalid input: ${text}`, result.issues);
106
52
  }
107
- return result.value;
108
- });
53
+ return Effect.succeed(result.value);
54
+ }));
55
+ }
56
+ export function validateInputEffect(schema, raw) {
57
+ return decodeInput(schema, raw);
109
58
  }
110
- export async function validateInput(schemaOrDef, raw) {
111
- return Effect.runPromise(validateInputEffect(schemaOrDef, raw));
59
+ export function validateInput(schema, raw) {
60
+ return Effect.runPromise(decodeInput(schema, raw));
112
61
  }
113
- export function validateInputSync(schemaOrDef, raw) {
114
- const schema = extractSchema(schemaOrDef);
62
+ export function validateInputSync(schema, raw) {
115
63
  if (!schema) {
116
- // SAFETY: when no schema is provided, raw JsonValue is accepted as TInput.
117
64
  return raw;
118
65
  }
119
- // SAFETY: StandardSchemaV1.validate returns Result<TInput> or Promise<Result<TInput>>.
120
- const result = schema['~standard'].validate(raw);
66
+ const result = toStandard(schema)['~standard'].validate(raw);
121
67
  if (result instanceof Promise) {
122
68
  throw new Error('Async schema validation is not supported in synchronous context');
123
69
  }
@@ -125,6 +71,5 @@ export function validateInputSync(schemaOrDef, raw) {
125
71
  const text = result.issues.map(formatIssue).join(', ');
126
72
  throw new InvalidInputError(`Invalid input: ${text}`, result.issues);
127
73
  }
128
- // SAFETY: When result.issues is undefined, result is SuccessResult.
129
74
  return result.value;
130
75
  }
@@ -23,11 +23,9 @@ export function createKeyedSerializer() {
23
23
  tails.set(key, cleanup);
24
24
  return next;
25
25
  };
26
- const drain = async (key) => {
26
+ const drain = (key) => {
27
27
  const pending = tails.get(key);
28
- if (pending) {
29
- await pending;
30
- }
28
+ return pending ? pending.then(() => undefined) : Promise.resolve();
31
29
  };
32
30
  const clear = (key) => {
33
31
  if (key !== undefined) {
@@ -1,4 +1,5 @@
1
1
  import { Context, Effect, Option } from 'effect';
2
+ import { SnapshotPayloadTooLargeError } from './limits';
2
3
  import type { RunState } from './state';
3
4
  import type { EventStore } from './store';
4
5
  /**
@@ -25,7 +26,7 @@ export declare class SnapshotStoreError extends SnapshotStoreError_base<{
25
26
  }
26
27
  export interface SnapshotStore {
27
28
  readonly loadLatest: (runId: string) => Effect.Effect<Option.Option<RunSnapshot>, SnapshotStoreError>;
28
- readonly save: (snapshot: RunSnapshot) => Effect.Effect<void, SnapshotStoreError>;
29
+ readonly save: (snapshot: RunSnapshot) => Effect.Effect<void, SnapshotStoreError | SnapshotPayloadTooLargeError>;
29
30
  readonly listCursors?: (runId: string) => Effect.Effect<number[], SnapshotStoreError>;
30
31
  readonly prune?: (runId: string, keepLatest: number) => Effect.Effect<void, SnapshotStoreError>;
31
32
  }
@@ -1,4 +1,5 @@
1
1
  import { Context, Data, Effect, Option, Ref } from 'effect';
2
+ import { assertSnapshotByteLimit, SnapshotPayloadTooLargeError } from './limits.js';
2
3
  export class SnapshotStoreError extends Data.TaggedError('SnapshotStoreError') {
3
4
  constructor(text, cause) {
4
5
  super({ message: text, cause });
@@ -29,12 +30,17 @@ export const makeMemorySnapshotStore = Effect.gen(function* () {
29
30
  }
30
31
  return Option.some(list[list.length - 1]);
31
32
  }),
32
- save: (snapshot) => Ref.update(snapshots, (map) => {
33
+ save: (snapshot) => Effect.try({
34
+ try: () => assertSnapshotByteLimit(snapshot),
35
+ catch: (cause) => cause instanceof SnapshotPayloadTooLargeError
36
+ ? cause
37
+ : new SnapshotStoreError('snapshot size validation failed', cause),
38
+ }).pipe(Effect.andThen(Ref.update(snapshots, (map) => {
33
39
  const next = new Map(map);
34
40
  const existing = next.get(snapshot.runId) ?? [];
35
41
  next.set(snapshot.runId, [...existing, snapshot]);
36
42
  return next;
37
- }),
43
+ }))),
38
44
  listCursors: (runId) => Effect.gen(function* () {
39
45
  const map = yield* Ref.get(snapshots);
40
46
  return (map.get(runId) ?? []).map((item) => item.cursor);
@@ -5,7 +5,7 @@ import { type RunState } from './state';
5
5
  import { type JsonValue } from './types';
6
6
  export declare const DEFAULT_SNAPSHOT_EVERY = 200;
7
7
  export declare const SnapshotTakenPayloadSchema: Schema.Struct<{
8
- readonly seq: Schema.Number;
8
+ readonly seq: Schema.Finite;
9
9
  readonly stateHash: Schema.String;
10
10
  readonly state: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
11
11
  }>;
@@ -16,6 +16,10 @@ export declare function runStateToJson(state: RunState): JsonValue;
16
16
  export declare function buildSnapshotEvent(runId: string, events: readonly EventEnvelope[], registry: FoldRegistry, options?: {
17
17
  includeState?: boolean;
18
18
  }): EventEnvelope;
19
+ /**
20
+ * Pointer-only snapshot marker. Inline `state` only when provided and
21
+ * serialized state is at most `maxInlineBytes` (default 256 KiB).
22
+ */
19
23
  export declare function buildSnapshotMarker(runId: string, cursor: number, stateHash: string, options?: {
20
24
  state?: RunState;
21
25
  maxInlineBytes?: number;
@@ -3,16 +3,17 @@ import { bytesToHex } from '@noble/hashes/utils.js';
3
3
  import { Schema } from 'effect';
4
4
  import { createEvent, JsonValueSchema } from './envelope.js';
5
5
  import { foldRun } from './fold.js';
6
- import { emptyRunState } from './state.js';
7
- import { asJson, fromJsonStruct } from './types.js';
6
+ import { utf8JsonBytes } from './limits.js';
7
+ import { emptyRunState, RunStateSchema } from './state.js';
8
+ import { asJson, stringifyJson } from './types.js';
8
9
  export const DEFAULT_SNAPSHOT_EVERY = 200;
9
10
  export const SnapshotTakenPayloadSchema = Schema.Struct({
10
- seq: Schema.Number,
11
+ seq: Schema.Finite,
11
12
  stateHash: Schema.String,
12
13
  state: Schema.optional(JsonValueSchema),
13
14
  });
14
15
  export function hashRunState(state) {
15
- const json = JSON.stringify(state);
16
+ const json = stringifyJson(state);
16
17
  return bytesToHex(sha256(new TextEncoder().encode(json))).slice(0, 16);
17
18
  }
18
19
  export function shouldTakeSnapshot(events, every = DEFAULT_SNAPSHOT_EVERY) {
@@ -33,8 +34,7 @@ export function shouldTakeSnapshot(events, every = DEFAULT_SNAPSHOT_EVERY) {
33
34
  }
34
35
  export const DEFAULT_MAX_INLINE_SNAPSHOT_BYTES = 256 * 1024;
35
36
  export function runStateToJson(state) {
36
- const raw = JSON.parse(JSON.stringify(state));
37
- return Schema.decodeUnknownSync(JsonValueSchema)(raw);
37
+ return asJson(state);
38
38
  }
39
39
  export function buildSnapshotEvent(runId, events, registry, options) {
40
40
  const state = foldRun(events, registry, { runId });
@@ -45,22 +45,17 @@ export function buildSnapshotEvent(runId, events, registry, options) {
45
45
  }
46
46
  /**
47
47
  * Pointer-only snapshot marker. Inline `state` only when provided and
48
- * `JSON.stringify(state).length <= maxInlineBytes` (default 256 KiB).
48
+ * serialized state is at most `maxInlineBytes` (default 256 KiB).
49
49
  */
50
- let warnedInlineSkip = false;
51
50
  export function buildSnapshotMarker(runId, cursor, stateHash, options) {
52
51
  const maxInlineBytes = options?.maxInlineBytes ?? DEFAULT_MAX_INLINE_SNAPSHOT_BYTES;
53
52
  let payload = asJson({ seq: cursor, stateHash });
54
53
  if (options?.state !== undefined) {
55
54
  const encoded = runStateToJson(options.state);
56
- const bytes = JSON.stringify(encoded).length;
55
+ const bytes = utf8JsonBytes(encoded);
57
56
  if (bytes <= maxInlineBytes) {
58
57
  payload = asJson({ seq: cursor, stateHash, state: encoded });
59
58
  }
60
- else if (!warnedInlineSkip) {
61
- warnedInlineSkip = true;
62
- console.warn(`[looms] snapshot state is ${bytes} bytes (limit ${maxInlineBytes}); writing a pointer-only marker`);
63
- }
64
59
  }
65
60
  return createEvent(runId, {
66
61
  type: 'runtime.snapshot.taken',
@@ -86,11 +81,11 @@ export function foldFromSnapshots(events, registry, options) {
86
81
  if (decoded.state !== undefined) {
87
82
  const trailing = events.filter((e) => e.seq > decoded.seq);
88
83
  if (trailing.length === 0) {
89
- return fromJsonStruct(decoded.state);
84
+ return Schema.decodeUnknownSync(RunStateSchema)(decoded.state);
90
85
  }
91
86
  return foldRun(trailing, registry, {
92
87
  runId,
93
- initial: fromJsonStruct(decoded.state),
88
+ initial: Schema.decodeUnknownSync(RunStateSchema)(decoded.state),
94
89
  });
95
90
  }
96
91
  return foldRun(events, registry, { runId });
@@ -1,6 +1,8 @@
1
1
  import { Effect, Option, Queue, Schema, Stream } from 'effect';
2
2
  import { EventEnvelopeSchema, fromWireEvent, withAssignedSeq } from './envelope.js';
3
+ import { assertEventByteLimits, assertSnapshotByteLimit, EventPayloadTooLargeError, SnapshotPayloadTooLargeError, } from './limits.js';
3
4
  import { SnapshotStoreError, withSnapshotStore, } from './snapshot-store.js';
5
+ import { RunStateSchema } from './state.js';
4
6
  import { EventStoreConflictError, EventStoreError, EventStoreFencedError, } from './store.js';
5
7
  export const SQLITE_EVENT_STORE_SCHEMA = `
6
8
  CREATE TABLE IF NOT EXISTS looms_streams (
@@ -25,11 +27,19 @@ CREATE TABLE IF NOT EXISTS looms_snapshots (
25
27
  state TEXT NOT NULL,
26
28
  PRIMARY KEY (run_id, cursor)
27
29
  );
30
+
31
+ CREATE TABLE IF NOT EXISTS looms_commands (
32
+ run_id TEXT NOT NULL,
33
+ idempotency_key TEXT NOT NULL,
34
+ fingerprint TEXT,
35
+ committed_tail INTEGER NOT NULL,
36
+ PRIMARY KEY (run_id, idempotency_key)
37
+ );
28
38
  `.trim();
29
39
  const StreamRowSchema = Schema.Struct({
30
40
  run_id: Schema.String,
31
- head: Schema.Number,
32
- tail: Schema.Number,
41
+ head: Schema.Finite,
42
+ tail: Schema.Finite,
33
43
  fence_token: Schema.NullOr(Schema.String),
34
44
  });
35
45
  const EventRowSchema = Schema.Struct({
@@ -37,25 +47,30 @@ const EventRowSchema = Schema.Struct({
37
47
  });
38
48
  const SnapshotRowSchema = Schema.Struct({
39
49
  run_id: Schema.String,
40
- cursor: Schema.Number,
50
+ cursor: Schema.Finite,
41
51
  state_hash: Schema.String,
42
- taken_at: Schema.Number,
52
+ taken_at: Schema.Finite,
43
53
  state: Schema.String,
44
54
  });
45
55
  const RunIdRowSchema = Schema.Struct({
46
56
  run_id: Schema.String,
47
57
  });
48
58
  const CursorRowSchema = Schema.Struct({
49
- cursor: Schema.Number,
59
+ cursor: Schema.Finite,
60
+ });
61
+ const CommandRowSchema = Schema.Struct({
62
+ fingerprint: Schema.NullOr(Schema.String),
63
+ committed_tail: Schema.Finite,
50
64
  });
51
65
  const decodeEventBody = Schema.decodeUnknownSync(Schema.fromJsonString(EventEnvelopeSchema));
52
- const decodeRunState = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown));
66
+ const decodeRunState = Schema.decodeUnknownSync(Schema.fromJsonString(RunStateSchema));
53
67
  const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
54
68
  const decodeStreamRow = Schema.decodeUnknownSync(StreamRowSchema);
55
69
  const decodeEventRow = Schema.decodeUnknownSync(EventRowSchema);
56
70
  const decodeSnapshotRow = Schema.decodeUnknownSync(SnapshotRowSchema);
57
71
  const decodeRunIdRow = Schema.decodeUnknownSync(RunIdRowSchema);
58
72
  const decodeCursorRow = Schema.decodeUnknownSync(CursorRowSchema);
73
+ const decodeCommandRow = Schema.decodeUnknownSync(CommandRowSchema);
59
74
  /** Adapter for Cloudflare Durable Object `ctx.storage.sql`. */
60
75
  export function sqlStorageExec(sql, options) {
61
76
  return {
@@ -100,7 +115,6 @@ function parseEvent(body) {
100
115
  return fromWireEvent(decodeEventBody(body));
101
116
  }
102
117
  function toSnapshot(row) {
103
- // SAFETY: snapshot state is a JSON-encoded RunState written by this store.
104
118
  const state = decodeRunState(row.state);
105
119
  return {
106
120
  runId: row.run_id,
@@ -121,6 +135,7 @@ function createSqliteSnapshotStore(exec) {
121
135
  }),
122
136
  save: (snapshot) => Effect.try({
123
137
  try: () => {
138
+ assertSnapshotByteLimit(snapshot);
124
139
  exec.run(`INSERT INTO looms_snapshots (run_id, cursor, state_hash, taken_at, state)
125
140
  VALUES (?, ?, ?, ?, ?)
126
141
  ON CONFLICT (run_id, cursor) DO UPDATE SET
@@ -134,7 +149,9 @@ function createSqliteSnapshotStore(exec) {
134
149
  encodeJson(snapshot.state),
135
150
  ]);
136
151
  },
137
- catch: (cause) => new SnapshotStoreError(`Failed to save snapshot for ${snapshot.runId}`, cause),
152
+ catch: (cause) => cause instanceof SnapshotPayloadTooLargeError
153
+ ? cause
154
+ : new SnapshotStoreError(`Failed to save snapshot for ${snapshot.runId}`, cause),
138
155
  }),
139
156
  listCursors: (runId) => Effect.try({
140
157
  try: () => exec
@@ -194,11 +211,33 @@ function createSqliteEventStore(exec) {
194
211
  const service = {
195
212
  append: (runId, events, options) => Effect.try({
196
213
  try: () => {
197
- if (events.length === 0 && options?.fence === undefined) {
214
+ assertEventByteLimits(runId, events);
215
+ if (events.length === 0 &&
216
+ options?.fence === undefined &&
217
+ options?.idempotency === undefined) {
198
218
  return { sequences: [], tail: getStream(runId).tail };
199
219
  }
200
220
  return runInTransaction(exec, () => {
201
221
  const log = ensureStream(runId);
222
+ const idempotency = options?.idempotency;
223
+ if (idempotency) {
224
+ const [commandRow] = exec.rows(`SELECT fingerprint, committed_tail
225
+ FROM looms_commands
226
+ WHERE run_id = ? AND idempotency_key = ?`, [runId, idempotency.key]);
227
+ if (commandRow !== undefined) {
228
+ const command = decodeCommandRow(commandRow);
229
+ if (idempotency.fingerprint !== undefined &&
230
+ command.fingerprint !== null &&
231
+ command.fingerprint !== idempotency.fingerprint) {
232
+ throw new EventStoreError(`Idempotency key "${idempotency.key}" was reused with different content`);
233
+ }
234
+ return {
235
+ sequences: [],
236
+ tail: getStream(runId).tail,
237
+ deduplicated: true,
238
+ };
239
+ }
240
+ }
202
241
  if (options?.expectedTail !== undefined && log.tail !== options.expectedTail) {
203
242
  throw new EventStoreConflictError(runId, options.expectedTail, log.tail);
204
243
  }
@@ -231,13 +270,19 @@ function createSqliteEventStore(exec) {
231
270
  ON CONFLICT (run_id) DO UPDATE SET
232
271
  tail = excluded.tail,
233
272
  fence_token = excluded.fence_token`, [runId, log.head, nextTail, fenceToken]);
273
+ if (idempotency) {
274
+ exec.run(`INSERT INTO looms_commands
275
+ (run_id, idempotency_key, fingerprint, committed_tail)
276
+ VALUES (?, ?, ?, ?)`, [runId, idempotency.key, idempotency.fingerprint ?? null, nextTail]);
277
+ }
234
278
  notify(runId, appended);
235
279
  return { sequences, tail: nextTail };
236
280
  });
237
281
  },
238
282
  catch: (cause) => cause instanceof EventStoreConflictError ||
239
283
  cause instanceof EventStoreFencedError ||
240
- cause instanceof EventStoreError
284
+ cause instanceof EventStoreError ||
285
+ cause instanceof EventPayloadTooLargeError
241
286
  ? cause
242
287
  : new EventStoreError(`append failed for ${runId}`, cause),
243
288
  }),
@@ -308,7 +353,7 @@ function createSqliteEventStore(exec) {
308
353
  }
309
354
  }));
310
355
  })),
311
- listRuns: () => Effect.try({
356
+ listRuns: Effect.try({
312
357
  try: () => exec
313
358
  .rows('SELECT run_id FROM looms_streams ORDER BY run_id ASC')
314
359
  .map((row) => decodeRunIdRow(row).run_id),
@@ -1,9 +1,11 @@
1
- import type { RuntimeEffect, WaitCondition } from './effects';
1
+ import { Schema } from 'effect';
2
+ import { type RuntimeEffect, type WaitCondition } from './effects';
2
3
  import type { JsonValue, ThreadStatus, RunStatus } from './types';
3
4
  export interface ThreadRecord {
4
5
  threadId: string;
5
6
  kind: string;
6
7
  definitionName: string;
8
+ definitionVersion: string;
7
9
  parentThreadId: string | null;
8
10
  status: ThreadStatus;
9
11
  input: JsonValue;
@@ -24,20 +26,156 @@ export interface OutstandingEffect {
24
26
  causingEventId: string;
25
27
  effect: RuntimeEffect;
26
28
  }
29
+ export interface EffectExecutionRecord {
30
+ effectId: string;
31
+ attempt: number;
32
+ status: 'queued' | 'dispatched' | 'started' | 'heartbeat' | 'retry_wait' | 'cancel_requested' | 'timed_out' | 'ambiguous' | 'running';
33
+ nextAttemptAt: number | null;
34
+ deadlineAt: number | null;
35
+ /** When the current attempt started executing, locally or on a worker. */
36
+ startedAt: number | null;
37
+ lastHeartbeatAt: number | null;
38
+ lastError: string | null;
39
+ }
40
+ export interface RunStartIdentity {
41
+ kind: string;
42
+ definitionName: string;
43
+ definitionVersion: string;
44
+ input: JsonValue;
45
+ requestedThreadId: string | null;
46
+ idempotencyKey: string | null;
47
+ rootThreadId: string;
48
+ }
27
49
  export interface RunState {
28
50
  runId: string;
29
51
  status: RunStatus;
30
52
  rootThreadId: string | null;
53
+ startIdentity: RunStartIdentity | null;
31
54
  threads: {
32
55
  [threadId: string]: ThreadRecord;
33
56
  };
34
57
  waits: {
35
58
  [waitId: string]: WaitRecord;
36
59
  };
37
- outstandingEffects: OutstandingEffect[];
38
- completedEffectIds?: string[];
39
- processedIdempotencyKeys?: string[];
60
+ outstandingEffects: readonly OutstandingEffect[];
61
+ effectExecutions: {
62
+ [effectId: string]: EffectExecutionRecord;
63
+ };
64
+ completedEffectIds: readonly string[];
65
+ processedIdempotencyKeys: readonly string[];
40
66
  }
67
+ export declare const RunStateSchema: Schema.Struct<{
68
+ readonly runId: Schema.String;
69
+ readonly status: Schema.Union<readonly [Schema.Literal<"running">, Schema.Literal<"completed">, Schema.Literal<"failed">, Schema.Literal<"cancelled">]>;
70
+ readonly rootThreadId: Schema.NullOr<Schema.String>;
71
+ readonly startIdentity: Schema.withDecodingDefaultKey<Schema.NullOr<Schema.Struct<{
72
+ readonly kind: Schema.String;
73
+ readonly definitionName: Schema.String;
74
+ readonly definitionVersion: Schema.String;
75
+ readonly input: Schema.Codec<Schema.Json, Schema.Json, never, never>;
76
+ readonly requestedThreadId: Schema.NullOr<Schema.String>;
77
+ readonly idempotencyKey: Schema.NullOr<Schema.String>;
78
+ readonly rootThreadId: Schema.String;
79
+ }>>, never>;
80
+ readonly threads: Schema.$Record<Schema.String, Schema.Struct<{
81
+ readonly threadId: Schema.String;
82
+ readonly kind: Schema.String;
83
+ readonly definitionName: Schema.String;
84
+ readonly definitionVersion: Schema.withDecodingDefaultKey<Schema.String, never>;
85
+ readonly parentThreadId: Schema.NullOr<Schema.String>;
86
+ readonly status: Schema.Union<readonly [Schema.Literal<"running">, Schema.Literal<"waiting">, Schema.Literal<"completed">, Schema.Literal<"failed">, Schema.Literal<"cancelled">]>;
87
+ readonly input: Schema.Codec<Schema.Json, Schema.Json, never, never>;
88
+ readonly output: Schema.NullOr<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
89
+ readonly error: Schema.NullOr<Schema.String>;
90
+ readonly state: Schema.Codec<Schema.Json, Schema.Json, never, never>;
91
+ }>>;
92
+ readonly waits: Schema.$Record<Schema.String, Schema.Struct<{
93
+ readonly waitId: Schema.String;
94
+ readonly threadId: Schema.String;
95
+ readonly on: Schema.Union<readonly [Schema.Struct<{
96
+ readonly type: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.String>]>;
97
+ readonly match: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
98
+ }>, Schema.Struct<{
99
+ readonly timerAt: Schema.Finite;
100
+ }>]>;
101
+ readonly tag: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
102
+ }>>;
103
+ readonly outstandingEffects: Schema.$Array<Schema.Struct<{
104
+ readonly effectId: Schema.String;
105
+ readonly threadId: Schema.String;
106
+ readonly causingSeq: Schema.Finite;
107
+ readonly causingEventId: Schema.String;
108
+ readonly effect: Schema.Union<readonly [Schema.Struct<{
109
+ readonly type: Schema.Literal<"runtime.spawn">;
110
+ readonly childThreadId: Schema.String;
111
+ readonly kind: Schema.String;
112
+ readonly definitionName: Schema.String;
113
+ readonly definitionVersion: Schema.withDecodingDefaultKey<Schema.String, never>;
114
+ readonly input: Schema.Codec<Schema.Json, Schema.Json, never, never>;
115
+ }>, Schema.Struct<{
116
+ readonly type: Schema.Literal<"runtime.wait">;
117
+ readonly waitId: Schema.String;
118
+ readonly on: Schema.Union<readonly [Schema.Struct<{
119
+ readonly type: Schema.Union<readonly [Schema.String, Schema.$Array<Schema.String>]>;
120
+ readonly match: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
121
+ }>, Schema.Struct<{
122
+ readonly timerAt: Schema.Finite;
123
+ }>]>;
124
+ readonly tag: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
125
+ }>, Schema.Struct<{
126
+ readonly type: Schema.Literal<"runtime.emit">;
127
+ readonly event: Schema.Struct<{
128
+ readonly type: Schema.String;
129
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
130
+ readonly threadId: Schema.optional<Schema.NullOr<Schema.String>>;
131
+ readonly parentThreadId: Schema.optional<Schema.NullOr<Schema.String>>;
132
+ readonly causationId: Schema.optional<Schema.NullOr<Schema.String>>;
133
+ readonly correlationId: Schema.optional<Schema.NullOr<Schema.String>>;
134
+ readonly effectId: Schema.optional<Schema.NullOr<Schema.String>>;
135
+ readonly origin: Schema.optional<Schema.Union<readonly [Schema.Struct<{
136
+ readonly type: Schema.Literal<"thread">;
137
+ readonly threadId: Schema.String;
138
+ }>, Schema.Struct<{
139
+ readonly type: Schema.Literal<"external">;
140
+ readonly actorId: Schema.optional<Schema.String>;
141
+ }>, Schema.Struct<{
142
+ readonly type: Schema.Literal<"system">;
143
+ }>]>>;
144
+ readonly ephemeral: Schema.optional<Schema.Boolean>;
145
+ readonly id: Schema.optional<Schema.String>;
146
+ readonly ts: Schema.optional<Schema.Finite>;
147
+ readonly idempotencyKey: Schema.optional<Schema.NullOr<Schema.String>>;
148
+ }>;
149
+ readonly tag: Schema.optional<Schema.String>;
150
+ }>, Schema.Struct<{
151
+ readonly type: Schema.Literal<"runtime.complete">;
152
+ readonly output: Schema.Codec<Schema.Json, Schema.Json, never, never>;
153
+ }>, Schema.Struct<{
154
+ readonly type: Schema.Literal<"runtime.fail">;
155
+ readonly error: Schema.String;
156
+ }>, Schema.Struct<{
157
+ readonly type: Schema.Literal<"runtime.cancel">;
158
+ readonly threadId: Schema.String;
159
+ readonly tag: Schema.optional<Schema.String>;
160
+ }>, Schema.Struct<{
161
+ readonly type: Schema.String;
162
+ readonly input: Schema.Codec<Schema.Json, Schema.Json, never, never>;
163
+ readonly tag: Schema.optional<Schema.String>;
164
+ }>]>;
165
+ }>>;
166
+ readonly effectExecutions: Schema.withDecodingDefaultKey<Schema.$Record<Schema.String, Schema.Struct<{
167
+ readonly effectId: Schema.String;
168
+ readonly attempt: Schema.Finite;
169
+ readonly status: Schema.Union<readonly [Schema.Literal<"queued">, Schema.Literal<"dispatched">, Schema.Literal<"started">, Schema.Literal<"heartbeat">, Schema.Literal<"retry_wait">, Schema.Literal<"cancel_requested">, Schema.Literal<"timed_out">, Schema.Literal<"ambiguous">, Schema.Literal<"running">]>;
170
+ readonly nextAttemptAt: Schema.NullOr<Schema.Finite>;
171
+ readonly deadlineAt: Schema.withDecodingDefaultKey<Schema.NullOr<Schema.Finite>, never>;
172
+ readonly startedAt: Schema.withDecodingDefaultKey<Schema.NullOr<Schema.Finite>, never>;
173
+ readonly lastHeartbeatAt: Schema.withDecodingDefaultKey<Schema.NullOr<Schema.Finite>, never>;
174
+ readonly lastError: Schema.NullOr<Schema.String>;
175
+ }>>, never>;
176
+ readonly completedEffectIds: Schema.withDecodingDefaultKey<Schema.$Array<Schema.String>, never>;
177
+ readonly processedIdempotencyKeys: Schema.withDecodingDefaultKey<Schema.$Array<Schema.String>, never>;
178
+ }>;
41
179
  export declare function emptyRunState(runId: string): RunState;
42
180
  export declare function isRunTerminal(state: RunState): boolean;
43
181
  export declare function isRunParked(state: RunState): boolean;