nexus-ai-pro 2.1.0 → 2.2.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 (194) hide show
  1. package/API_STABILITY.md +38 -0
  2. package/CHANGELOG.md +145 -1
  3. package/README.md +13 -8
  4. package/dist/cache/memory-cache.d.ts +8 -0
  5. package/dist/cache/memory-cache.js +9 -0
  6. package/dist/cache/semantic-cache.d.ts +6 -3
  7. package/dist/cache/semantic-cache.js +5 -2
  8. package/dist/cli/args.js +3 -0
  9. package/dist/cli/db.d.ts +16 -4
  10. package/dist/cli/db.js +169 -22
  11. package/dist/cli/doctor.d.ts +17 -0
  12. package/dist/cli/doctor.js +53 -0
  13. package/dist/cli.js +12 -1
  14. package/dist/core/lifecycle.js +11 -6
  15. package/dist/core/nexus.d.ts +6 -0
  16. package/dist/core/nexus.js +103 -22
  17. package/dist/doctor/index.d.ts +90 -0
  18. package/dist/doctor/index.js +237 -0
  19. package/dist/graph/functional.d.ts +80 -7
  20. package/dist/graph/functional.js +138 -35
  21. package/dist/graph/graph.d.ts +15 -5
  22. package/dist/graph/graph.js +72 -20
  23. package/dist/images/evals.js +4 -2
  24. package/dist/operations/adapters.d.ts +52 -4
  25. package/dist/operations/adapters.js +194 -7
  26. package/dist/operations/runner.js +1 -0
  27. package/dist/operations/stats.js +8 -1
  28. package/dist/operations/store.d.ts +5 -1
  29. package/dist/operations/store.js +7 -0
  30. package/dist/ops/circuit-breaker.d.ts +52 -0
  31. package/dist/ops/circuit-breaker.js +4 -0
  32. package/dist/ops/circuit-store.d.ts +13 -2
  33. package/dist/ops/circuit-store.js +68 -0
  34. package/dist/ops/circuit-sync.d.ts +17 -0
  35. package/dist/ops/circuit-sync.js +86 -0
  36. package/dist/ops/rate-limiter.d.ts +2 -0
  37. package/dist/ops/rate-limiter.js +2 -0
  38. package/dist/postgres/circuits.d.ts +21 -4
  39. package/dist/postgres/circuits.js +65 -4
  40. package/dist/postgres/client.d.ts +0 -2
  41. package/dist/postgres/client.js +0 -4
  42. package/dist/postgres/evaluate.d.ts +16 -4
  43. package/dist/postgres/evaluate.js +17 -5
  44. package/dist/postgres/index.d.ts +32 -11
  45. package/dist/postgres/index.js +65 -31
  46. package/dist/postgres/migrations.d.ts +60 -0
  47. package/dist/postgres/migrations.js +121 -0
  48. package/dist/postgres/operations.d.ts +14 -3
  49. package/dist/postgres/operations.js +45 -15
  50. package/dist/postgres/prompts.d.ts +11 -2
  51. package/dist/postgres/prompts.js +14 -3
  52. package/dist/postgres/rollups.d.ts +35 -0
  53. package/dist/postgres/rollups.js +129 -0
  54. package/dist/postgres/store.d.ts +11 -2
  55. package/dist/postgres/store.js +14 -3
  56. package/dist/postgres/traces.d.ts +13 -3
  57. package/dist/postgres/traces.js +25 -3
  58. package/dist/postgres/vectors.d.ts +11 -2
  59. package/dist/postgres/vectors.js +14 -3
  60. package/dist/router/auto-router.d.ts +34 -7
  61. package/dist/router/auto-router.js +201 -68
  62. package/dist/server/assistant.d.ts +4 -0
  63. package/dist/server/assistant.js +8 -0
  64. package/dist/server/errors.d.ts +12 -0
  65. package/dist/server/errors.js +7 -0
  66. package/dist/server/index.d.ts +1 -1
  67. package/dist/server/index.js +1 -1
  68. package/dist/server/runs.d.ts +24 -1
  69. package/dist/server/runs.js +90 -14
  70. package/dist/sqlite/index.d.ts +16 -3
  71. package/dist/sqlite/index.js +16 -3
  72. package/dist/sqlite/migrations.d.ts +26 -0
  73. package/dist/sqlite/migrations.js +63 -0
  74. package/dist/sqlite/operations.d.ts +14 -3
  75. package/dist/sqlite/operations.js +42 -15
  76. package/dist/sqlite/store.d.ts +9 -2
  77. package/dist/sqlite/store.js +13 -3
  78. package/dist/sqlite/vectors.d.ts +11 -2
  79. package/dist/sqlite/vectors.js +13 -2
  80. package/dist/store/tenant.d.ts +12 -0
  81. package/dist/store/tenant.js +29 -0
  82. package/dist/tenancy/index.d.ts +113 -0
  83. package/dist/tenancy/index.js +233 -0
  84. package/dist/tracing/instrument.js +2 -0
  85. package/dist/types/config.d.ts +24 -2
  86. package/dist/types/graph.d.ts +11 -1
  87. package/dist/types/messages.d.ts +5 -0
  88. package/dist/types/operations.d.ts +6 -0
  89. package/dist/types/response.d.ts +20 -0
  90. package/dist/types/server.d.ts +10 -1
  91. package/dist/utils/schema-migrations.d.ts +97 -0
  92. package/dist/utils/schema-migrations.js +91 -0
  93. package/dist/utils/signals.d.ts +17 -0
  94. package/dist/utils/signals.js +26 -0
  95. package/dist/utils/tenant.d.ts +14 -0
  96. package/dist/utils/tenant.js +16 -0
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist-cjs/cache/memory-cache.d.ts +8 -0
  100. package/dist-cjs/cache/memory-cache.js +9 -0
  101. package/dist-cjs/cache/semantic-cache.d.ts +6 -3
  102. package/dist-cjs/cache/semantic-cache.js +5 -2
  103. package/dist-cjs/cli/args.js +3 -0
  104. package/dist-cjs/cli/db.d.ts +16 -4
  105. package/dist-cjs/cli/db.js +200 -20
  106. package/dist-cjs/cli/doctor.d.ts +17 -0
  107. package/dist-cjs/cli/doctor.js +56 -0
  108. package/dist-cjs/core/lifecycle.js +11 -6
  109. package/dist-cjs/core/nexus.d.ts +6 -0
  110. package/dist-cjs/core/nexus.js +103 -22
  111. package/dist-cjs/doctor/index.d.ts +90 -0
  112. package/dist-cjs/doctor/index.js +276 -0
  113. package/dist-cjs/graph/functional.d.ts +80 -7
  114. package/dist-cjs/graph/functional.js +137 -34
  115. package/dist-cjs/graph/graph.d.ts +15 -5
  116. package/dist-cjs/graph/graph.js +72 -20
  117. package/dist-cjs/images/evals.js +4 -2
  118. package/dist-cjs/operations/adapters.d.ts +52 -4
  119. package/dist-cjs/operations/adapters.js +193 -6
  120. package/dist-cjs/operations/runner.js +1 -0
  121. package/dist-cjs/operations/stats.js +8 -1
  122. package/dist-cjs/operations/store.d.ts +5 -1
  123. package/dist-cjs/operations/store.js +7 -0
  124. package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
  125. package/dist-cjs/ops/circuit-breaker.js +4 -0
  126. package/dist-cjs/ops/circuit-store.d.ts +13 -2
  127. package/dist-cjs/ops/circuit-store.js +68 -0
  128. package/dist-cjs/ops/circuit-sync.d.ts +17 -0
  129. package/dist-cjs/ops/circuit-sync.js +87 -0
  130. package/dist-cjs/ops/rate-limiter.d.ts +2 -0
  131. package/dist-cjs/ops/rate-limiter.js +2 -0
  132. package/dist-cjs/postgres/circuits.d.ts +21 -4
  133. package/dist-cjs/postgres/circuits.js +98 -3
  134. package/dist-cjs/postgres/client.d.ts +0 -2
  135. package/dist-cjs/postgres/client.js +0 -5
  136. package/dist-cjs/postgres/evaluate.d.ts +16 -4
  137. package/dist-cjs/postgres/evaluate.js +50 -4
  138. package/dist-cjs/postgres/index.d.ts +32 -11
  139. package/dist-cjs/postgres/index.js +69 -20
  140. package/dist-cjs/postgres/migrations.d.ts +60 -0
  141. package/dist-cjs/postgres/migrations.js +129 -0
  142. package/dist-cjs/postgres/operations.d.ts +14 -3
  143. package/dist-cjs/postgres/operations.js +78 -14
  144. package/dist-cjs/postgres/prompts.d.ts +11 -2
  145. package/dist-cjs/postgres/prompts.js +47 -2
  146. package/dist-cjs/postgres/rollups.d.ts +35 -0
  147. package/dist-cjs/postgres/rollups.js +168 -0
  148. package/dist-cjs/postgres/store.d.ts +11 -2
  149. package/dist-cjs/postgres/store.js +47 -2
  150. package/dist-cjs/postgres/traces.d.ts +13 -3
  151. package/dist-cjs/postgres/traces.js +58 -2
  152. package/dist-cjs/postgres/vectors.d.ts +11 -2
  153. package/dist-cjs/postgres/vectors.js +47 -2
  154. package/dist-cjs/router/auto-router.d.ts +34 -7
  155. package/dist-cjs/router/auto-router.js +201 -68
  156. package/dist-cjs/server/assistant.d.ts +4 -0
  157. package/dist-cjs/server/assistant.js +8 -0
  158. package/dist-cjs/server/errors.d.ts +12 -0
  159. package/dist-cjs/server/errors.js +9 -1
  160. package/dist-cjs/server/index.d.ts +1 -1
  161. package/dist-cjs/server/index.js +2 -1
  162. package/dist-cjs/server/runs.d.ts +24 -1
  163. package/dist-cjs/server/runs.js +89 -13
  164. package/dist-cjs/sqlite/index.d.ts +16 -3
  165. package/dist-cjs/sqlite/index.js +31 -10
  166. package/dist-cjs/sqlite/migrations.d.ts +26 -0
  167. package/dist-cjs/sqlite/migrations.js +71 -0
  168. package/dist-cjs/sqlite/operations.d.ts +14 -3
  169. package/dist-cjs/sqlite/operations.js +76 -15
  170. package/dist-cjs/sqlite/store.d.ts +9 -2
  171. package/dist-cjs/sqlite/store.js +47 -3
  172. package/dist-cjs/sqlite/vectors.d.ts +11 -2
  173. package/dist-cjs/sqlite/vectors.js +47 -2
  174. package/dist-cjs/store/tenant.d.ts +12 -0
  175. package/dist-cjs/store/tenant.js +33 -0
  176. package/dist-cjs/tenancy/index.d.ts +113 -0
  177. package/dist-cjs/tenancy/index.js +250 -0
  178. package/dist-cjs/tracing/instrument.js +2 -0
  179. package/dist-cjs/types/config.d.ts +24 -2
  180. package/dist-cjs/types/graph.d.ts +11 -1
  181. package/dist-cjs/types/messages.d.ts +5 -0
  182. package/dist-cjs/types/operations.d.ts +6 -0
  183. package/dist-cjs/types/response.d.ts +20 -0
  184. package/dist-cjs/types/server.d.ts +10 -1
  185. package/dist-cjs/utils/schema-migrations.d.ts +97 -0
  186. package/dist-cjs/utils/schema-migrations.js +102 -0
  187. package/dist-cjs/utils/signals.d.ts +17 -0
  188. package/dist-cjs/utils/signals.js +29 -0
  189. package/dist-cjs/utils/tenant.d.ts +14 -0
  190. package/dist-cjs/utils/tenant.js +22 -0
  191. package/dist-cjs/version.d.ts +1 -1
  192. package/dist-cjs/version.js +1 -1
  193. package/llms.txt +4 -3
  194. package/package.json +71 -2
