@swirls/looms 0.2.0 → 0.3.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 (100) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/actor/host.js +4 -6
  3. package/dist/actor/index.d.ts +1 -1
  4. package/dist/actor/index.js +1 -1
  5. package/dist/actor/routing.d.ts +5 -0
  6. package/dist/actor/routing.js +14 -0
  7. package/dist/agent/agent-session.js +1 -0
  8. package/dist/agent/debugger/agent-chat.js +45 -11
  9. package/dist/agent/debugger/summarize.js +2 -0
  10. package/dist/agent/effects.d.ts +30 -0
  11. package/dist/agent/events.d.ts +2 -0
  12. package/dist/agent/events.js +2 -0
  13. package/dist/agent/llm.js +1 -8
  14. package/dist/agent/module.d.ts +604 -534
  15. package/dist/agent/module.js +11 -7
  16. package/dist/agent/projections.d.ts +5 -0
  17. package/dist/agent/projections.js +43 -2
  18. package/dist/agent/scope.d.ts +2 -0
  19. package/dist/agent/signals.d.ts +5 -0
  20. package/dist/agent/signals.js +7 -0
  21. package/dist/agent/threads.js +35 -21
  22. package/dist/agent/transcript.d.ts +7 -0
  23. package/dist/agent/transcript.js +19 -0
  24. package/dist/agent/types.d.ts +10 -0
  25. package/dist/agent/types.js +4 -0
  26. package/dist/approval/debugger/approvals-view.js +5 -3
  27. package/dist/approval/effects.d.ts +12 -0
  28. package/dist/approval/module.d.ts +24 -0
  29. package/dist/client/client.d.ts +292 -0
  30. package/dist/client/client.js +17 -1
  31. package/dist/cloudflare/router.d.ts +1 -1
  32. package/dist/cloudflare/router.js +6 -8
  33. package/dist/core/effects.d.ts +7 -0
  34. package/dist/core/effects.js +1 -0
  35. package/dist/core/fold.js +12 -6
  36. package/dist/core/fork.d.ts +35 -0
  37. package/dist/core/fork.js +129 -0
  38. package/dist/core/index.d.ts +1 -0
  39. package/dist/core/index.js +1 -0
  40. package/dist/core/module-scope.d.ts +1 -0
  41. package/dist/core/module.d.ts +2 -2
  42. package/dist/core/projection.js +1 -0
  43. package/dist/core/protocol.d.ts +10 -1
  44. package/dist/core/protocol.js +11 -0
  45. package/dist/core/replay.d.ts +8 -0
  46. package/dist/core/snapshots.js +3 -17
  47. package/dist/core/state.d.ts +14 -0
  48. package/dist/core/state.js +25 -2
  49. package/dist/core/store.d.ts +1 -0
  50. package/dist/debugger/app/assets/{index-Qb371t_O.js → index-BevQYa-U.js} +2 -2
  51. package/dist/debugger/app/assets/index-CgDzWJg7.css +2 -0
  52. package/dist/debugger/app/assets/routes-B17Om93i.js +47 -0
  53. package/dist/debugger/app/index.html +2 -2
  54. package/dist/debugger/components/composer.d.ts +5 -2
  55. package/dist/debugger/components/composer.js +5 -4
  56. package/dist/debugger/components/run-tree.js +2 -5
  57. package/dist/debugger/events/catalog.js +1 -3
  58. package/dist/debugger/events/protocol-summarize.js +7 -0
  59. package/dist/debugger/lib/event-counts.d.ts +2 -2
  60. package/dist/debugger/lib/event-counts.js +5 -9
  61. package/dist/debugger/shell/debugger-shell.js +1 -1
  62. package/dist/debugger/shell/run-controls.d.ts +2 -1
  63. package/dist/debugger/shell/run-controls.js +40 -13
  64. package/dist/debugger/shell/run-search.js +1 -3
  65. package/dist/evaluator/effects.d.ts +12 -0
  66. package/dist/evaluator/module.d.ts +24 -0
  67. package/dist/projectors/index-model.d.ts +13 -7
  68. package/dist/projectors/index-model.js +21 -26
  69. package/dist/projectors/memory.js +19 -11
  70. package/dist/projectors/postgres.js +20 -19
  71. package/dist/projectors/schema.js +20 -18
  72. package/dist/projectors/sqlite.js +32 -27
  73. package/dist/react/materialize.js +16 -11
  74. package/dist/react/react.d.ts +6 -2
  75. package/dist/react/react.js +5 -2
  76. package/dist/react/tables.d.ts +3 -2
  77. package/dist/runtime/cancellation.js +14 -27
  78. package/dist/runtime/errors.d.ts +39 -0
  79. package/dist/runtime/errors.js +32 -0
  80. package/dist/runtime/helpers.d.ts +2 -0
  81. package/dist/runtime/helpers.js +23 -1
  82. package/dist/runtime/index.d.ts +1 -1
  83. package/dist/runtime/index.js +1 -1
  84. package/dist/runtime/looms.d.ts +4 -0
  85. package/dist/runtime/looms.js +1 -0
  86. package/dist/runtime/run-start.js +1 -3
  87. package/dist/runtime/runtime.d.ts +1 -1
  88. package/dist/runtime/runtime.js +45 -18
  89. package/dist/runtime/server/http.d.ts +2 -0
  90. package/dist/runtime/server/http.js +17 -2
  91. package/dist/runtime/server/routes/runs.js +35 -2
  92. package/dist/runtime/types.d.ts +8 -0
  93. package/dist/runtime/wake-engine.js +5 -2
  94. package/dist/s2/snapshot-store.js +2 -6
  95. package/dist/workflow/effects.d.ts +24 -0
  96. package/dist/workflow/effects.js +1 -0
  97. package/dist/workflow/module.d.ts +48 -0
  98. package/package.json +1 -1
  99. package/dist/debugger/app/assets/index-B3Ah8WAE.css +0 -2
  100. package/dist/debugger/app/assets/routes-CjtEHgPQ.js +0 -47
