@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
@@ -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,4 +1,4 @@
1
- import { isJsonObject, isJsonString } from '@swirls/looms/core';
1
+ import { completedRunStatus, isJsonObject, isJsonString, } from '@swirls/looms/core';
2
2
  import { emptyTables } from './tables.js';
3
3
  function payloadObject(event) {
4
4
  return isJsonObject(event.payload) ? event.payload : {};
@@ -7,6 +7,16 @@ function readString(obj, key) {
7
7
  const value = obj[key];
8
8
  return isJsonString(value) ? value : undefined;
9
9
  }
10
+ function setRunStatus(tables, runId, status) {
11
+ const existing = tables.runs.get(runId);
12
+ tables.runs.set(runId, {
13
+ runId,
14
+ status,
15
+ rootThreadId: existing?.rootThreadId ?? null,
16
+ kind: existing?.kind ?? null,
17
+ definitionName: existing?.definitionName ?? null,
18
+ });
19
+ }
10
20
  export function materializeEvents(events, from = emptyTables()) {
11
21
  const tables = {
12
22
  runs: from.runs,
@@ -28,17 +38,12 @@ export function materializeEvents(events, from = emptyTables()) {
28
38
  tables.runs.set(event.runId, next);
29
39
  break;
30
40
  }
31
- case 'runtime.run.completed': {
32
- const existing = tables.runs.get(event.runId);
33
- tables.runs.set(event.runId, {
34
- runId: event.runId,
35
- status: readString(payload, 'error') ? 'failed' : 'completed',
36
- rootThreadId: existing?.rootThreadId ?? null,
37
- kind: existing?.kind ?? null,
38
- definitionName: existing?.definitionName ?? null,
39
- });
41
+ case 'runtime.run.completed':
42
+ setRunStatus(tables, event.runId, completedRunStatus(payload));
43
+ break;
44
+ case 'runtime.run.cancelled':
45
+ setRunStatus(tables, event.runId, 'cancelled');
40
46
  break;
41
- }
42
47
  case 'runtime.thread.started': {
43
48
  const threadId = readString(payload, 'threadId') ?? event.threadId;
44
49
  const kind = readString(payload, 'kind');
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type ProjectionDefinition, type ThreadTree } from '@swirls/looms/core';
2
+ import { type ProjectionDefinition, type RunStatus, type ThreadTree } from '@swirls/looms/core';
3
3
  import { createFold, foldEvents, type EventFoldDefinition } from './derived';
4
4
  import type { AnyEventEnvelope, RegisteredEvent } from './register';
5
5
  import { createLoomsStore, type CoalesceOption, type LoomsClientStore, type LoomsConnectionStatus } from './store';
@@ -30,7 +30,11 @@ export declare function useProjection<S, TEvent extends AnyEventEnvelope = Regis
30
30
  /** Subscribe to arbitrary store derivations with an equality check. */
31
31
  export declare function useRunSelector<Selected, TEvent extends AnyEventEnvelope = RegisteredEvent>(store: LoomsClientStore<TEvent>, selector: (store: LoomsClientStore<TEvent>) => Selected, isEqual?: (a: Selected, b: Selected) => boolean): Selected;
32
32
  export interface RunSummary {
33
- readonly status: string;
33
+ readonly status: RunStatus;
34
+ /** True once the run is completed, failed, or cancelled and accepts no new signals. */
35
+ readonly finished: boolean;
36
+ /** True when the run failed or was cancelled, so a fork can continue it from where it stopped. */
37
+ readonly resumable: boolean;
34
38
  readonly kind: string | undefined;
35
39
  readonly definitionName: string | undefined;
36
40
  readonly rootThreadId: string | undefined;
@@ -1,6 +1,6 @@
1
1
  import { Effect, Fiber } from 'effect';
2
2
  import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore, } from 'react';
3
- import { threadTree, toThreadTree } from '@swirls/looms/core';
3
+ import { isTerminalStatus, threadTree, toThreadTree, } from '@swirls/looms/core';
4
4
  import { createFold, foldEvents } from './derived.js';
5
5
  import { createLoomsStore, } from './store.js';
6
6
  const LoomsContext = createContext({ endpoint: '' });
@@ -125,8 +125,11 @@ function shallowEqualSummary(a, b) {
125
125
  export function useRunSummary(store) {
126
126
  const selector = useCallback((s) => {
127
127
  const run = s.getState().runs.get(s.storeId);
128
+ const status = run?.status ?? 'running';
128
129
  return {
129
- status: run?.status ?? 'running',
130
+ status,
131
+ finished: isTerminalStatus(status),
132
+ resumable: status === 'failed' || status === 'cancelled',
130
133
  kind: run?.kind ?? undefined,
131
134
  definitionName: run?.definitionName ?? undefined,
132
135
  rootThreadId: run?.rootThreadId ?? undefined,
@@ -1,6 +1,7 @@
1
+ import type { RunStatus, ThreadStatus } from '@swirls/looms/core';
1
2
  export interface RunRow {
2
3
  runId: string;
3
- status: string;
4
+ status: RunStatus;
4
5
  rootThreadId: string | null;
5
6
  kind: string | null;
6
7
  definitionName: string | null;
@@ -11,7 +12,7 @@ export interface ThreadRow {
11
12
  kind: string;
12
13
  definitionName: string;
13
14
  parentThreadId: string | null;
14
- status: string;
15
+ status: ThreadStatus;
15
16
  }
16
17
  /** Materialized run/thread rows. Event log lives only in `store.events()` / EventIndex. */
17
18
  export interface MaterializedTables {
@@ -1,29 +1,15 @@
1
1
  import { Clock, Effect } from 'effect';
2
- import { createEvent, EventStoreTag, foldRun, } from '@swirls/looms/core';
2
+ import { createEvent, descendantThreadIds, EventStoreTag, foldRun, } from '@swirls/looms/core';
3
3
  import { RuntimeExecutionError } from './errors.js';
4
4
  import { waitSatisfiedEvents, workerTask } from './helpers.js';
5
5
  import { notifyObserver } from './observer.js';
6
6
  import { isWorkerEffect } from './worker-effect-dispatch.js';
7
- function descendantThreadIds(state, targetThreadId) {
8
- return Object.values(state.threads)
9
- .filter((record) => {
10
- let parentThreadId = record.parentThreadId;
11
- while (parentThreadId) {
12
- if (parentThreadId === targetThreadId) {
13
- return true;
14
- }
15
- parentThreadId = state.threads[parentThreadId]?.parentThreadId ?? null;
16
- }
17
- return false;
18
- })
19
- .map((record) => record.threadId);
20
- }
21
7
  /**
22
8
  * Cancel a thread and its descendants. Worker attempts in flight are asked to stop and must
23
9
  * acknowledge; attempts waiting to retry were never handed out and are cancelled outright.
24
10
  */
25
11
  function planCancellation(input) {
26
- const { runId, state, registry, now } = input;
12
+ const { runId, seq, state, registry, now } = input;
27
13
  const targetThreadId = input.threadId ?? state.rootThreadId ?? '';
28
14
  const idempotencyKey = `cancel:${targetThreadId}`;
29
15
  if (state.processedIdempotencyKeys.includes(idempotencyKey)) {
@@ -65,18 +51,18 @@ function planCancellation(input) {
65
51
  idempotencyKey,
66
52
  }));
67
53
  }
68
- const threadEvents = [
69
- ...descendants.map((_, index) => descendants[descendants.length - 1 - index]),
70
- targetThreadId,
71
- ]
54
+ const threadEvents = [...descendants.toReversed(), targetThreadId]
72
55
  .filter((threadId) => threadId.length > 0)
73
- .map((threadId) => createEvent(runId, {
74
- type: 'runtime.thread.cancelled',
75
- payload: { threadId, reason: 'cancelled' },
76
- threadId,
77
- origin: { type: 'external' },
78
- idempotencyKey,
79
- }));
56
+ .map((threadId) => {
57
+ const payload = { threadId, reason: 'cancelled' };
58
+ return createEvent(runId, {
59
+ type: 'runtime.thread.cancelled',
60
+ payload: threadId === targetThreadId ? { ...payload, resumeSeq: seq } : payload,
61
+ threadId,
62
+ origin: { type: 'external' },
63
+ idempotencyKey,
64
+ });
65
+ });
80
66
  const batch = [...effectEvents, ...threadEvents];
81
67
  const folded = foldRun(batch, registry, { runId, initial: state });
82
68
  const satisfied = waitSatisfiedEvents(folded, batch).map((event) => createEvent(runId, event));
@@ -91,6 +77,7 @@ export function createCancellation(options) {
91
77
  return yield* repository.commit(store, runId, (current) => {
92
78
  const plan = planCancellation({
93
79
  runId,
80
+ seq: current.seq,
94
81
  state: current.state,
95
82
  threadId,
96
83
  registry,
@@ -19,6 +19,45 @@ export declare class StartRunConflictError extends StartRunConflictError_base<{
19
19
  }> {
20
20
  constructor(runId: string, requested: RunStartIdentity, existing: DurableRunStartIdentity, mismatches: readonly (keyof RunStartIdentity)[]);
21
21
  }
22
+ declare const RunTerminalError_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 & {
23
+ readonly _tag: "RunTerminalError";
24
+ } & Readonly<A>;
25
+ export declare class RunTerminalError extends RunTerminalError_base<{
26
+ readonly runId: string;
27
+ readonly status: string;
28
+ readonly message: string;
29
+ }> {
30
+ constructor(runId: string, status: string);
31
+ }
32
+ declare const RunNotFoundError_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 & {
33
+ readonly _tag: "RunNotFoundError";
34
+ } & Readonly<A>;
35
+ export declare class RunNotFoundError extends RunNotFoundError_base<{
36
+ readonly runId: string;
37
+ readonly message: string;
38
+ }> {
39
+ constructor(runId: string);
40
+ }
41
+ declare const RunNotFinishedError_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 & {
42
+ readonly _tag: "RunNotFinishedError";
43
+ } & Readonly<A>;
44
+ export declare class RunNotFinishedError extends RunNotFinishedError_base<{
45
+ readonly runId: string;
46
+ readonly status: string;
47
+ readonly message: string;
48
+ }> {
49
+ constructor(runId: string, status: string);
50
+ }
51
+ declare const ForkConflictError_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 & {
52
+ readonly _tag: "ForkConflictError";
53
+ } & Readonly<A>;
54
+ export declare class ForkConflictError extends ForkConflictError_base<{
55
+ readonly runId: string;
56
+ readonly sourceRunId: string;
57
+ readonly message: string;
58
+ }> {
59
+ constructor(runId: string, sourceRunId: string);
60
+ }
22
61
  declare const DuplicateEffectDispatchError_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 & {
23
62
  readonly _tag: "DuplicateEffectDispatchError";
24
63
  } & Readonly<A>;
@@ -11,6 +11,38 @@ export class StartRunConflictError extends Data.TaggedError('StartRunConflictErr
11
11
  this.name = 'StartRunConflictError';
12
12
  }
13
13
  }
14
+ export class RunTerminalError extends Data.TaggedError('RunTerminalError') {
15
+ constructor(runId, status) {
16
+ super({ runId, status, message: `Run "${runId}" is ${status} and accepts no new events` });
17
+ this.name = 'RunTerminalError';
18
+ }
19
+ }
20
+ export class RunNotFoundError extends Data.TaggedError('RunNotFoundError') {
21
+ constructor(runId) {
22
+ super({ runId, message: `Run "${runId}" does not exist` });
23
+ this.name = 'RunNotFoundError';
24
+ }
25
+ }
26
+ export class RunNotFinishedError extends Data.TaggedError('RunNotFinishedError') {
27
+ constructor(runId, status) {
28
+ super({
29
+ runId,
30
+ status,
31
+ message: `Run "${runId}" is ${status}; only finished runs can be forked`,
32
+ });
33
+ this.name = 'RunNotFinishedError';
34
+ }
35
+ }
36
+ export class ForkConflictError extends Data.TaggedError('ForkConflictError') {
37
+ constructor(runId, sourceRunId) {
38
+ super({
39
+ runId,
40
+ sourceRunId,
41
+ message: `Run "${runId}" already exists and is not a fork of "${sourceRunId}"`,
42
+ });
43
+ this.name = 'ForkConflictError';
44
+ }
45
+ }
14
46
  export class DuplicateEffectDispatchError extends Data.TaggedError('DuplicateEffectDispatchError') {
15
47
  constructor(runId, effectId, threadId) {
16
48
  super({
@@ -43,6 +43,8 @@ export declare function threadStartedEvents(definitions: ReadonlyMap<string, Reg
43
43
  parentThreadId: string | null;
44
44
  }): Effect.Effect<ReadonlyArray<EventInput>, UnknownDefinitionError>;
45
45
  export declare function synthesizedThreadFailed(before: RunState, after: RunState, written: readonly EventEnvelope[]): EventInput[];
46
+ /** Cancel whatever is still live below each thread `written` cancels, deepest first. */
47
+ export declare function cascadedCancellations(state: RunState, written: readonly EventEnvelope[]): EventInput[];
46
48
  export declare function waitSatisfiedEvents(state: RunState, events: readonly EventEnvelope[]): EventInput[];
47
49
  export declare function createEffectFailedEvent(runId: string, item: {
48
50
  effectId: string;
@@ -1,5 +1,5 @@
1
1
  import { Data, Effect, Layer, Predicate } from 'effect';
2
- import { asJson, createEvent, definitionKey, makeMemorySnapshotStore, matchingWaits, snapshotStoreOf, validateEventInput, validateEventsEffect, validateInputEffect, withSnapshotStore, InvalidEventError, } from '@swirls/looms/core';
2
+ import { asJson, createEvent, definitionKey, descendantThreadIds, isTerminalStatus, makeMemorySnapshotStore, matchingWaits, snapshotStoreOf, validateEventInput, validateEventsEffect, validateInputEffect, withSnapshotStore, InvalidEventError, } from '@swirls/looms/core';
3
3
  export function workerTask(runId, item, attempt) {
4
4
  return {
5
5
  runId,
@@ -180,6 +180,28 @@ export function synthesizedThreadFailed(before, after, written) {
180
180
  }
181
181
  return out;
182
182
  }
183
+ /** Cancel whatever is still live below each thread `written` cancels, deepest first. */
184
+ export function cascadedCancellations(state, written) {
185
+ const cancelled = written.flatMap((event) => {
186
+ if (event.type !== 'runtime.thread.cancelled') {
187
+ return [];
188
+ }
189
+ const payload = Predicate.isObject(event.payload) ? event.payload : {};
190
+ const threadId = Predicate.isString(payload.threadId) ? payload.threadId : event.threadId;
191
+ return threadId ? [threadId] : [];
192
+ });
193
+ const live = new Set(cancelled
194
+ .flatMap((threadId) => descendantThreadIds(state, threadId))
195
+ .filter((threadId) => {
196
+ const record = state.threads[threadId];
197
+ return record !== undefined && !isTerminalStatus(record.status);
198
+ }));
199
+ return [...live].toReversed().map((threadId) => ({
200
+ type: 'runtime.thread.cancelled',
201
+ payload: { threadId, reason: 'cancelled' },
202
+ threadId,
203
+ }));
204
+ }
183
205
  export function waitSatisfiedEvents(state, events) {
184
206
  const produced = [];
185
207
  const remaining = { ...state.waits };
@@ -1,4 +1,4 @@
1
- export { createRuntime, createTimeoutScheduler, DuplicateEffectDispatchError, MaxWakeIterationsError, RunOverloadedError, StartRunConflictError, type CreateRuntimeOptions, type EffectWorker, type EffectWorkerTask, type LoomsRuntime, type RunOperationalStatus, type RegisteredDefinition, type StartRunArgs, type WakeError, type WakeScheduler, type WorkerCallback, type WorkerCallbackInput, } from './runtime';
1
+ export { createRuntime, createTimeoutScheduler, DuplicateEffectDispatchError, ForkConflictError, MaxWakeIterationsError, RunNotFinishedError, RunNotFoundError, RunOverloadedError, RunTerminalError, StartRunConflictError, type CreateRuntimeOptions, type EffectWorker, type EffectWorkerTask, type LoomsRuntime, type RunOperationalStatus, type RegisteredDefinition, type StartRunArgs, type WakeError, type WakeScheduler, type WorkerCallback, type WorkerCallbackInput, } from './runtime';
2
2
  export { createLooms, type Looms, type CreateLoomsOptions, type StartResult } from './looms';
3
3
  export { serveHttp, createFetchHandler, isLoomsApiPath, type FetchHandlerOptions, authAllowed, authDenied, bearerAuth, defaultAuthorize, type AuthDecision, type AuthorizedAccess, type Authorize, type BearerTokens, type RouteAccess, type RouteInfo, type RunningServer, type LoomsFetchResult, } from './server';
4
4
  export { createEventStreamResponse, type EventStreamOptions } from './sse';
@@ -1,4 +1,4 @@
1
- export { createRuntime, createTimeoutScheduler, DuplicateEffectDispatchError, MaxWakeIterationsError, RunOverloadedError, StartRunConflictError, } from './runtime.js';
1
+ export { createRuntime, createTimeoutScheduler, DuplicateEffectDispatchError, ForkConflictError, MaxWakeIterationsError, RunNotFinishedError, RunNotFoundError, RunOverloadedError, RunTerminalError, StartRunConflictError, } from './runtime.js';
2
2
  export { createLooms } from './looms.js';
3
3
  export { serveHttp, createFetchHandler, isLoomsApiPath, authAllowed, authDenied, bearerAuth, defaultAuthorize, } from './server.js';
4
4
  export { createEventStreamResponse } from './sse.js';
@@ -89,6 +89,10 @@ export interface Looms {
89
89
  project<S>(runId: string, definition: ProjectionDefinition<S>): Promise<S>;
90
90
  replayTo(runId: string, seq: number): Promise<ReplayStep | null>;
91
91
  cancel(runId: string, threadId?: string): Promise<RunState>;
92
+ /** Start a new run that continues where a finished run ended. */
93
+ fork(sourceRunId: string, options?: {
94
+ runId?: string;
95
+ }): Promise<StartResult>;
92
96
  workerCallback(runId: string, input: WorkerCallbackInput): Promise<RunState>;
93
97
  fetch(req: Request): Promise<Response | null>;
94
98
  serve(options?: {
@@ -98,6 +98,7 @@ export function createLooms(options = {}) {
98
98
  project: (runId, definition) => runEffect((i) => i.runtime.project(runId, definition)),
99
99
  replayTo: (runId, seq) => runEffect((i) => i.runtime.replayTo(runId, seq)),
100
100
  cancel: (runId, threadId) => runEffect((i) => i.runtime.cancel(runId, threadId)),
101
+ fork: (sourceRunId, forkOpts) => runEffect((i) => i.runtime.fork(sourceRunId, forkOpts)),
101
102
  workerCallback: (runId, input) => runEffect((i) => i.runtime.workerCallback(runId, input)),
102
103
  fetch: (req) => {
103
104
  if (!handlesPath(new URL(req.url).pathname, debuggerMount)) {
@@ -5,9 +5,7 @@ function canonicalJson(value) {
5
5
  return `[${value.map(canonicalJson).join(',')}]`;
6
6
  }
7
7
  if (Predicate.isObject(value)) {
8
- const keys = Object.keys(value);
9
- // oxlint-disable-next-line unicorn/no-array-sort
10
- keys.sort();
8
+ const keys = Object.keys(value).toSorted();
11
9
  return `{${keys
12
10
  .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key] ?? null)}`)
13
11
  .join(',')}}`;
@@ -2,7 +2,7 @@ import { type AnyRuntimeModule } from '@swirls/looms/core';
2
2
  export type { RegisteredDefinition } from '@swirls/looms/core';
3
3
  export type { WakeScheduler } from './wake-scheduler';
4
4
  export { createTimeoutScheduler } from './wake-scheduler';
5
- export { DuplicateEffectDispatchError, MaxWakeIterationsError, StartRunConflictError, } from './errors';
5
+ export { DuplicateEffectDispatchError, ForkConflictError, MaxWakeIterationsError, RunNotFinishedError, RunNotFoundError, RunTerminalError, StartRunConflictError, } from './errors';
6
6
  export { RunOverloadedError } from './ingress';
7
7
  export type { CreateRuntimeOptions, EffectWorker, EffectWorkerTask, LoomsRuntime, RunOperationalStatus, StartResult, StartRunArgs, WakeError, } from './types';
8
8
  export type { WorkerCallback, WorkerCallbackInput } from '@swirls/looms/core';