@@ -1,14 +1,18 @@
1
1
  import type { OperationLifecycleLike } from '../types/lifecycle.js';
2
2
  import { GraphError } from './errors.js';
3
- import type { GraphCheckpoint, GraphCheckpointer, GraphDescription, GraphEvent, GraphStatus, InterruptRequest, PendingInterrupt, RetryPolicy } from '../types/graph.js';
3
+ import type { GraphCheckpoint, GraphCheckpointer, GraphDescription, GraphEvent, GraphStatus, InterruptRequest, PendingInterrupt, RetryPolicy, RunControlLike } from '../types/graph.js';
4
4
  import type { Store } from '../types/store.js';
5
5
  /** What a workflow checkpoint holds: the input, every completed step's result, and the output. */
6
6
  export interface WorkflowState<I = unknown, O = unknown> {
7
7
  /** The input the workflow was started with. */
8
8
  input: I;
9
- /** Each completed step's result, keyed by step name — `name#2` for the second call with a name. */
9
+ /**
10
+ * Each completed step's result, keyed by step name — `name#2` for the second call with a name. A
11
+ * step whose `onError` supplied its result after its retries ran out also records `recovered`.
12
+ */
10
13
  steps: Record<string, {
11
14
  value?: unknown;
15
+ recovered?: StepRecovery;
12
16
  }>;
13
17
  /** The workflow's return value, once it has completed. */
14
18
  output?: O;
@@ -23,21 +27,68 @@ export interface StepContext {
23
27
  readonly attempt: number;
24
28
  /** Aborted when the run is cancelled or the step exceeds its timeout. */
25
29
  readonly signal: AbortSignal;
30
+ /**
31
+ * Shows the step is still making progress, which refreshes its idle timeout. For long work that
32
+ * sends nothing else, such as a large upload or a slow tool call.
33
+ */
34
+ heartbeat(): void;
35
+ }
36
+ /** Limits on one attempt of a step: its whole run, and the time it may go without progress. */
37
+ export interface StepTimeout {
38
+ /** Fails the attempt when it runs longer than this, in milliseconds. */
39
+ runMs?: number;
40
+ /**
41
+ * Fails the attempt when this long passes without `context.heartbeat()`, in milliseconds, which
42
+ * tells a hung step from a slow one that is working.
43
+ */
44
+ idleMs?: number;
45
+ }
46
+ /** What a step's `onError` receives once its retries have run out. */
47
+ export interface StepFailure {
48
+ /** The step's name. */
49
+ name: string;
50
+ /** The key its result is recorded under. */
51
+ key: string;
52
+ /** Attempts made. */
53
+ attempts: number;
54
+ /** The last attempt's error. */
55
+ error: unknown;
56
+ }
57
+ /** How a step's result was recovered, as recorded in the workflow state. */
58
+ export interface StepRecovery {
59
+ /** The error that exhausted the step's retries. */
60
+ error: {
61
+ name: string;
62
+ message: string;
63
+ };
64
+ /** Attempts made before `onError` decided. */
65
+ attempts: number;
26
66
  }
27
67
  /** How one step runs. */
28
- export interface StepOptions {
68
+ export interface StepOptions<T = unknown> {
29
69
  /** Retries a failing attempt, overriding the workflow's default policy. */
30
70
  retry?: RetryPolicy;
31
71
  /** Aborts the step's signal and fails the attempt when it runs longer than this, in milliseconds. */
32
72
  timeoutMs?: number;
73
+ /** Run and idle limits on each attempt. `runMs` is `timeoutMs` by another name; `timeoutMs` wins when both are set. */
74
+ timeout?: StepTimeout;
75
+ /**
76
+ * Decides what happens once the step's retries run out. What it returns becomes the step's
77
+ * result, recorded with `recovered`, so a resumed run does not run the step again — a refund after
78
+ * a charge that kept failing, say. Throwing fails the step with that error. A cancelled run, a
79
+ * question, or a drain is never passed to it.
80
+ */
81
+ onError?: (failure: StepFailure) => T | Promise<T>;
33
82
  }
34
83
  /** What a workflow function receives besides its input. */
35
84
  export interface WorkflowContext {
36
85
  /** The thread this run belongs to. */
37
86
  readonly threadId: string;
87
+ /** The tenant the run is for, when it has one. */
88
+ readonly tenantId?: string;
38
89
  /** Aborted when the run is cancelled. */
39
90
  readonly signal: AbortSignal;
40
- /** Long-term memory, when the workflow was created with a store. */
91
+ /** Long-term memory, when the workflow was created with a store. In a tenant's run, the tenant's view of it. */
41
92
  readonly store?: Store;
42
93
  /**
43
94
  * Runs a piece of work once per thread and records its result.
@@ -46,7 +97,7 @@ export interface WorkflowContext {
46
97
  * running. Name steps so the same call has the same name every time; a name used more than once
47
98
  * is numbered in call order. The result must survive JSON serialization.
48
99
  */
49
- step<T>(name: string, run: (context: StepContext) => Promise<T> | T, options?: StepOptions): Promise<T>;
100
+ step<T>(name: string, run: (context: StepContext) => Promise<T> | T, options?: StepOptions<T>): Promise<T>;
50
101
  /**
51
102
  * Suspends the workflow until a person answers.
52
103
  *
@@ -73,6 +124,11 @@ export interface WorkflowOptions {
73
124
  store?: Store;
74
125
  /** Default retry policy for every step. A step's own policy wins. Defaults to one attempt. */
75
126
  retry?: RetryPolicy;
127
+ /**
128
+ * Defaults for every step: its retries, its run and idle timeouts, and its `onError`. A step's own
129
+ * options win, field by field.
130
+ */
131
+ stepDefaults?: Pick<StepOptions, 'retry' | 'timeout' | 'onError'>;
76
132
  /** Steps that may run at once, for a workflow that starts many with `Promise.all`. Defaults to 16. */
77
133
  maxConcurrency?: number;
78
134
  /**
@@ -93,6 +149,19 @@ export interface WorkflowRunOptions {
93
149
  metadata?: Record<string, unknown>;
94
150
  /** Receives each step starting, retrying, and finishing, each checkpoint, and custom events. */
95
151
  onEvent?: (event: GraphEvent) => void;
152
+ /**
153
+ * Lets the caller stop the run between steps: pass a `RunControl` and call `drain()`. Steps in
154
+ * flight finish and are recorded, no new step starts, and the run ends with `GraphDrainedError`;
155
+ * `continue()` carries it on, on this worker or another. The agent server drains a workflow it
156
+ * serves this way.
157
+ */
158
+ control?: RunControlLike;
159
+ /**
160
+ * The tenant the run is for. Recorded on every checkpoint as `metadata.tenantId`, read as
161
+ * `context.tenantId`, and scoping `context.store`; another tenant's thread is not found. A run
162
+ * without one carries on the thread's.
163
+ */
164
+ tenantId?: string;
96
165
  }
97
166
  /** One event of a streamed workflow run. */
98
167
  export type WorkflowEvent<O = unknown> = {
@@ -152,17 +221,21 @@ export interface WorkflowResult<O = unknown> {
152
221
  /** Every pending question. */
153
222
  interrupts?: PendingInterrupt[];
154
223
  }
155
- /** Raised when a step outlives its `timeoutMs`. */
224
+ /** Raised when a step outlives its run timeout, or goes longer than its idle timeout without progress. */
156
225
  export declare class WorkflowStepTimeoutError extends GraphError {
157
226
  /** The step. */
158
227
  readonly stepName: string;
159
228
  /** The limit it exceeded, in milliseconds. */
160
229
  readonly timeoutMs: number;
230
+ /** Which limit: `run` for the whole attempt, `idle` for the time without progress. */
231
+ readonly kind: 'run' | 'idle';
161
232
  constructor(
162
233
  /** The step. */
163
234
  stepName: string,
164
235
  /** The limit it exceeded, in milliseconds. */
165
- timeoutMs: number);
236
+ timeoutMs: number,
237
+ /** Which limit: `run` for the whole attempt, `idle` for the time without progress. */
238
+ kind?: 'run' | 'idle');
166
239
  }
167
240
  /**
168
241
  * A durable workflow: a function whose steps are checkpointed, so it survives interrupts, restarts,
@@ -1,17 +1,27 @@
1
1
  import { MemoryGraphCheckpointer } from './checkpointer.js';
2
2
  import { withLifecycle } from '../lifecycle/run.js';
3
3
  import { migrateCheckpoint, toCheckpoint } from './checkpoint-migration.js';
4
- import { GraphError, GraphNotInterruptedError, GraphThreadNotFoundError } from './errors.js';
4
+ import { tenantStore } from '../store/tenant.js';
5
+ import { GraphDrainedError, GraphError, GraphNotInterruptedError, GraphThreadNotFoundError } from './errors.js';
5
6
  export class WorkflowStepTimeoutError extends GraphError {
6
7
  stepName;
7
8
  timeoutMs;
8
- constructor(stepName, timeoutMs) {
9
- super(`Step "${stepName}" exceeded its ${timeoutMs} ms timeout`, 'WORKFLOW_STEP_TIMEOUT');
9
+ kind;
10
+ constructor(stepName, timeoutMs, kind = 'run') {
11
+ super(kind === 'idle'
12
+ ? `Step "${stepName}" made no progress for ${timeoutMs} ms`
13
+ : `Step "${stepName}" exceeded its ${timeoutMs} ms timeout`, 'WORKFLOW_STEP_TIMEOUT');
10
14
  this.stepName = stepName;
11
15
  this.timeoutMs = timeoutMs;
16
+ this.kind = kind;
12
17
  this.name = 'WorkflowStepTimeoutError';
13
18
  }
14
19
  }
20
+ class WorkflowDraining extends Error {
21
+ constructor() {
22
+ super('The workflow is draining');
23
+ }
24
+ }
15
25
  class WorkflowSuspended extends Error {
16
26
  pending;
17
27
  constructor(pending) {
@@ -40,10 +50,16 @@ export class Workflow {
40
50
  }
41
51
  stream(input, runOptions = {}) {
42
52
  const threadId = runOptions.threadId ?? generatedId();
43
- return this.execute(threadId, runOptions, async () => ({
44
- ...this.fresh(threadId, input),
45
- step: (await this.latest(threadId))?.step ?? 0,
46
- }));
53
+ return this.execute(threadId, runOptions, async () => {
54
+ const latest = await this.latest(threadId);
55
+ if (latest && runOptions.tenantId !== undefined) {
56
+ const owner = latest.metadata?.tenantId;
57
+ if (typeof owner === 'string' && owner !== runOptions.tenantId) {
58
+ throw new GraphError(`Thread "${threadId}" cannot be used by this tenant; choose another id`, 'GRAPH_VALIDATION_ERROR');
59
+ }
60
+ }
61
+ return { ...this.fresh(threadId, input), step: latest?.step ?? 0 };
62
+ });
47
63
  }
48
64
  resume(threadId, value, runOptions = {}) {
49
65
  return this.execute(threadId, runOptions, async () => {
@@ -137,11 +153,21 @@ export class Workflow {
137
153
  }
138
154
  async run(threadId, runOptions, start, queue) {
139
155
  const initial = await start();
156
+ const owner = typeof initial.metadata?.tenantId === 'string' ? initial.metadata.tenantId : undefined;
157
+ if (runOptions.tenantId !== undefined && owner !== undefined && owner !== runOptions.tenantId) {
158
+ throw new GraphThreadNotFoundError(threadId);
159
+ }
160
+ const tenantId = runOptions.tenantId ?? owner;
140
161
  const checkpointer = this.checkpointer();
141
162
  const state = structuredClone(initial.state);
142
163
  state.steps ??= {};
143
164
  const resolved = { ...initial.resolved };
144
- const metadata = { ...initial.metadata, graph: this.name, ...runOptions.metadata };
165
+ const metadata = {
166
+ ...initial.metadata,
167
+ graph: this.name,
168
+ ...runOptions.metadata,
169
+ ...(tenantId === undefined ? {} : { tenantId }),
170
+ };
145
171
  let stepNumber = initial.step;
146
172
  let writes = Promise.resolve();
147
173
  const persist = (checkpoint) => {
@@ -171,11 +197,16 @@ export class Workflow {
171
197
  const inFlight = new Set();
172
198
  const suspended = [];
173
199
  let interruptIndex = 0;
200
+ let drained = false;
174
201
  const limiter = new Limiter(this.options.maxConcurrency ?? 16);
202
+ const defaults = this.options.stepDefaults ?? {};
175
203
  const context = {
176
204
  threadId,
205
+ ...(tenantId === undefined ? {} : { tenantId }),
177
206
  signal: controller.signal,
178
- ...(this.options.store ? { store: this.options.store } : {}),
207
+ ...(this.options.store
208
+ ? { store: tenantId === undefined ? this.options.store : tenantStore(this.options.store, tenantId) }
209
+ : {}),
179
210
  step: (name, run, options = {}) => {
180
211
  const count = (counts.get(name) ?? 0) + 1;
181
212
  counts.set(name, count);
@@ -185,10 +216,22 @@ export class Workflow {
185
216
  queue.push({ type: 'step', step: stepNumber, name, key, cached: true, state: snapshot(), status: 'running' });
186
217
  return Promise.resolve(recorded.value);
187
218
  }
219
+ if (runOptions.control?.draining) {
220
+ drained = true;
221
+ return Promise.reject(new WorkflowDraining());
222
+ }
223
+ const merged = {
224
+ ...defaults,
225
+ ...options,
226
+ timeout: { ...defaults.timeout, ...options.timeout },
227
+ };
188
228
  const work = limiter
189
- .run(() => this.runStep(name, key, run, { ...this.options.retry, ...options.retry }, options, controller.signal, runOptions, stepNumber))
190
- .then(async (value) => {
191
- state.steps[key] = value === undefined ? {} : { value: structuredClone(value) };
229
+ .run(() => this.runStep(name, key, run, { ...this.options.retry, ...defaults.retry, ...options.retry }, merged, controller.signal, runOptions, stepNumber))
230
+ .then(async ({ value, recovered }) => {
231
+ state.steps[key] = {
232
+ ...(value === undefined ? {} : { value: structuredClone(value) }),
233
+ ...(recovered ? { recovered } : {}),
234
+ };
192
235
  await persist({ state: state, next: [this.name], status: 'running' });
193
236
  queue.push({
194
237
  type: 'step',
@@ -262,6 +305,18 @@ export class Workflow {
262
305
  queue.push({ type: 'interrupt', step: stepNumber, state: snapshot(), status: 'interrupted' });
263
306
  return;
264
307
  }
308
+ if (drained && outcome.kind === 'failed') {
309
+ const reason = runOptions.control?.reason;
310
+ await persist({
311
+ state: state,
312
+ next: [this.name],
313
+ status: 'interrupted',
314
+ resolved,
315
+ drained: { ...(reason ? { reason } : {}), at: this.now().toISOString() },
316
+ });
317
+ await writes;
318
+ throw new GraphDrainedError(threadId, stepNumber, reason);
319
+ }
265
320
  if (outcome.kind === 'failed') {
266
321
  const error = outcome.error;
267
322
  await persist({
@@ -284,41 +339,87 @@ export class Workflow {
284
339
  }
285
340
  async runStep(name, key, run, retry, options, signal, runOptions, step) {
286
341
  const policy = { ...DEFAULT_RETRY, ...retry };
342
+ const runMs = options.timeoutMs ?? options.timeout?.runMs;
343
+ const idleMs = options.timeout?.idleMs;
287
344
  for (let attempt = 1;; attempt += 1) {
288
345
  notify(runOptions, { type: 'task_start', step, taskId: key, node: name, attempt });
289
346
  const stepController = new AbortController();
290
347
  const forward = () => stepController.abort(signal.reason);
291
348
  signal.addEventListener('abort', forward, { once: true });
292
- let timer;
293
- let timedOut = false;
349
+ let runTimer;
350
+ let idleTimer;
351
+ let timedOut;
352
+ let expire = () => undefined;
353
+ const limits = runMs === undefined && idleMs === undefined
354
+ ? undefined
355
+ : new Promise((_, reject) => {
356
+ expire = (error) => {
357
+ if (timedOut)
358
+ return;
359
+ timedOut = error;
360
+ reject(error);
361
+ stepController.abort();
362
+ };
363
+ });
364
+ limits?.catch(() => undefined);
365
+ if (runMs !== undefined)
366
+ runTimer = setTimeout(() => expire(new WorkflowStepTimeoutError(name, runMs)), runMs);
367
+ const armIdle = () => {
368
+ if (idleMs === undefined || timedOut)
369
+ return;
370
+ if (idleTimer)
371
+ clearTimeout(idleTimer);
372
+ idleTimer = setTimeout(() => expire(new WorkflowStepTimeoutError(name, idleMs, 'idle')), idleMs);
373
+ };
374
+ armIdle();
294
375
  try {
295
- const work = Promise.resolve(run({ name, key, attempt, signal: stepController.signal }));
296
- const value = options.timeoutMs === undefined
297
- ? await work
298
- : await Promise.race([
299
- work,
300
- new Promise((_, reject) => {
301
- timer = setTimeout(() => {
302
- timedOut = true;
303
- reject(new WorkflowStepTimeoutError(name, options.timeoutMs));
304
- stepController.abort();
305
- }, options.timeoutMs);
306
- }),
307
- ]);
376
+ const work = Promise.resolve(run({ name, key, attempt, signal: stepController.signal, heartbeat: armIdle }));
377
+ const value = limits ? await Promise.race([work, limits]) : await work;
308
378
  if (timedOut)
309
- throw new WorkflowStepTimeoutError(name, options.timeoutMs);
379
+ throw timedOut;
310
380
  notify(runOptions, { type: 'task_end', step, taskId: key, node: name, update: value });
311
- return value;
381
+ return { value };
312
382
  }
313
383
  catch (error) {
314
384
  if (signal.aborted || error instanceof WorkflowSuspended)
315
385
  throw error;
316
- const failure = timedOut ? new WorkflowStepTimeoutError(name, options.timeoutMs) : error;
386
+ const failure = timedOut ?? error;
317
387
  const retryable = policy.retryOn
318
388
  ? policy.retryOn(failure, attempt)
319
- : !(failure instanceof GraphError) || timedOut;
320
- if (attempt >= Math.max(1, policy.maxAttempts) || !retryable)
321
- throw failure;
389
+ : !(failure instanceof GraphError) || timedOut !== undefined;
390
+ if (attempt >= Math.max(1, policy.maxAttempts) || !retryable) {
391
+ if (!options.onError)
392
+ throw failure;
393
+ const failed = (recovered) => notify(runOptions, {
394
+ type: 'task_failed',
395
+ step,
396
+ taskId: key,
397
+ node: name,
398
+ attempts: attempt,
399
+ error: failure instanceof Error ? failure.message : String(failure),
400
+ recovered,
401
+ });
402
+ let value;
403
+ try {
404
+ value = await options.onError({ name, key, attempts: attempt, error: failure });
405
+ }
406
+ catch (decision) {
407
+ failed(false);
408
+ throw decision;
409
+ }
410
+ failed(true);
411
+ notify(runOptions, { type: 'task_end', step, taskId: key, node: name, update: value });
412
+ return {
413
+ value,
414
+ recovered: {
415
+ error: {
416
+ name: failure instanceof Error ? failure.name : 'Error',
417
+ message: failure instanceof Error ? failure.message : String(failure),
418
+ },
419
+ attempts: attempt,
420
+ },
421
+ };
422
+ }
322
423
  notify(runOptions, {
323
424
  type: 'task_retry',
324
425
  step,
@@ -330,8 +431,10 @@ export class Workflow {
330
431
  await sleep(backoff(policy, attempt), signal);
331
432
  }
332
433
  finally {
333
- if (timer)
334
- clearTimeout(timer);
434
+ if (runTimer)
435
+ clearTimeout(runTimer);
436
+ if (idleTimer)
437
+ clearTimeout(idleTimer);
335
438
  signal.removeEventListener('abort', forward);
336
439
  }
337
440
  }
@@ -111,10 +111,17 @@ export declare class CompiledGraph<S extends ChannelSchema, I extends keyof S =
111
111
  * once the rewound checkpoint is written.
112
112
  */
113
113
  resumeFrom(threadId: string, step: number, runOptions?: GraphRunOptions): AsyncIterable<GraphStepEvent<S>>;
114
- /** Latest checkpoint, or the one at `step`. */
115
- state(threadId: string, step?: number): Promise<GraphCheckpoint<S> | undefined>;
116
- /** Checkpoints for a thread, newest first. */
117
- history(threadId: string, limit?: number): Promise<Array<GraphCheckpoint<S>>>;
114
+ /**
115
+ * Latest checkpoint, or the one at `step`. With a `tenantId`, a thread another tenant owns reads
116
+ * as absent.
117
+ */
118
+ state(threadId: string, step?: number, options?: {
119
+ tenantId?: string;
120
+ }): Promise<GraphCheckpoint<S> | undefined>;
121
+ /** Checkpoints for a thread, newest first. With a `tenantId`, another tenant's thread has none. */
122
+ history(threadId: string, limit?: number, options?: {
123
+ tenantId?: string;
124
+ }): Promise<Array<GraphCheckpoint<S>>>;
118
125
  /**
119
126
  * Wraps this graph as a node in another graph.
120
127
  *
@@ -143,17 +150,20 @@ export declare class CompiledGraph<S extends ChannelSchema, I extends keyof S =
143
150
  */
144
151
  updateState(threadId: string, update: StateUpdate<S>, options?: {
145
152
  asNode?: string;
153
+ tenantId?: string;
146
154
  }): Promise<GraphCheckpoint<S>>;
147
155
  /**
148
156
  * Copies a thread's history up to `step` into a new thread, and returns the new thread's id.
149
157
  *
150
158
  * Rewinding with `resumeFrom()` replaces the original timeline. A fork keeps it: both threads stay
151
159
  * readable and runnable, so two answers to the same question can be compared side by side. Every
152
- * copied checkpoint records `metadata.forkedFrom`.
160
+ * copied checkpoint records `metadata.forkedFrom`, and keeps the tenant of the thread it came from.
161
+ * With a `tenantId`, another tenant's thread is not found.
153
162
  */
154
163
  fork(threadId: string, options?: {
155
164
  step?: number;
156
165
  threadId?: string;
166
+ tenantId?: string;
157
167
  }): Promise<string>;
158
168
  /**
159
169
  * Runs the supersteps, as one operation of the compiled lifecycle when there is one. The run is
@@ -1,5 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { Command, END, Send, START } from '../types/graph.js';
3
+ import { tenantStore } from '../store/tenant.js';
4
+ import { linkSignals } from '../utils/signals.js';
3
5
  import { MemoryGraphCheckpointer } from './checkpointer.js';
4
6
  import { migrateCheckpoint, toCheckpoint } from './checkpoint-migration.js';
5
7
  import { CheckpointWriter } from './durability.js';
@@ -150,6 +152,12 @@ export class CompiledGraph {
150
152
  const threadId = resolveThreadId(runOptions.threadId);
151
153
  return this.run(threadId, runOptions, async () => {
152
154
  this.assertInput(input);
155
+ if (runOptions.tenantId !== undefined) {
156
+ const existing = await this.checkpointer()?.get(threadId);
157
+ if (existing && !ownedBy(existing, runOptions.tenantId)) {
158
+ throw new GraphValidationError(`Thread "${threadId}" cannot be used by this tenant; choose another id`);
159
+ }
160
+ }
153
161
  const state = this.seedState(input);
154
162
  const next = this.entryNodes();
155
163
  return { threadId, step: 0, state, next, status: 'running', createdAt: this.now().toISOString() };
@@ -166,16 +174,19 @@ export class CompiledGraph {
166
174
  return this.eventStream(threadId, options, (runOptions) => this.continue(threadId, runOptions));
167
175
  }
168
176
  eventStream(threadId, options, run) {
169
- return new GraphEventStream((onEvent, signal) => run({
170
- ...options,
171
- threadId,
172
- signal: options.signal ? AbortSignal.any([options.signal, signal]) : signal,
173
- ...(options.subgraphs ? { subgraphEvents: true } : {}),
174
- onEvent: (event) => {
175
- options.onEvent?.(event);
176
- onEvent(event);
177
- },
178
- }), (last) => this.toResult(threadId, last), streamSettings(options));
177
+ return new GraphEventStream((onEvent, signal) => {
178
+ const linked = linkSignals([options.signal, signal]);
179
+ return releasing(run({
180
+ ...options,
181
+ threadId,
182
+ signal: linked.signal,
183
+ ...(options.subgraphs ? { subgraphEvents: true } : {}),
184
+ onEvent: (event) => {
185
+ options.onEvent?.(event);
186
+ onEvent(event);
187
+ },
188
+ }), linked.dispose);
189
+ }, (last) => this.toResult(threadId, last), streamSettings(options));
179
190
  }
180
191
  resume(threadId, value, runOptions = {}) {
181
192
  return this.run(threadId, runOptions, async () => {
@@ -237,12 +248,16 @@ export class CompiledGraph {
237
248
  return { ...migrateCheckpoint(stored), status: 'running' };
238
249
  });
239
250
  }
240
- async state(threadId, step) {
251
+ async state(threadId, step, options = {}) {
241
252
  const stored = await this.checkpointer()?.get(threadId, step);
242
- return stored ? migrateCheckpoint(stored) : undefined;
253
+ if (!stored || !ownedBy(stored, options.tenantId))
254
+ return undefined;
255
+ return migrateCheckpoint(stored);
243
256
  }
244
- async history(threadId, limit) {
257
+ async history(threadId, limit, options = {}) {
245
258
  const stored = (await this.checkpointer()?.history(threadId, limit)) ?? [];
259
+ if (stored[0] && !ownedBy(stored[0], options.tenantId))
260
+ return [];
246
261
  return stored.map((item) => migrateCheckpoint(item));
247
262
  }
248
263
  asNode() {
@@ -346,7 +361,7 @@ export class CompiledGraph {
346
361
  };
347
362
  }
348
363
  async updateState(threadId, update, options = {}) {
349
- const checkpoint = await this.requireCheckpoint(threadId);
364
+ const checkpoint = await this.requireCheckpoint(threadId, options.tenantId);
350
365
  const state = this.reduce(checkpoint.state, [update]);
351
366
  const createdAt = this.now().toISOString();
352
367
  if (!options.asNode) {
@@ -381,8 +396,9 @@ export class CompiledGraph {
381
396
  if (!store)
382
397
  throw new GraphValidationError('fork() needs a checkpointer to copy history from');
383
398
  const history = (await store.history(threadId, Number.MAX_SAFE_INTEGER)).map((item) => migrateCheckpoint(item));
384
- if (history.length === 0)
399
+ if (history.length === 0 || !ownedBy(history[0], options.tenantId)) {
385
400
  throw new GraphThreadNotFoundError(threadId);
401
+ }
386
402
  const cutoff = options.step ?? history[0].step;
387
403
  const lineage = history.filter((item) => item.step <= cutoff).reverse();
388
404
  if (lineage.at(-1)?.step !== cutoff) {
@@ -460,6 +476,16 @@ export class CompiledGraph {
460
476
  const stopBefore = runOptions.interruptBefore ?? this.options.interruptBefore ?? [];
461
477
  const stopAfter = runOptions.interruptAfter ?? this.options.interruptAfter ?? [];
462
478
  let checkpoint = await seed();
479
+ const owner = tenantOf(checkpoint);
480
+ if (runOptions.tenantId !== undefined && owner !== undefined && owner !== runOptions.tenantId) {
481
+ throw new GraphThreadNotFoundError(threadId);
482
+ }
483
+ const tenantId = runOptions.tenantId ?? owner;
484
+ if (tenantId !== undefined) {
485
+ runOptions = { ...runOptions, tenantId, metadata: { ...runOptions.metadata, tenantId } };
486
+ if (owner === undefined)
487
+ checkpoint = { ...checkpoint, metadata: { ...checkpoint.metadata, tenantId } };
488
+ }
463
489
  let passBreakpoint = checkpoint.breakpoint?.when === 'before';
464
490
  if (checkpoint.breakpoint)
465
491
  checkpoint = { ...checkpoint, breakpoint: undefined };
@@ -670,8 +696,8 @@ export class CompiledGraph {
670
696
  attempts += 1;
671
697
  notify(runOptions, { type: 'task_start', step, taskId: task.id, node: task.node, attempt: attempts });
672
698
  const timer = attemptTimer(task.node, limits);
673
- const sources = [stepSignal, ...(runOptions.signal ? [runOptions.signal] : []), ...(timer ? [timer.signal] : [])];
674
- const signal = sources.length === 1 ? sources[0] : AbortSignal.any(sources);
699
+ const linked = linkSignals([stepSignal, runOptions.signal, timer?.signal]);
700
+ const signal = linked.signal;
675
701
  try {
676
702
  const running = this.runNode(definition.fn, task, step, threadId, checkpoint, runOptions, signal, attempts, timer?.touch);
677
703
  if (timer)
@@ -743,6 +769,7 @@ export class CompiledGraph {
743
769
  }
744
770
  finally {
745
771
  timer?.stop();
772
+ linked.dispose();
746
773
  }
747
774
  }
748
775
  }
@@ -822,11 +849,18 @@ export class CompiledGraph {
822
849
  node,
823
850
  step,
824
851
  threadId,
852
+ ...(runOptions.tenantId !== undefined ? { tenantId: runOptions.tenantId } : {}),
825
853
  taskId: task.id,
826
854
  input: task.input,
827
855
  attempt,
828
856
  signal,
829
- ...(this.options.store ? { store: this.options.store } : {}),
857
+ ...(this.options.store
858
+ ? {
859
+ store: runOptions.tenantId !== undefined
860
+ ? tenantStore(this.options.store, runOptions.tenantId)
861
+ : this.options.store,
862
+ }
863
+ : {}),
830
864
  interrupt: (request) => {
831
865
  const index = interruptIndex++;
832
866
  const key = interruptKey({ node, taskId: task.id, step, index });
@@ -976,9 +1010,9 @@ export class CompiledGraph {
976
1010
  }
977
1011
  return this.reduce(state, [input]);
978
1012
  }
979
- async requireCheckpoint(threadId) {
1013
+ async requireCheckpoint(threadId, tenantId) {
980
1014
  const checkpoint = await this.checkpointer()?.get(threadId);
981
- if (!checkpoint)
1015
+ if (!checkpoint || !ownedBy(checkpoint, tenantId))
982
1016
  throw new GraphThreadNotFoundError(threadId);
983
1017
  return migrateCheckpoint(checkpoint);
984
1018
  }
@@ -1180,3 +1214,21 @@ function attemptTimer(node, limits) {
1180
1214
  function isRetryable(error) {
1181
1215
  return !(error instanceof GraphValidationError);
1182
1216
  }
1217
+ function tenantOf(checkpoint) {
1218
+ const tenantId = checkpoint.metadata?.tenantId;
1219
+ return typeof tenantId === 'string' ? tenantId : undefined;
1220
+ }
1221
+ function ownedBy(checkpoint, tenantId) {
1222
+ if (tenantId === undefined)
1223
+ return true;
1224
+ const owner = tenantOf(checkpoint);
1225
+ return owner === undefined || owner === tenantId;
1226
+ }
1227
+ async function* releasing(source, release) {
1228
+ try {
1229
+ yield* source;
1230
+ }
1231
+ finally {
1232
+ release();
1233
+ }
1234
+ }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { linkSignals } from '../utils/signals.js';
2
3
  import { ImageValidationError } from './errors.js';
3
4
  export class MemoryReviewQueue {
4
5
  items = [];
@@ -32,7 +33,8 @@ export class MediaEvalRunner {
32
33
  const runsOf = new Map(examples.map((example, index) => [example.id, runCounts[index]]));
33
34
  const outcomes = new Map();
34
35
  const stop = new AbortController();
35
- const signal = runOptions.signal ? AbortSignal.any([stop.signal, runOptions.signal]) : stop.signal;
36
+ const linked = linkSignals([stop.signal, runOptions.signal]);
37
+ const signal = linked.signal;
36
38
  const { evaluate } = await import('../evaluate/run.js');
37
39
  const experiment = await evaluate(async (evalCase, { example, run }) => {
38
40
  try {
@@ -57,7 +59,7 @@ export class MediaEvalRunner {
57
59
  signal,
58
60
  ...(runOptions.store ? { store: runOptions.store } : {}),
59
61
  ...(runOptions.metadata ? { metadata: runOptions.metadata } : {}),
60
- });
62
+ }).finally(linked.dispose);
61
63
  const caseReports = [];
62
64
  const allScores = {};
63
65
  const safety = { truePositives: 0, falsePositives: 0, trueNegatives: 0, falseNegatives: 0 };