@@ -37,6 +37,10 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
37
37
  readonly idempotencyKey: string | null;
38
38
  readonly rootThreadId: string;
39
39
  } | null;
40
+ readonly forkedFrom: {
41
+ readonly runId: string;
42
+ readonly seq: number;
43
+ } | null;
40
44
  readonly threads: {
41
45
  readonly [x: string]: {
42
46
  readonly threadId: string;
@@ -160,6 +164,10 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
160
164
  readonly idempotencyKey: string | null;
161
165
  readonly rootThreadId: string;
162
166
  } | null;
167
+ readonly forkedFrom: {
168
+ readonly runId: string;
169
+ readonly seq: number;
170
+ } | null;
163
171
  readonly threads: {
164
172
  readonly [x: string]: {
165
173
  readonly threadId: string;
@@ -281,6 +289,10 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
281
289
  readonly idempotencyKey: string | null;
282
290
  readonly rootThreadId: string;
283
291
  } | null;
292
+ readonly forkedFrom: {
293
+ readonly runId: string;
294
+ readonly seq: number;
295
+ } | null;
284
296
  readonly threads: {
285
297
  readonly [x: string]: {
286
298
  readonly threadId: string;
@@ -402,6 +414,10 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
402
414
  readonly idempotencyKey: string | null;
403
415
  readonly rootThreadId: string;
404
416
  } | null;
417
+ readonly forkedFrom: {
418
+ readonly runId: string;
419
+ readonly seq: number;
420
+ } | null;
405
421
  readonly threads: {
406
422
  readonly [x: string]: {
407
423
  readonly threadId: string;
@@ -553,6 +569,10 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
553
569
  readonly idempotencyKey: string | null;
554
570
  readonly rootThreadId: string;
555
571
  } | null;
572
+ readonly forkedFrom: {
573
+ readonly runId: string;
574
+ readonly seq: number;
575
+ } | null;
556
576
  readonly threads: {
557
577
  readonly [x: string]: {
558
578
  readonly threadId: string;
@@ -674,6 +694,266 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
674
694
  readonly idempotencyKey: string | null;
675
695
  readonly rootThreadId: string;
676
696
  } | null;
697
+ readonly forkedFrom: {
698
+ readonly runId: string;
699
+ readonly seq: number;
700
+ } | null;
701
+ readonly threads: {
702
+ readonly [x: string]: {
703
+ readonly threadId: string;
704
+ readonly kind: string;
705
+ readonly definitionName: string;
706
+ readonly definitionVersion: string;
707
+ readonly parentThreadId: string | null;
708
+ readonly status: "cancelled" | "completed" | "failed" | "running" | "waiting";
709
+ readonly input: Schema.Json;
710
+ readonly output: Schema.Json;
711
+ readonly error: string | null;
712
+ readonly state: Schema.Json;
713
+ };
714
+ };
715
+ readonly waits: {
716
+ readonly [x: string]: {
717
+ readonly waitId: string;
718
+ readonly threadId: string;
719
+ readonly on: {
720
+ readonly type: string | readonly string[];
721
+ readonly match?: Schema.Json | undefined;
722
+ } | {
723
+ readonly timerAt: number;
724
+ };
725
+ readonly tag?: Schema.Json | undefined;
726
+ };
727
+ };
728
+ readonly outstandingEffects: readonly {
729
+ readonly effectId: string;
730
+ readonly threadId: string;
731
+ readonly causingSeq: number;
732
+ readonly causingEventId: string;
733
+ readonly effect: {
734
+ readonly type: "runtime.spawn";
735
+ readonly childThreadId: string;
736
+ readonly kind: string;
737
+ readonly definitionName: string;
738
+ readonly definitionVersion: string;
739
+ readonly input: Schema.Json;
740
+ } | {
741
+ readonly type: "runtime.wait";
742
+ readonly waitId: string;
743
+ readonly on: {
744
+ readonly type: string | readonly string[];
745
+ readonly match?: Schema.Json | undefined;
746
+ } | {
747
+ readonly timerAt: number;
748
+ };
749
+ readonly tag?: Schema.Json | undefined;
750
+ } | {
751
+ readonly type: "runtime.emit";
752
+ readonly event: {
753
+ readonly type: string;
754
+ readonly payload: Schema.Json;
755
+ readonly threadId?: string | null | undefined;
756
+ readonly parentThreadId?: string | null | undefined;
757
+ readonly causationId?: string | null | undefined;
758
+ readonly correlationId?: string | null | undefined;
759
+ readonly effectId?: string | null | undefined;
760
+ readonly origin?: {
761
+ readonly type: "thread";
762
+ readonly threadId: string;
763
+ } | {
764
+ readonly type: "external";
765
+ readonly actorId?: string | undefined;
766
+ } | {
767
+ readonly type: "system";
768
+ } | undefined;
769
+ readonly ephemeral?: boolean | undefined;
770
+ readonly id?: string | undefined;
771
+ readonly ts?: number | undefined;
772
+ readonly idempotencyKey?: string | null | undefined;
773
+ };
774
+ readonly tag?: string | undefined;
775
+ } | {
776
+ readonly type: "runtime.complete";
777
+ readonly output: Schema.Json;
778
+ } | {
779
+ readonly type: "runtime.fail";
780
+ readonly error: string;
781
+ } | {
782
+ readonly type: "runtime.cancel";
783
+ readonly threadId: string;
784
+ readonly tag?: string | undefined;
785
+ } | {
786
+ readonly type: string;
787
+ readonly input: Schema.Json;
788
+ readonly tag?: string | undefined;
789
+ };
790
+ }[];
791
+ readonly effectExecutions: {
792
+ readonly [x: string]: {
793
+ readonly effectId: string;
794
+ readonly attempt: number;
795
+ readonly status: "ambiguous" | "cancel_requested" | "dispatched" | "heartbeat" | "queued" | "retry_wait" | "running" | "started" | "timed_out";
796
+ readonly nextAttemptAt: number | null;
797
+ readonly deadlineAt: number | null;
798
+ readonly startedAt: number | null;
799
+ readonly lastHeartbeatAt: number | null;
800
+ readonly lastError: string | null;
801
+ };
802
+ };
803
+ readonly completedEffectIds: readonly string[];
804
+ readonly processedIdempotencyKeys: readonly string[];
805
+ };
806
+ readonly appended: number;
807
+ }>;
808
+ /** Cancel a thread and its descendants; omit `threadId` to cancel the whole run. */
809
+ cancel: (runId: string, threadId?: string) => Promise<{
810
+ readonly runId: string;
811
+ readonly state: {
812
+ readonly runId: string;
813
+ readonly status: "cancelled" | "completed" | "failed" | "running";
814
+ readonly rootThreadId: string | null;
815
+ readonly startIdentity: {
816
+ readonly kind: string;
817
+ readonly definitionName: string;
818
+ readonly definitionVersion: string;
819
+ readonly input: Schema.Json;
820
+ readonly requestedThreadId: string | null;
821
+ readonly idempotencyKey: string | null;
822
+ readonly rootThreadId: string;
823
+ } | null;
824
+ readonly forkedFrom: {
825
+ readonly runId: string;
826
+ readonly seq: number;
827
+ } | null;
828
+ readonly threads: {
829
+ readonly [x: string]: {
830
+ readonly threadId: string;
831
+ readonly kind: string;
832
+ readonly definitionName: string;
833
+ readonly definitionVersion: string;
834
+ readonly parentThreadId: string | null;
835
+ readonly status: "cancelled" | "completed" | "failed" | "running" | "waiting";
836
+ readonly input: Schema.Json;
837
+ readonly output: Schema.Json;
838
+ readonly error: string | null;
839
+ readonly state: Schema.Json;
840
+ };
841
+ };
842
+ readonly waits: {
843
+ readonly [x: string]: {
844
+ readonly waitId: string;
845
+ readonly threadId: string;
846
+ readonly on: {
847
+ readonly type: string | readonly string[];
848
+ readonly match?: Schema.Json | undefined;
849
+ } | {
850
+ readonly timerAt: number;
851
+ };
852
+ readonly tag?: Schema.Json | undefined;
853
+ };
854
+ };
855
+ readonly outstandingEffects: readonly {
856
+ readonly effectId: string;
857
+ readonly threadId: string;
858
+ readonly causingSeq: number;
859
+ readonly causingEventId: string;
860
+ readonly effect: {
861
+ readonly type: "runtime.spawn";
862
+ readonly childThreadId: string;
863
+ readonly kind: string;
864
+ readonly definitionName: string;
865
+ readonly definitionVersion: string;
866
+ readonly input: Schema.Json;
867
+ } | {
868
+ readonly type: "runtime.wait";
869
+ readonly waitId: string;
870
+ readonly on: {
871
+ readonly type: string | readonly string[];
872
+ readonly match?: Schema.Json | undefined;
873
+ } | {
874
+ readonly timerAt: number;
875
+ };
876
+ readonly tag?: Schema.Json | undefined;
877
+ } | {
878
+ readonly type: "runtime.emit";
879
+ readonly event: {
880
+ readonly type: string;
881
+ readonly payload: Schema.Json;
882
+ readonly threadId?: string | null | undefined;
883
+ readonly parentThreadId?: string | null | undefined;
884
+ readonly causationId?: string | null | undefined;
885
+ readonly correlationId?: string | null | undefined;
886
+ readonly effectId?: string | null | undefined;
887
+ readonly origin?: {
888
+ readonly type: "thread";
889
+ readonly threadId: string;
890
+ } | {
891
+ readonly type: "external";
892
+ readonly actorId?: string | undefined;
893
+ } | {
894
+ readonly type: "system";
895
+ } | undefined;
896
+ readonly ephemeral?: boolean | undefined;
897
+ readonly id?: string | undefined;
898
+ readonly ts?: number | undefined;
899
+ readonly idempotencyKey?: string | null | undefined;
900
+ };
901
+ readonly tag?: string | undefined;
902
+ } | {
903
+ readonly type: "runtime.complete";
904
+ readonly output: Schema.Json;
905
+ } | {
906
+ readonly type: "runtime.fail";
907
+ readonly error: string;
908
+ } | {
909
+ readonly type: "runtime.cancel";
910
+ readonly threadId: string;
911
+ readonly tag?: string | undefined;
912
+ } | {
913
+ readonly type: string;
914
+ readonly input: Schema.Json;
915
+ readonly tag?: string | undefined;
916
+ };
917
+ }[];
918
+ readonly effectExecutions: {
919
+ readonly [x: string]: {
920
+ readonly effectId: string;
921
+ readonly attempt: number;
922
+ readonly status: "ambiguous" | "cancel_requested" | "dispatched" | "heartbeat" | "queued" | "retry_wait" | "running" | "started" | "timed_out";
923
+ readonly nextAttemptAt: number | null;
924
+ readonly deadlineAt: number | null;
925
+ readonly startedAt: number | null;
926
+ readonly lastHeartbeatAt: number | null;
927
+ readonly lastError: string | null;
928
+ };
929
+ };
930
+ readonly completedEffectIds: readonly string[];
931
+ readonly processedIdempotencyKeys: readonly string[];
932
+ };
933
+ }>;
934
+ /** Start a new run that continues where a finished run ended. */
935
+ fork: (sourceRunId: string, forkOptions?: {
936
+ runId?: string;
937
+ }) => Promise<{
938
+ readonly runId: string;
939
+ readonly threadId: string;
940
+ readonly state: {
941
+ readonly runId: string;
942
+ readonly status: "cancelled" | "completed" | "failed" | "running";
943
+ readonly rootThreadId: string | null;
944
+ readonly startIdentity: {
945
+ readonly kind: string;
946
+ readonly definitionName: string;
947
+ readonly definitionVersion: string;
948
+ readonly input: Schema.Json;
949
+ readonly requestedThreadId: string | null;
950
+ readonly idempotencyKey: string | null;
951
+ readonly rootThreadId: string;
952
+ } | null;
953
+ readonly forkedFrom: {
954
+ readonly runId: string;
955
+ readonly seq: number;
956
+ } | null;
677
957
  readonly threads: {
678
958
  readonly [x: string]: {
679
959
  readonly threadId: string;
@@ -795,6 +1075,10 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
795
1075
  readonly idempotencyKey: string | null;
796
1076
  readonly rootThreadId: string;
797
1077
  } | null;
1078
+ readonly forkedFrom: {
1079
+ readonly runId: string;
1080
+ readonly seq: number;
1081
+ } | null;
798
1082
  readonly threads: {
799
1083
  readonly [x: string]: {
800
1084
  readonly threadId: string;
@@ -960,6 +1244,10 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
960
1244
  readonly idempotencyKey: string | null;
961
1245
  readonly rootThreadId: string;
962
1246
  } | null;
1247
+ readonly forkedFrom: {
1248
+ readonly runId: string;
1249
+ readonly seq: number;
1250
+ } | null;
963
1251
  readonly threads: {
964
1252
  readonly [x: string]: {
965
1253
  readonly threadId: string;
@@ -1078,6 +1366,10 @@ export declare function createLoomsClient(options?: LoomsClientOptions): {
1078
1366
  readonly idempotencyKey: string | null;
1079
1367
  readonly rootThreadId: string;
1080
1368
  } | null;
1369
+ readonly forkedFrom: {
1370
+ readonly runId: string;
1371
+ readonly seq: number;
1372
+ } | null;
1081
1373
  readonly threads: {
1082
1374
  readonly [x: string]: {
1083
1375
  readonly threadId: string;
@@ -14,6 +14,12 @@ const StartResultSchema = Schema.Struct({
14
14
  state: RunStateSchema,
15
15
  });
16
16
  const RunResultSchema = Schema.Struct({ runId: Schema.String, state: RunStateSchema });
17
+ /** `appended` counts the events the run gained while the wake ran. */
18
+ const WakeResultSchema = Schema.Struct({
19
+ runId: Schema.String,
20
+ state: RunStateSchema,
21
+ appended: Schema.Finite,
22
+ });
17
23
  const EventsResultSchema = Schema.Struct({
18
24
  runId: Schema.String,
19
25
  events: Schema.Array(EventEnvelopeSchema),
@@ -157,7 +163,17 @@ export function createLoomsClient(options = {}) {
157
163
  method: 'POST',
158
164
  body: stringifyJson({ events }),
159
165
  }),
160
- wake: (runId) => request(`/runs/${encodeURIComponent(runId)}/wake`, RunResultSchema, { method: 'POST' }),
166
+ wake: (runId) => request(`/runs/${encodeURIComponent(runId)}/wake`, WakeResultSchema, { method: 'POST' }),
167
+ /** Cancel a thread and its descendants; omit `threadId` to cancel the whole run. */
168
+ cancel: (runId, threadId) => request(`/runs/${encodeURIComponent(runId)}/cancel`, RunResultSchema, {
169
+ method: 'POST',
170
+ body: stringifyJson(threadId === undefined ? {} : { threadId }),
171
+ }),
172
+ /** Start a new run that continues where a finished run ended. */
173
+ fork: (sourceRunId, forkOptions) => request(`/runs/${encodeURIComponent(sourceRunId)}/fork`, StartResultSchema, {
174
+ method: 'POST',
175
+ body: stringifyJson(forkOptions?.runId === undefined ? {} : { runId: forkOptions.runId }),
176
+ }),
161
177
  workerCallback,
162
178
  listRunSummaries: () => request('/runs/summaries', RunSummariesSchema),
163
179
  listDefinitions: () => request('/definitions', DefinitionCatalogSchema),
@@ -6,7 +6,7 @@ export interface DurableObjectNamespaceLike<T extends DurableObjectStubLike = Du
6
6
  }
7
7
  /**
8
8
  * Routes incoming HTTP requests to the appropriate Durable Object actor cell.
9
- * Handles `/health` locally, returns 501 for global run lists, and forwards
9
+ * Handles `/health` locally, returns 501 for global run lists and forks, and forwards
10
10
  * all other run and event-stream requests to `namespace.getByName(runId)`.
11
11
  */
12
12
  export declare function routeToDurableObject(namespace: DurableObjectNamespaceLike, req: Request): Promise<Response | null>;
@@ -1,18 +1,16 @@
1
- import { resolveRunTarget, RunTargetError } from '@swirls/looms/actor';
1
+ import { crossRunResponse, resolveRunTarget, RunTargetError } from '@swirls/looms/actor';
2
2
  /**
3
3
  * Routes incoming HTTP requests to the appropriate Durable Object actor cell.
4
- * Handles `/health` locally, returns 501 for global run lists, and forwards
4
+ * Handles `/health` locally, returns 501 for global run lists and forks, and forwards
5
5
  * all other run and event-stream requests to `namespace.getByName(runId)`.
6
6
  */
7
7
  export function routeToDurableObject(namespace, req) {
8
- const url = new URL(req.url);
9
- if (url.pathname === '/health') {
8
+ if (new URL(req.url).pathname === '/health') {
10
9
  return Promise.resolve(Response.json({ ok: true }));
11
10
  }
12
- if (req.method === 'GET' && (url.pathname === '/runs' || url.pathname === '/runs/summaries')) {
13
- return Promise.resolve(Response.json({
14
- error: 'Global run listing is not supported in the Durable Object backend without a global index',
15
- }, { status: 501 }));
11
+ const unsupported = crossRunResponse(req, 'the Durable Object backend');
12
+ if (unsupported) {
13
+ return Promise.resolve(unsupported);
16
14
  }
17
15
  return resolveRunTarget(req)
18
16
  .then(({ runId, request }) => namespace.getByName(runId).fetch(request))
@@ -166,6 +166,11 @@ export interface EffectDefinition<TInput = JsonValue, R = Context.Service.Any, E
166
166
  readonly execution?: EffectExecution;
167
167
  /** Whether `execute` runs a user handler. Without one, the effect can only complete through a worker. */
168
168
  readonly hasHandler: boolean;
169
+ /**
170
+ * The outcome follows from the input alone, like scheduling from state already folded. Forking
171
+ * a failed run retries the effect that produced the failure, not a deterministic one passing it on.
172
+ */
173
+ readonly deterministic?: boolean;
169
174
  readonly [effectInputType]?: (input: TInput) => TInput;
170
175
  execute(input: JsonValue, ctx: EffectContext): Effect.Effect<ReadonlyArray<E>, InvalidInputError | EffectHandlerError, R>;
171
176
  }
@@ -182,6 +187,7 @@ export declare function defineEffect<TSchema extends SchemaInput, R = never, E e
182
187
  input: TSchema;
183
188
  retry?: RetryPolicy;
184
189
  execution?: EffectExecution;
190
+ deterministic?: boolean;
185
191
  execute?: (input: InferDefinedSchema<TSchema>, ctx: EffectContext) => EffectHandlerResult<R, E, HandlerError>;
186
192
  }): EffectDefinition<InferDefinedSchema<TSchema>, R, E>;
187
193
  export declare function defineEffect<R = never, E extends EventInput = EventInput, HandlerError = never>(def: {
@@ -189,6 +195,7 @@ export declare function defineEffect<R = never, E extends EventInput = EventInpu
189
195
  input?: undefined;
190
196
  retry?: RetryPolicy;
191
197
  execution?: EffectExecution;
198
+ deterministic?: boolean;
192
199
  execute?: (input: JsonValue, ctx: EffectContext) => EffectHandlerResult<R, E, HandlerError>;
193
200
  }): EffectDefinition<JsonValue, R, E>;
194
201
  export declare function spawn(args: {
@@ -106,6 +106,7 @@ export function defineEffect(def) {
106
106
  retry: def.retry,
107
107
  execution: def.execution,
108
108
  hasHandler: handler !== undefined,
109
+ deterministic: def.deterministic,
109
110
  execute: (raw, ctx) => Effect.gen(function* () {
110
111
  const input = schema ? yield* validateInputEffect(schema, raw) : raw;
111
112
  return handler ? yield* liftHandlerResult(handler(input, ctx)) : noOutput;
package/dist/core/fold.js CHANGED
@@ -2,7 +2,7 @@ import { Option, Predicate, Schema, Stream } from 'effect';
2
2
  import { isWithdrawnError, WaitConditionSchema } from './effects.js';
3
3
  import { createEffectId } from './ids.js';
4
4
  import { DEFAULT_DEFINITION_VERSION } from './module.js';
5
- import { emptyRunState } from './state.js';
5
+ import { completedRunStatus, emptyRunState } from './state.js';
6
6
  import { isTerminalStatus } from './thread.js';
7
7
  const decodeWaitCondition = Schema.decodeUnknownOption(WaitConditionSchema);
8
8
  function payloadObject(event) {
@@ -25,6 +25,7 @@ function cloneState(state) {
25
25
  status: state.status,
26
26
  rootThreadId: state.rootThreadId,
27
27
  startIdentity: state.startIdentity,
28
+ forkedFrom: state.forkedFrom,
28
29
  threads: { ...state.threads },
29
30
  waits: { ...state.waits },
30
31
  outstandingEffects: [...state.outstandingEffects],
@@ -104,13 +105,18 @@ function applyProtocol(state, event, registry) {
104
105
  status: 'running',
105
106
  };
106
107
  }
107
- case 'runtime.run.completed': {
108
- const error = payload.error;
109
- const status = Predicate.isString(error) && error.length > 0 ? 'failed' : 'completed';
110
- return { ...state, status };
111
- }
108
+ case 'runtime.run.completed':
109
+ return { ...state, status: completedRunStatus(payload) };
112
110
  case 'runtime.run.cancelled':
113
111
  return { ...state, status: 'cancelled' };
112
+ case 'runtime.run.forked': {
113
+ const sourceRunId = readString(payload, 'sourceRunId');
114
+ const sourceSeq = payload.sourceSeq;
115
+ // Attempts recorded in the copied history belong to the source run's processes.
116
+ return sourceRunId && Predicate.isNumber(sourceSeq)
117
+ ? { ...state, forkedFrom: { runId: sourceRunId, seq: sourceSeq }, effectExecutions: {} }
118
+ : state;
119
+ }
114
120
  case 'runtime.thread.started': {
115
121
  const threadId = readString(payload, 'threadId') ?? event.threadId;
116
122
  const kind = readString(payload, 'kind');
@@ -0,0 +1,35 @@
1
+ import { Effect } from 'effect';
2
+ import { type EffectDefinition } from './effects';
3
+ import { type EventEnvelope } from './envelope';
4
+ import { type FoldRegistry } from './fold';
5
+ declare const ForkSourceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
6
+ readonly _tag: "ForkSourceError";
7
+ } & Readonly<A>;
8
+ export declare class ForkSourceError extends ForkSourceError_base<{
9
+ readonly runId: string;
10
+ readonly message: string;
11
+ }> {
12
+ constructor(runId: string, message: string);
13
+ }
14
+ export interface ForkRegistry extends FoldRegistry {
15
+ readonly effects: ReadonlyMap<string, Pick<EffectDefinition, 'deterministic'>>;
16
+ }
17
+ export interface ForkPlan {
18
+ /** The fork's history: the copied prefix, keeping source seqs, then the `runtime.run.forked` marker. */
19
+ readonly events: readonly EventEnvelope[];
20
+ /** Last source seq the fork copied. */
21
+ readonly sourceSeq: number;
22
+ readonly rootThreadId: string;
23
+ }
24
+ /**
25
+ * Plan a new run that continues where the source run ended. The fork copies the source's
26
+ * history up to the point the run finished, keeping sequence numbers so effect ids still
27
+ * line up, and its next wake re-issues whatever effects were outstanding there.
28
+ */
29
+ export declare function planFork(input: {
30
+ readonly sourceRunId: string;
31
+ readonly runId: string;
32
+ readonly events: readonly EventEnvelope[];
33
+ readonly registry: ForkRegistry;
34
+ }): Effect.Effect<ForkPlan, ForkSourceError>;
35
+ export {};
@@ -0,0 +1,129 @@
1
+ import { Data, Effect, Predicate } from 'effect';
2
+ import { isPrimitiveEffect, isWithdrawnError, } from './effects.js';
3
+ import { createEvent } from './envelope.js';
4
+ import { foldEvent, foldRun } from './fold.js';
5
+ import { emptyRunState, isRunTerminal } from './state.js';
6
+ import { isTerminalStatus } from './thread.js';
7
+ export class ForkSourceError extends Data.TaggedError('ForkSourceError') {
8
+ constructor(runId, message) {
9
+ super({ runId, message });
10
+ this.name = 'ForkSourceError';
11
+ }
12
+ }
13
+ function payloadThreadId(event) {
14
+ const threadId = Predicate.isObject(event.payload) ? event.payload.threadId : undefined;
15
+ return Predicate.isString(threadId) ? threadId : event.threadId;
16
+ }
17
+ function rootTerminal(state) {
18
+ const root = state.rootThreadId ? state.threads[state.rootThreadId] : undefined;
19
+ return isRunTerminal(state) || (root !== undefined && isTerminalStatus(root.status));
20
+ }
21
+ /** Where the cancellation that wrote `event` began, when it recorded that; a seq is an index + 1. */
22
+ function cancellationStart(event) {
23
+ const resumeSeq = event.type === 'runtime.thread.cancelled' && Predicate.isObject(event.payload)
24
+ ? event.payload.resumeSeq
25
+ : undefined;
26
+ return Predicate.isNumber(resumeSeq) ? resumeSeq : undefined;
27
+ }
28
+ /**
29
+ * Fold the source until its root thread ends. Everything from that event on is how the run
30
+ * finished, so the fork leaves it out; a cancellation that recorded where it began is left
31
+ * out from there.
32
+ */
33
+ function scanSource(events, registry) {
34
+ const effects = new Map();
35
+ const failedAt = new Map();
36
+ let state = emptyRunState(events[0]?.runId ?? '');
37
+ for (const [index, event] of events.entries()) {
38
+ state = foldEvent(state, event, registry);
39
+ for (const item of state.outstandingEffects) {
40
+ effects.set(item.effectId, item.effect);
41
+ }
42
+ for (const record of Object.values(state.threads)) {
43
+ if (record.status === 'failed' && !failedAt.has(record.threadId)) {
44
+ failedAt.set(record.threadId, index);
45
+ }
46
+ }
47
+ if (rootTerminal(state)) {
48
+ const root = state.rootThreadId ? state.threads[state.rootThreadId] : undefined;
49
+ const rootFailed = root?.status === 'failed' || state.status === 'failed';
50
+ return { cut: cancellationStart(event) ?? index, effects, failedAt, rootFailed };
51
+ }
52
+ }
53
+ return { cut: events.length, effects, failedAt, rootFailed: false };
54
+ }
55
+ /**
56
+ * Walk back from the root's failure to the effect outcome that started it, so the fork
57
+ * re-issues that effect instead of replaying a failure the modules already folded in.
58
+ */
59
+ function failureOrigin(events, scan, registry) {
60
+ const indexById = new Map(events.map((event, index) => [event.id, index]));
61
+ /** A failed thread leads back to where the fold failed it, anything else to its cause. */
62
+ const previous = (index) => {
63
+ const event = events[index];
64
+ const threadId = event.type === 'runtime.thread.failed' ? payloadThreadId(event) : null;
65
+ const failedAt = threadId ? scan.failedAt.get(threadId) : undefined;
66
+ const cause = event.causationId ? indexById.get(event.causationId) : undefined;
67
+ const next = failedAt !== undefined && failedAt < index ? failedAt : cause;
68
+ return next !== undefined && next < index ? next : undefined;
69
+ };
70
+ for (let index = scan.cut; index !== undefined; index = previous(index)) {
71
+ const event = events[index];
72
+ const effect = event.effectId ? scan.effects.get(event.effectId) : undefined;
73
+ const error = Predicate.isObject(event.payload) ? event.payload.error : undefined;
74
+ const withdrawn = Predicate.isString(error) && isWithdrawnError(error);
75
+ if (effect &&
76
+ !isPrimitiveEffect(effect) &&
77
+ !registry.effects.get(effect.type)?.deterministic &&
78
+ !withdrawn) {
79
+ return index;
80
+ }
81
+ }
82
+ return scan.cut;
83
+ }
84
+ function copyEvent(event, runId, reissued) {
85
+ if (event.ephemeral && event.effectId && reissued.has(event.effectId)) {
86
+ return { ...event, runId, type: 'runtime.fork.elided', payload: { type: event.type } };
87
+ }
88
+ if (event.type === 'runtime.snapshot.taken' && Predicate.isObject(event.payload)) {
89
+ const { state: _state, ...pointer } = event.payload;
90
+ return { ...event, runId, payload: pointer };
91
+ }
92
+ if (event.type === 'runtime.run.started') {
93
+ return { ...event, runId, idempotencyKey: null };
94
+ }
95
+ return { ...event, runId };
96
+ }
97
+ /**
98
+ * Plan a new run that continues where the source run ended. The fork copies the source's
99
+ * history up to the point the run finished, keeping sequence numbers so effect ids still
100
+ * line up, and its next wake re-issues whatever effects were outstanding there.
101
+ */
102
+ export function planFork(input) {
103
+ const { sourceRunId, runId, events, registry } = input;
104
+ if (events.length === 0) {
105
+ return Effect.fail(new ForkSourceError(sourceRunId, `Run "${sourceRunId}" has no events`));
106
+ }
107
+ if (events.some((event, index) => event.seq !== index + 1)) {
108
+ return Effect.fail(new ForkSourceError(sourceRunId, `Run "${sourceRunId}" does not have its full history from seq 1; it cannot be forked`));
109
+ }
110
+ const scan = scanSource(events, registry);
111
+ const cut = scan.rootFailed ? failureOrigin(events, scan, registry) : scan.cut;
112
+ const prefix = events.slice(0, cut);
113
+ const atCut = foldRun(prefix, registry, { runId });
114
+ if (!atCut.rootThreadId) {
115
+ return Effect.fail(new ForkSourceError(sourceRunId, `Run "${sourceRunId}" never started a root thread`));
116
+ }
117
+ const reissued = new Set(atCut.outstandingEffects.map((item) => item.effectId));
118
+ const marker = createEvent(runId, {
119
+ type: 'runtime.run.forked',
120
+ payload: { sourceRunId, sourceSeq: cut },
121
+ threadId: null,
122
+ origin: { type: 'system' },
123
+ }, { seq: cut + 1 });
124
+ return Effect.succeed({
125
+ events: [...prefix.map((event) => copyEvent(event, runId, reissued)), marker],
126
+ sourceSeq: cut,
127
+ rootThreadId: atCut.rootThreadId,
128
+ });
129
+ }
@@ -13,6 +13,7 @@ export * from './state';
13
13
  export * from './match';
14
14
  export * from './fold';
15
15
  export * from './replay';
16
+ export * from './fork';
16
17
  export * from './projection';
17
18
  export * from './module';
18
19
  export * from './kind';