@swirls/looms 0.2.0 → 0.4.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 (113) hide show
  1. package/CHANGELOG.md +31 -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 -38
  9. package/dist/agent/debugger/streaming-text.d.ts +6 -0
  10. package/dist/agent/debugger/streaming-text.js +33 -0
  11. package/dist/agent/debugger/summarize.js +2 -0
  12. package/dist/agent/effects.d.ts +30 -0
  13. package/dist/agent/events.d.ts +2 -0
  14. package/dist/agent/events.js +2 -0
  15. package/dist/agent/llm.js +1 -8
  16. package/dist/agent/module.d.ts +230 -160
  17. package/dist/agent/module.js +11 -7
  18. package/dist/agent/projections.d.ts +5 -0
  19. package/dist/agent/projections.js +43 -2
  20. package/dist/agent/scope.d.ts +2 -0
  21. package/dist/agent/signals.d.ts +5 -0
  22. package/dist/agent/signals.js +7 -0
  23. package/dist/agent/threads.js +35 -21
  24. package/dist/agent/transcript.d.ts +7 -0
  25. package/dist/agent/transcript.js +19 -0
  26. package/dist/agent/types.d.ts +10 -0
  27. package/dist/agent/types.js +4 -0
  28. package/dist/approval/debugger/approvals-view.js +5 -3
  29. package/dist/approval/effects.d.ts +12 -0
  30. package/dist/approval/module.d.ts +24 -0
  31. package/dist/client/client.d.ts +292 -0
  32. package/dist/client/client.js +17 -1
  33. package/dist/cloudflare/router.d.ts +1 -1
  34. package/dist/cloudflare/router.js +6 -8
  35. package/dist/core/attempts.d.ts +8 -0
  36. package/dist/core/attempts.js +27 -0
  37. package/dist/core/effects.d.ts +7 -0
  38. package/dist/core/effects.js +1 -0
  39. package/dist/core/fold.js +12 -6
  40. package/dist/core/fork.d.ts +35 -0
  41. package/dist/core/fork.js +129 -0
  42. package/dist/core/index.d.ts +3 -0
  43. package/dist/core/index.js +2 -0
  44. package/dist/core/module-scope.d.ts +1 -0
  45. package/dist/core/module.d.ts +2 -2
  46. package/dist/core/projection.d.ts +5 -4
  47. package/dist/core/projection.js +1 -0
  48. package/dist/core/protocol.d.ts +10 -1
  49. package/dist/core/protocol.js +11 -0
  50. package/dist/{react → core}/register.d.ts +9 -8
  51. package/dist/core/replay.d.ts +8 -0
  52. package/dist/core/snapshots.js +3 -17
  53. package/dist/core/state.d.ts +14 -0
  54. package/dist/core/state.js +25 -2
  55. package/dist/core/store.d.ts +1 -0
  56. package/dist/debugger/app/assets/index-CgDzWJg7.css +2 -0
  57. package/dist/debugger/app/assets/index-D--9esm9.js +22 -0
  58. package/dist/debugger/app/assets/routes-Dakrdusf.js +47 -0
  59. package/dist/debugger/app/index.html +2 -2
  60. package/dist/debugger/components/composer.d.ts +5 -2
  61. package/dist/debugger/components/composer.js +5 -4
  62. package/dist/debugger/components/run-tree.js +2 -5
  63. package/dist/debugger/events/catalog.js +1 -3
  64. package/dist/debugger/events/protocol-summarize.js +7 -0
  65. package/dist/debugger/lib/event-counts.d.ts +2 -2
  66. package/dist/debugger/lib/event-counts.js +5 -9
  67. package/dist/debugger/shell/debugger-shell.js +1 -1
  68. package/dist/debugger/shell/run-controls.d.ts +2 -1
  69. package/dist/debugger/shell/run-controls.js +40 -13
  70. package/dist/debugger/shell/run-search.js +1 -3
  71. package/dist/evaluator/effects.d.ts +45 -33
  72. package/dist/evaluator/module.d.ts +90 -66
  73. package/dist/projectors/index-model.d.ts +13 -7
  74. package/dist/projectors/index-model.js +21 -26
  75. package/dist/projectors/memory.js +19 -11
  76. package/dist/projectors/postgres.js +20 -19
  77. package/dist/projectors/schema.js +20 -18
  78. package/dist/projectors/sqlite.js +32 -27
  79. package/dist/react/derived.d.ts +26 -8
  80. package/dist/react/derived.js +23 -26
  81. package/dist/react/index.d.ts +3 -3
  82. package/dist/react/index.js +2 -2
  83. package/dist/react/materialize.js +16 -11
  84. package/dist/react/react.d.ts +12 -9
  85. package/dist/react/react.js +9 -6
  86. package/dist/react/store.d.ts +4 -5
  87. package/dist/react/tables.d.ts +3 -2
  88. package/dist/runtime/cancellation.js +14 -27
  89. package/dist/runtime/errors.d.ts +39 -0
  90. package/dist/runtime/errors.js +32 -0
  91. package/dist/runtime/helpers.d.ts +2 -0
  92. package/dist/runtime/helpers.js +23 -1
  93. package/dist/runtime/index.d.ts +1 -1
  94. package/dist/runtime/index.js +1 -1
  95. package/dist/runtime/looms.d.ts +4 -0
  96. package/dist/runtime/looms.js +1 -0
  97. package/dist/runtime/run-start.js +1 -3
  98. package/dist/runtime/runtime.d.ts +1 -1
  99. package/dist/runtime/runtime.js +45 -18
  100. package/dist/runtime/server/http.d.ts +2 -0
  101. package/dist/runtime/server/http.js +17 -2
  102. package/dist/runtime/server/routes/runs.js +35 -2
  103. package/dist/runtime/types.d.ts +8 -0
  104. package/dist/runtime/wake-engine.js +5 -2
  105. package/dist/s2/snapshot-store.js +2 -6
  106. package/dist/workflow/effects.d.ts +24 -0
  107. package/dist/workflow/effects.js +1 -0
  108. package/dist/workflow/module.d.ts +48 -0
  109. package/package.json +2 -2
  110. package/dist/debugger/app/assets/index-B3Ah8WAE.css +0 -2
  111. package/dist/debugger/app/assets/index-Qb371t_O.js +0 -22
  112. package/dist/debugger/app/assets/routes-CjtEHgPQ.js +0 -47
  113. /package/dist/{react → core}/register.js +0 -0
@@ -1,6 +1,8 @@
1
- import type { EventEnvelope } from '@swirls/looms/core';
1
+ import { type EventEnvelope } from '@swirls/looms/core';
2
2
  import type { Projector } from './projector';
3
+ /** A run (`actorId === runId`) or one of its threads. Thread ids are unique within a run. */
3
4
  export interface ActorIndexRow {
5
+ runId: string;
4
6
  actorId: string;
5
7
  kind: string | null;
6
8
  status: string | null;
@@ -9,14 +11,15 @@ export interface ActorIndexRow {
9
11
  updatedAt: number;
10
12
  }
11
13
  export interface ReviewIndexRow {
14
+ runId: string;
12
15
  reviewId: string;
13
- actorId: string;
14
16
  status: string;
15
17
  title: string;
16
18
  updatedAt: number;
17
19
  }
18
20
  export type IndexOp = {
19
21
  type: 'upsertActor';
22
+ runId: string;
20
23
  actorId: string;
21
24
  kind: string | null;
22
25
  status: string | null;
@@ -25,32 +28,35 @@ export type IndexOp = {
25
28
  updatedAt: number;
26
29
  } | {
27
30
  type: 'setActorStatus';
31
+ runId: string;
28
32
  actorId: string;
29
33
  status: string;
30
34
  updatedAt: number;
31
35
  } | {
32
36
  type: 'upsertReview';
37
+ runId: string;
33
38
  reviewId: string;
34
- actorId: string;
35
39
  status: string;
36
40
  title: string;
37
41
  updatedAt: number;
38
42
  } | {
39
43
  type: 'setReviewStatus';
44
+ runId: string;
40
45
  reviewId: string;
41
46
  status: string;
42
47
  updatedAt: number;
43
48
  };
44
49
  export interface IndexBackend {
45
50
  applyOps: (ops: ReadonlyArray<IndexOp>) => Promise<void>;
46
- getActor: (actorId: string) => Promise<ActorIndexRow | null>;
47
- listReviews: (actorId?: string) => Promise<ReviewIndexRow[]>;
51
+ getActor: (runId: string, actorId: string) => Promise<ActorIndexRow | null>;
52
+ listReviews: (runId?: string) => Promise<ReviewIndexRow[]>;
48
53
  init?: () => Promise<void>;
49
54
  dispose?: () => Promise<void>;
50
55
  }
51
56
  export interface IndexProjector extends Projector {
52
- getActor: (actorId: string) => Promise<ActorIndexRow | null>;
53
- listReviews: (actorId?: string) => Promise<ReviewIndexRow[]>;
57
+ /** The run's row, or one of its threads when `actorId` is given. */
58
+ getActor: (runId: string, actorId?: string) => Promise<ActorIndexRow | null>;
59
+ listReviews: (runId?: string) => Promise<ReviewIndexRow[]>;
54
60
  }
55
61
  export declare function indexOpsFor(event: EventEnvelope): IndexOp[];
56
62
  export declare function createIndexProjector(name: string, backend: IndexBackend, version?: string): IndexProjector;
@@ -1,4 +1,5 @@
1
1
  import { Predicate } from 'effect';
2
+ import { completedRunStatus } from '@swirls/looms/core';
2
3
  function payloadObject(event) {
3
4
  if (!Predicate.isObject(event.payload)) {
4
5
  return {};
@@ -11,12 +12,15 @@ function readString(obj, key) {
11
12
  }
12
13
  export function indexOpsFor(event) {
13
14
  const payload = payloadObject(event);
15
+ const runId = event.runId;
16
+ const actorId = readString(payload, 'threadId') ?? event.threadId ?? runId;
14
17
  switch (event.type) {
15
18
  case 'runtime.run.started':
16
19
  return [
17
20
  {
18
21
  type: 'upsertActor',
19
- actorId: event.runId,
22
+ runId,
23
+ actorId: runId,
20
24
  kind: readString(payload, 'kind') ?? null,
21
25
  status: 'running',
22
26
  definitionName: readString(payload, 'definitionName') ?? null,
@@ -28,7 +32,8 @@ export function indexOpsFor(event) {
28
32
  return [
29
33
  {
30
34
  type: 'upsertActor',
31
- actorId: readString(payload, 'threadId') ?? event.threadId ?? event.runId,
35
+ runId,
36
+ actorId,
32
37
  kind: readString(payload, 'kind') ?? null,
33
38
  status: 'running',
34
39
  definitionName: readString(payload, 'definitionName') ?? null,
@@ -37,39 +42,28 @@ export function indexOpsFor(event) {
37
42
  },
38
43
  ];
39
44
  case 'runtime.run.completed':
40
- case 'runtime.thread.completed':
41
45
  return [
42
46
  {
43
47
  type: 'setActorStatus',
44
- actorId: readString(payload, 'threadId') ?? event.threadId ?? event.runId,
45
- status: 'completed',
48
+ runId,
49
+ actorId: runId,
50
+ status: completedRunStatus(event.payload),
46
51
  updatedAt: event.ts,
47
52
  },
48
53
  ];
54
+ case 'runtime.thread.completed':
55
+ return [{ type: 'setActorStatus', runId, actorId, status: 'completed', updatedAt: event.ts }];
49
56
  case 'runtime.thread.failed':
50
- return [
51
- {
52
- type: 'setActorStatus',
53
- actorId: readString(payload, 'threadId') ?? event.threadId ?? event.runId,
54
- status: 'failed',
55
- updatedAt: event.ts,
56
- },
57
- ];
57
+ return [{ type: 'setActorStatus', runId, actorId, status: 'failed', updatedAt: event.ts }];
58
+ case 'runtime.run.cancelled':
58
59
  case 'runtime.thread.cancelled':
59
- return [
60
- {
61
- type: 'setActorStatus',
62
- actorId: readString(payload, 'threadId') ?? event.threadId ?? event.runId,
63
- status: 'cancelled',
64
- updatedAt: event.ts,
65
- },
66
- ];
60
+ return [{ type: 'setActorStatus', runId, actorId, status: 'cancelled', updatedAt: event.ts }];
67
61
  case 'approval.requested':
68
62
  return [
69
63
  {
70
64
  type: 'upsertReview',
65
+ runId,
71
66
  reviewId: readString(payload, 'approvalId') ?? 'unknown',
72
- actorId: event.runId,
73
67
  status: 'pending',
74
68
  title: readString(payload, 'title') ?? 'Approval',
75
69
  updatedAt: event.ts,
@@ -79,8 +73,8 @@ export function indexOpsFor(event) {
79
73
  return [
80
74
  {
81
75
  type: 'upsertReview',
76
+ runId,
82
77
  reviewId: readString(payload, 'approvalId') ?? 'unknown',
83
- actorId: event.runId,
84
78
  status: payload.outcome === 'reject' ? 'rejected' : 'approved',
85
79
  title: 'Approval',
86
80
  updatedAt: event.ts,
@@ -90,6 +84,7 @@ export function indexOpsFor(event) {
90
84
  return [
91
85
  {
92
86
  type: 'setReviewStatus',
87
+ runId,
93
88
  reviewId: readString(payload, 'approvalId') ?? 'unknown',
94
89
  status: 'timed_out',
95
90
  updatedAt: event.ts,
@@ -99,7 +94,7 @@ export function indexOpsFor(event) {
99
94
  return [];
100
95
  }
101
96
  }
102
- export function createIndexProjector(name, backend, version = '1') {
97
+ export function createIndexProjector(name, backend, version = '2') {
103
98
  return {
104
99
  name,
105
100
  version,
@@ -109,7 +104,7 @@ export function createIndexProjector(name, backend, version = '1') {
109
104
  const ops = events.flatMap(indexOpsFor);
110
105
  return ops.length === 0 ? Promise.resolve() : backend.applyOps(ops);
111
106
  },
112
- getActor: (actorId) => backend.getActor(actorId),
113
- listReviews: (actorId) => backend.listReviews(actorId),
107
+ getActor: (runId, actorId) => backend.getActor(runId, actorId ?? runId),
108
+ listReviews: (runId) => backend.listReviews(runId),
114
109
  };
115
110
  }
@@ -1,9 +1,13 @@
1
1
  import { createIndexProjector, } from './index-model.js';
2
2
  import { runProjectorSync } from './projector.js';
3
+ function rowKey(runId, id) {
4
+ return `${runId}\0${id}`;
5
+ }
3
6
  function applyOp(actors, reviews, op) {
4
7
  switch (op.type) {
5
8
  case 'upsertActor':
6
- actors.set(op.actorId, {
9
+ actors.set(rowKey(op.runId, op.actorId), {
10
+ runId: op.runId,
7
11
  actorId: op.actorId,
8
12
  kind: op.kind,
9
13
  status: op.status,
@@ -13,8 +17,10 @@ function applyOp(actors, reviews, op) {
13
17
  });
14
18
  return;
15
19
  case 'setActorStatus': {
16
- const existing = actors.get(op.actorId);
17
- actors.set(op.actorId, {
20
+ const key = rowKey(op.runId, op.actorId);
21
+ const existing = actors.get(key);
22
+ actors.set(key, {
23
+ runId: op.runId,
18
24
  actorId: op.actorId,
19
25
  kind: existing?.kind ?? null,
20
26
  status: op.status,
@@ -25,10 +31,11 @@ function applyOp(actors, reviews, op) {
25
31
  return;
26
32
  }
27
33
  case 'upsertReview': {
28
- const existing = reviews.get(op.reviewId);
29
- reviews.set(op.reviewId, {
34
+ const key = rowKey(op.runId, op.reviewId);
35
+ const existing = reviews.get(key);
36
+ reviews.set(key, {
37
+ runId: op.runId,
30
38
  reviewId: op.reviewId,
31
- actorId: op.actorId,
32
39
  status: op.status,
33
40
  title: existing?.title ?? op.title,
34
41
  updatedAt: op.updatedAt,
@@ -36,9 +43,10 @@ function applyOp(actors, reviews, op) {
36
43
  return;
37
44
  }
38
45
  case 'setReviewStatus': {
39
- const existing = reviews.get(op.reviewId);
46
+ const key = rowKey(op.runId, op.reviewId);
47
+ const existing = reviews.get(key);
40
48
  if (existing) {
41
- reviews.set(op.reviewId, { ...existing, status: op.status, updatedAt: op.updatedAt });
49
+ reviews.set(key, { ...existing, status: op.status, updatedAt: op.updatedAt });
42
50
  }
43
51
  return;
44
52
  }
@@ -57,10 +65,10 @@ export function memory() {
57
65
  applyOp(actors, reviews, op);
58
66
  }
59
67
  }),
60
- getActor: (actorId) => runProjectorSync('memory', () => actors.get(actorId) ?? null),
61
- listReviews: (actorId) => runProjectorSync('memory', () => {
68
+ getActor: (runId, actorId) => runProjectorSync('memory', () => actors.get(rowKey(runId, actorId)) ?? null),
69
+ listReviews: (runId) => runProjectorSync('memory', () => {
62
70
  const rows = [...reviews.values()];
63
- return actorId ? rows.filter((row) => row.actorId === actorId) : rows;
71
+ return runId ? rows.filter((row) => row.runId === runId) : rows;
64
72
  }),
65
73
  dispose: () => runProjectorSync('memory', () => {
66
74
  actors.clear();
@@ -12,9 +12,9 @@ function applyOp(sql, op) {
12
12
  switch (op.type) {
13
13
  case 'upsertActor':
14
14
  return sql `
15
- INSERT INTO looms_actors (actor_id, kind, status, definition_name, parent_actor_id, updated_at)
16
- VALUES (${op.actorId}, ${op.kind}, ${op.status}, ${op.definitionName}, ${op.parentActorId}, ${date})
17
- ON CONFLICT (actor_id) DO UPDATE SET
15
+ INSERT INTO looms_run_actors (run_id, actor_id, kind, status, definition_name, parent_actor_id, updated_at)
16
+ VALUES (${op.runId}, ${op.actorId}, ${op.kind}, ${op.status}, ${op.definitionName}, ${op.parentActorId}, ${date})
17
+ ON CONFLICT (run_id, actor_id) DO UPDATE SET
18
18
  kind = EXCLUDED.kind,
19
19
  status = EXCLUDED.status,
20
20
  definition_name = EXCLUDED.definition_name,
@@ -23,24 +23,24 @@ function applyOp(sql, op) {
23
23
  `;
24
24
  case 'setActorStatus':
25
25
  return sql `
26
- INSERT INTO looms_actors (actor_id, status, updated_at)
27
- VALUES (${op.actorId}, ${op.status}, ${date})
28
- ON CONFLICT (actor_id) DO UPDATE SET
26
+ INSERT INTO looms_run_actors (run_id, actor_id, status, updated_at)
27
+ VALUES (${op.runId}, ${op.actorId}, ${op.status}, ${date})
28
+ ON CONFLICT (run_id, actor_id) DO UPDATE SET
29
29
  status = EXCLUDED.status,
30
30
  updated_at = EXCLUDED.updated_at
31
31
  `;
32
32
  case 'upsertReview':
33
33
  return sql `
34
- INSERT INTO looms_reviews (review_id, actor_id, status, title, updated_at)
35
- VALUES (${op.reviewId}, ${op.actorId}, ${op.status}, ${op.title}, ${date})
36
- ON CONFLICT (review_id) DO UPDATE SET
34
+ INSERT INTO looms_run_reviews (run_id, review_id, status, title, updated_at)
35
+ VALUES (${op.runId}, ${op.reviewId}, ${op.status}, ${op.title}, ${date})
36
+ ON CONFLICT (run_id, review_id) DO UPDATE SET
37
37
  status = EXCLUDED.status,
38
38
  updated_at = EXCLUDED.updated_at
39
39
  `;
40
40
  case 'setReviewStatus':
41
41
  return sql `
42
- UPDATE looms_reviews SET status = ${op.status}, updated_at = ${date}
43
- WHERE review_id = ${op.reviewId}
42
+ UPDATE looms_run_reviews SET status = ${op.status}, updated_at = ${date}
43
+ WHERE run_id = ${op.runId} AND review_id = ${op.reviewId}
44
44
  `;
45
45
  default: {
46
46
  const exhaustiveCheck = op;
@@ -56,13 +56,14 @@ export function postgres(options) {
56
56
  applyOps: (ops) => runProjectorPromise('postgres', () => sql.begin((tx) => Effect.runPromise(Effect.forEach(ops, (op) => Effect.promise(() => Promise.resolve(applyOp(tx, op))), {
57
57
  discard: true,
58
58
  })))).then(() => undefined),
59
- getActor: (actorId) => runProjectorPromise('postgres', () => sql `
60
- SELECT actor_id, kind, status, definition_name, parent_actor_id, updated_at
61
- FROM looms_actors WHERE actor_id = ${actorId}
59
+ getActor: (runId, actorId) => runProjectorPromise('postgres', () => sql `
60
+ SELECT run_id, actor_id, kind, status, definition_name, parent_actor_id, updated_at
61
+ FROM looms_run_actors WHERE run_id = ${runId} AND actor_id = ${actorId}
62
62
  `).then((rows) => {
63
63
  const row = rows[0];
64
64
  return row
65
65
  ? {
66
+ runId: row.run_id,
66
67
  actorId: row.actor_id,
67
68
  kind: row.kind,
68
69
  status: row.status,
@@ -72,16 +73,16 @@ export function postgres(options) {
72
73
  }
73
74
  : null;
74
75
  }),
75
- listReviews: (actorId) => runProjectorPromise('postgres', () => actorId
76
+ listReviews: (runId) => runProjectorPromise('postgres', () => runId
76
77
  ? sql `
77
- SELECT review_id, actor_id, status, title, updated_at
78
- FROM looms_reviews WHERE actor_id = ${actorId}
78
+ SELECT run_id, review_id, status, title, updated_at
79
+ FROM looms_run_reviews WHERE run_id = ${runId}
79
80
  `
80
81
  : sql `
81
- SELECT review_id, actor_id, status, title, updated_at FROM looms_reviews
82
+ SELECT run_id, review_id, status, title, updated_at FROM looms_run_reviews
82
83
  `).then((rows) => rows.map((row) => ({
84
+ runId: row.run_id,
83
85
  reviewId: row.review_id,
84
- actorId: row.actor_id,
85
86
  status: row.status,
86
87
  title: row.title,
87
88
  updatedAt: row.updated_at.getTime(),
@@ -2,42 +2,44 @@
2
2
  * SQL schema for the optional Looms secondary index.
3
3
  */
4
4
  export const POSTGRES_SCHEMA_SQL = `
5
- CREATE TABLE IF NOT EXISTS looms_actors (
6
- actor_id TEXT PRIMARY KEY,
5
+ CREATE TABLE IF NOT EXISTS looms_run_actors (
6
+ run_id TEXT NOT NULL,
7
+ actor_id TEXT NOT NULL,
7
8
  kind TEXT,
8
9
  status TEXT,
9
10
  definition_name TEXT,
10
11
  parent_actor_id TEXT,
11
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
12
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
13
+ PRIMARY KEY (run_id, actor_id)
12
14
  );
13
15
 
14
- CREATE TABLE IF NOT EXISTS looms_reviews (
15
- review_id TEXT PRIMARY KEY,
16
- actor_id TEXT NOT NULL,
16
+ CREATE TABLE IF NOT EXISTS looms_run_reviews (
17
+ run_id TEXT NOT NULL,
18
+ review_id TEXT NOT NULL,
17
19
  status TEXT NOT NULL,
18
20
  title TEXT NOT NULL,
19
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
21
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
22
+ PRIMARY KEY (run_id, review_id)
20
23
  );
21
-
22
- CREATE INDEX IF NOT EXISTS looms_reviews_actor_id_idx ON looms_reviews (actor_id);
23
24
  `.trim();
24
25
  export const SQLITE_SCHEMA_SQL = `
25
- CREATE TABLE IF NOT EXISTS looms_actors (
26
- actor_id TEXT PRIMARY KEY,
26
+ CREATE TABLE IF NOT EXISTS looms_run_actors (
27
+ run_id TEXT NOT NULL,
28
+ actor_id TEXT NOT NULL,
27
29
  kind TEXT,
28
30
  status TEXT,
29
31
  definition_name TEXT,
30
32
  parent_actor_id TEXT,
31
- updated_at INTEGER NOT NULL
33
+ updated_at INTEGER NOT NULL,
34
+ PRIMARY KEY (run_id, actor_id)
32
35
  );
33
36
 
34
- CREATE TABLE IF NOT EXISTS looms_reviews (
35
- review_id TEXT PRIMARY KEY,
36
- actor_id TEXT NOT NULL,
37
+ CREATE TABLE IF NOT EXISTS looms_run_reviews (
38
+ run_id TEXT NOT NULL,
39
+ review_id TEXT NOT NULL,
37
40
  status TEXT NOT NULL,
38
41
  title TEXT NOT NULL,
39
- updated_at INTEGER NOT NULL
42
+ updated_at INTEGER NOT NULL,
43
+ PRIMARY KEY (run_id, review_id)
40
44
  );
41
-
42
- CREATE INDEX IF NOT EXISTS looms_reviews_actor_id_idx ON looms_reviews (actor_id);
43
45
  `.trim();
@@ -9,35 +9,39 @@ function hasDb(options) {
9
9
  function applyOp(db, op) {
10
10
  switch (op.type) {
11
11
  case 'upsertActor':
12
- db.run(`INSERT INTO looms_actors (actor_id, kind, status, definition_name, parent_actor_id, updated_at)
13
- VALUES (?, ?, ?, ?, ?, ?)
14
- ON CONFLICT (actor_id) DO UPDATE SET
12
+ db.run(`INSERT INTO looms_run_actors (run_id, actor_id, kind, status, definition_name, parent_actor_id, updated_at)
13
+ VALUES (?, ?, ?, ?, ?, ?, ?)
14
+ ON CONFLICT (run_id, actor_id) DO UPDATE SET
15
15
  kind = excluded.kind,
16
16
  status = excluded.status,
17
17
  definition_name = excluded.definition_name,
18
18
  parent_actor_id = excluded.parent_actor_id,
19
- updated_at = excluded.updated_at`, [op.actorId, op.kind, op.status, op.definitionName, op.parentActorId, op.updatedAt]);
19
+ updated_at = excluded.updated_at`, [
20
+ op.runId,
21
+ op.actorId,
22
+ op.kind,
23
+ op.status,
24
+ op.definitionName,
25
+ op.parentActorId,
26
+ op.updatedAt,
27
+ ]);
20
28
  return;
21
29
  case 'setActorStatus':
22
- db.run(`INSERT INTO looms_actors (actor_id, status, updated_at)
23
- VALUES (?, ?, ?)
24
- ON CONFLICT (actor_id) DO UPDATE SET
30
+ db.run(`INSERT INTO looms_run_actors (run_id, actor_id, status, updated_at)
31
+ VALUES (?, ?, ?, ?)
32
+ ON CONFLICT (run_id, actor_id) DO UPDATE SET
25
33
  status = excluded.status,
26
- updated_at = excluded.updated_at`, [op.actorId, op.status, op.updatedAt]);
34
+ updated_at = excluded.updated_at`, [op.runId, op.actorId, op.status, op.updatedAt]);
27
35
  return;
28
36
  case 'upsertReview':
29
- db.run(`INSERT INTO looms_reviews (review_id, actor_id, status, title, updated_at)
37
+ db.run(`INSERT INTO looms_run_reviews (run_id, review_id, status, title, updated_at)
30
38
  VALUES (?, ?, ?, ?, ?)
31
- ON CONFLICT (review_id) DO UPDATE SET
39
+ ON CONFLICT (run_id, review_id) DO UPDATE SET
32
40
  status = excluded.status,
33
- updated_at = excluded.updated_at`, [op.reviewId, op.actorId, op.status, op.title, op.updatedAt]);
41
+ updated_at = excluded.updated_at`, [op.runId, op.reviewId, op.status, op.title, op.updatedAt]);
34
42
  return;
35
43
  case 'setReviewStatus':
36
- db.run(`UPDATE looms_reviews SET status = ?, updated_at = ? WHERE review_id = ?`, [
37
- op.status,
38
- op.updatedAt,
39
- op.reviewId,
40
- ]);
44
+ db.run(`UPDATE looms_run_reviews SET status = ?, updated_at = ? WHERE run_id = ? AND review_id = ?`, [op.status, op.updatedAt, op.runId, op.reviewId]);
41
45
  return;
42
46
  default: {
43
47
  const exhaustiveCheck = op;
@@ -60,13 +64,14 @@ export function sqlite(options = {}) {
60
64
  });
61
65
  apply(ops);
62
66
  }),
63
- getActor: (actorId) => runProjectorSync('sqlite', () => {
67
+ getActor: (runId, actorId) => runProjectorSync('sqlite', () => {
64
68
  const row = db
65
- .query(`SELECT actor_id, kind, status, definition_name, parent_actor_id, updated_at
66
- FROM looms_actors WHERE actor_id = ?`)
67
- .get(actorId);
69
+ .query(`SELECT run_id, actor_id, kind, status, definition_name, parent_actor_id, updated_at
70
+ FROM looms_run_actors WHERE run_id = ? AND actor_id = ?`)
71
+ .get(runId, actorId);
68
72
  return row
69
73
  ? {
74
+ runId: row.run_id,
70
75
  actorId: row.actor_id,
71
76
  kind: row.kind,
72
77
  status: row.status,
@@ -76,18 +81,18 @@ export function sqlite(options = {}) {
76
81
  }
77
82
  : null;
78
83
  }),
79
- listReviews: (actorId) => runProjectorSync('sqlite', () => {
80
- const rows = actorId
84
+ listReviews: (runId) => runProjectorSync('sqlite', () => {
85
+ const rows = runId
81
86
  ? db
82
- .query(`SELECT review_id, actor_id, status, title, updated_at
83
- FROM looms_reviews WHERE actor_id = ?`)
84
- .all(actorId)
87
+ .query(`SELECT run_id, review_id, status, title, updated_at
88
+ FROM looms_run_reviews WHERE run_id = ?`)
89
+ .all(runId)
85
90
  : db
86
- .query(`SELECT review_id, actor_id, status, title, updated_at FROM looms_reviews`)
91
+ .query(`SELECT run_id, review_id, status, title, updated_at FROM looms_run_reviews`)
87
92
  .all();
88
93
  return rows.map((row) => ({
94
+ runId: row.run_id,
89
95
  reviewId: row.review_id,
90
- actorId: row.actor_id,
91
96
  status: row.status,
92
97
  title: row.title,
93
98
  updatedAt: row.updated_at,
@@ -1,19 +1,37 @@
1
- import { type EventEnvelope, type ProjectionDefinition } from '@swirls/looms/core';
2
- import type { AnyEventEnvelope, RegisteredEvent } from './register';
3
- export type { AnyEventEnvelope } from './register';
1
+ import { type AnyEventEnvelope, type EventEnvelope, type InferSchemaOutput, type ProjectionDefinition, type RegisteredEvent, type SchemaInput } from '@swirls/looms/core';
4
2
  export { EventIndex, eventThreadKey } from '@swirls/looms/core';
5
- export interface EventFoldDefinition<S, TEvent extends AnyEventEnvelope = RegisteredEvent> {
3
+ export interface LiveFoldDefinition<S, TEvent extends AnyEventEnvelope = RegisteredEvent> {
4
+ readonly kind: 'liveFold';
6
5
  readonly name?: string;
6
+ readonly shape?: unknown;
7
7
  readonly initialState: S;
8
8
  reduce(state: S, event: TEvent): S;
9
- readonly includeEphemeral?: boolean;
10
9
  }
11
- export declare function createFold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(def: EventFoldDefinition<S, TEvent>): EventFoldDefinition<S, TEvent>;
12
- export declare function foldEvents<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(definition: EventFoldDefinition<S, TEvent>, events: readonly TEvent[], from?: S): S;
10
+ /**
11
+ * Client-side fold that sees ephemeral events, which projections skip. Ephemeral events from an
12
+ * effect attempt that the runtime later retried never reach the reducer. The reducer still owns
13
+ * resets on domain events, such as the durable message that replaces streamed text. Use a
14
+ * projection for anything else.
15
+ */
16
+ export declare function defineLiveFold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(def: {
17
+ readonly name?: string;
18
+ readonly shape?: undefined;
19
+ readonly initialState: S;
20
+ reduce(state: S, event: TEvent): S;
21
+ }): LiveFoldDefinition<S, TEvent>;
22
+ export declare function defineLiveFold<TShape extends SchemaInput, TEvent extends AnyEventEnvelope = RegisteredEvent>(def: {
23
+ readonly name?: string;
24
+ readonly shape: TShape;
25
+ readonly initialState: InferSchemaOutput<TShape>;
26
+ reduce(state: NoInfer<InferSchemaOutput<TShape>>, event: TEvent): InferSchemaOutput<TShape>;
27
+ }): LiveFoldDefinition<InferSchemaOutput<TShape>, TEvent>;
28
+ /** Fold events through a live fold, as `useLiveFold` does, including ephemeral events. */
29
+ export declare function foldLive<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(definition: LiveFoldDefinition<S, TEvent>, events: readonly TEvent[]): S;
13
30
  export declare class ProjectionCache {
14
31
  private projectionCache;
15
32
  private foldCache;
16
33
  project<S>(definition: ProjectionDefinition<S>, events: readonly EventEnvelope[]): S;
17
- fold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(definition: EventFoldDefinition<S, TEvent>, events: readonly TEvent[]): S;
34
+ /** A retry can supersede events already folded, so a batch containing one refolds from scratch. */
35
+ fold<S, TEvent extends AnyEventEnvelope = RegisteredEvent>(definition: LiveFoldDefinition<S, TEvent>, events: readonly TEvent[]): S;
18
36
  clear(): void;
19
37
  }
@@ -1,37 +1,33 @@
1
- import { project } from '@swirls/looms/core';
1
+ import { dropSupersededAttempts, project, retriedEffectId, } from '@swirls/looms/core';
2
2
  export { EventIndex, eventThreadKey } from '@swirls/looms/core';
3
- export function createFold(def) {
4
- return def;
3
+ export function defineLiveFold(def) {
4
+ return { ...def, kind: 'liveFold' };
5
5
  }
6
- export function foldEvents(definition, events, from) {
7
- let state = from !== undefined ? from : definition.initialState;
8
- const includeEphemeral = definition.includeEphemeral ?? false;
9
- for (const event of events) {
10
- if (event.ephemeral && !includeEphemeral) {
11
- continue;
12
- }
6
+ function foldFrom(definition, events, from) {
7
+ let state = from;
8
+ for (const event of dropSupersededAttempts(events)) {
13
9
  state = definition.reduce(state, event);
14
10
  }
15
11
  return state;
16
12
  }
17
- function advanceCache(cache, definition, events, compute) {
18
- const total = events.length;
13
+ /** Fold events through a live fold, as `useLiveFold` does, including ephemeral events. */
14
+ export function foldLive(definition, events) {
15
+ return foldFrom(definition, events, definition.initialState);
16
+ }
17
+ function hasNoRetries(batch) {
18
+ return !batch.some((event) => retriedEffectId(event) !== undefined);
19
+ }
20
+ /** Folds only the events appended since the last call, unless `canAppend` rejects them. */
21
+ function advanceCache(cache, definition, events, compute, canAppend = () => true) {
19
22
  const cached = cache.get(definition);
20
- if (!cached) {
21
- const state = compute(definition, events);
22
- cache.set(definition, { state, upTo: total });
23
- return state;
24
- }
25
- if (cached.upTo === total) {
23
+ if (cached?.upTo === events.length) {
26
24
  return cached.state;
27
25
  }
28
- if (cached.upTo < total) {
29
- const state = compute(definition, events.slice(cached.upTo), cached.state);
30
- cache.set(definition, { state, upTo: total });
31
- return state;
32
- }
33
- const state = compute(definition, events);
34
- cache.set(definition, { state, upTo: total });
26
+ const batch = cached && cached.upTo < events.length ? events.slice(cached.upTo) : undefined;
27
+ const state = cached && batch && canAppend(batch)
28
+ ? compute(definition, batch, cached.state)
29
+ : compute(definition, events, definition.initialState);
30
+ cache.set(definition, { state, upTo: events.length });
35
31
  return state;
36
32
  }
37
33
  export class ProjectionCache {
@@ -40,8 +36,9 @@ export class ProjectionCache {
40
36
  project(definition, events) {
41
37
  return advanceCache(this.projectionCache, definition, events, project);
42
38
  }
39
+ /** A retry can supersede events already folded, so a batch containing one refolds from scratch. */
43
40
  fold(definition, events) {
44
- return advanceCache(this.foldCache, definition, events, foldEvents);
41
+ return advanceCache(this.foldCache, definition, events, foldFrom, hasNoRetries);
45
42
  }
46
43
  clear() {
47
44
  this.projectionCache = new WeakMap();
@@ -1,6 +1,6 @@
1
1
  export { createLoomsStore, type LoomsClientStore, type LoomsClientStoreOptions, type LoomsConnectionStatus, type LoomsStoreQuery, type CoalesceOption, type StoreListener, } from './store';
2
- export { createFold, foldEvents, EventIndex, eventThreadKey, type EventFoldDefinition, } from './derived';
2
+ export { defineLiveFold, foldLive, EventIndex, eventThreadKey, type LiveFoldDefinition, } from './derived';
3
3
  export { materializeEvents } from './materialize';
4
4
  export { emptyTables, type MaterializedTables, type RunRow, type ThreadRow } from './tables';
5
- export type { LoomsRegister, RegisteredEvent, AnyEventEnvelope } from './register';
6
- export { LoomsProvider, useRunStore, useProjection, useRunSelector, useRunSummary, useRunEvents, useThreadTree, useEventCounts, useEventBySeq, useLatestEvent, useEventFold, type LoomsProviderProps, type UseRunStoreOptions, type RunSummary, } from './react';
5
+ export type { LoomsRegister, RegisteredEvent, AnyEventEnvelope } from '@swirls/looms/core';
6
+ export { LoomsProvider, useRunStore, useProjection, useRunSelector, useRunSummary, useRunEvents, useThreadTree, useEventCounts, useEventBySeq, useLatestEvent, useLiveFold, type LoomsProviderProps, type UseRunStoreOptions, type RunSummary, } from './react';
@@ -1,5 +1,5 @@
1
1
  export { createLoomsStore, } from './store.js';
2
- export { createFold, foldEvents, EventIndex, eventThreadKey, } from './derived.js';
2
+ export { defineLiveFold, foldLive, EventIndex, eventThreadKey, } from './derived.js';
3
3
  export { materializeEvents } from './materialize.js';
4
4
  export { emptyTables } from './tables.js';
5
- export { LoomsProvider, useRunStore, useProjection, useRunSelector, useRunSummary, useRunEvents, useThreadTree, useEventCounts, useEventBySeq, useLatestEvent, useEventFold, } from './react.js';
5
+ export { LoomsProvider, useRunStore, useProjection, useRunSelector, useRunSummary, useRunEvents, useThreadTree, useEventCounts, useEventBySeq, useLatestEvent, useLiveFold, } from './react.js';