nexus-ai-pro 1.7.0 → 1.8.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.
package/API_STABILITY.md CHANGED
@@ -225,6 +225,28 @@ guarantee about how the registry is maintained, not a runtime surface: the resol
225
225
  `KNOWN_MODELS`, and a test asserts the two match. Switching the resolver over is a later change, so
226
226
  that introducing generation cannot alter pricing behavior in the same release.
227
227
 
228
+ ## Graph API stage
229
+
230
+ `createGraph`, `StateGraph`, `CompiledGraph`, the channel constructors, both checkpointers, the error
231
+ classes, and the `nexus-ai-pro/graph` subpath are public and follow the 1.x rules, as are the
232
+ normalized checkpoint, result, and step-event shapes.
233
+
234
+ Four behaviors are guaranteed. A superstep is atomic with respect to checkpointing: state is reduced
235
+ and written before the next set of nodes is computed. A node that finished is never re-run by a
236
+ resume, so a completed branch's side effect happens once. `interrupt()` returns the supplied value on
237
+ replay rather than throwing again, keyed by node, step, and position, so a node may ask more than one
238
+ question. And `GraphCheckpoint` stays JSON-serializable, because a checkpoint that cannot be written
239
+ to Redis is not a checkpoint.
240
+
241
+ `GraphCheckpointer` is deliberately not generic over the channel schema: it persists opaque state,
242
+ and threading the schema through it would force an annotation on every construction for no benefit.
243
+ The graph casts at that boundary and hands typed state back through `state()` and `history()`.
244
+
245
+ Scheduling within a superstep is not a guarantee. Nodes in a fan-out currently run in edge order and
246
+ sequentially; running them concurrently is a valid future change, so a node must not depend on
247
+ observing another node's write within the same superstep — that is what channels are for. The default
248
+ `maxSteps` may change; pass it explicitly when a run depends on the bound.
249
+
228
250
  ## Deprecation process
229
251
 
230
252
  Deprecated APIs are marked with `@deprecated` in declarations and described in the changelog. Removals
package/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [1.8.0] - 2026-09-14
10
+
11
+ Graphs. Nodes, edges, cycles, fan-out and subgraphs over typed state, with every superstep
12
+ checkpointed so a run is resumable, inspectable and interruptible by construction rather than
13
+ after opting in. This closes the five capability rows where LangGraph led, on a subpath that
14
+ costs 5% of the root import.
15
+
16
+ ### Added
17
+
18
+ - **Typed state graphs.** `nexus-ai-pro/graph` adds nodes, edges, conditional edges, cycles, fan-out,
19
+ and subgraphs over typed state channels, closing the five capability rows where LangGraph led:
20
+ explicit nodes and edges, persistent graph state, checkpoint and resume, human-in-the-loop
21
+ interrupts, and complex branching.
22
+ - **Durable by construction.** Every superstep is checkpointed, so a run is resumable without opting
23
+ into a checkpointer first. `MemoryGraphCheckpointer` covers a single process;
24
+ `OperationStoreCheckpointer` persists through the `OperationStore` that already backs durable
25
+ operations, which makes a Redis-backed thread a one-line change and lets a different worker resume
26
+ what another suspended. The store is imported as a type only, so the graph subpath stays small.
27
+ - **Human in the loop.** A node calls `context.interrupt()`; the graph checkpoints and reports
28
+ `awaiting_input`, and `resume()` supplies the value. On replay the same call returns that value
29
+ instead of throwing, keyed by node, step, and position, so one node can ask several questions
30
+ across several resumes. A sibling branch that already completed is not re-run.
31
+ - **Channels with reducers** — `lastValue`, `appendList`, `appendSet`, `mergeObject`, `counter`, and
32
+ `reducerChannel` — so two branches writing the same slot in one superstep combine rather than
33
+ clobber.
34
+ - **Time travel.** `state()`, `history()`, and `resumeFrom(step)` read and rewind a thread.
35
+ - **Compile-time validation.** An edge to an unknown node, a node nothing routes to, a duplicate node
36
+ name, and a reserved name are all refused at `compile()` rather than at run time.
37
+ - New subpath `nexus-ai-pro/graph`. It is deliberately absent from the root import, so a user who
38
+ does not build graphs pays nothing for it.
39
+
7
40
  ## [1.7.0] - 2026-09-08
8
41
 
9
42
  Batch economics, distributed limits, and durable assets: the rest of the theme *work that
@@ -397,7 +430,8 @@ shared policy vocabulary is in place for a future release that revisits this.
397
430
  - URL fetching rejects unsafe private-network targets and limits response reads.
398
431
  - CLI and audit findings no longer disclose detected secret values.
399
432
 
400
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.7.0...HEAD
433
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.8.0...HEAD
434
+ [1.8.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.7.0...v1.8.0
401
435
  [1.7.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.6.0...v1.7.0
402
436
  [1.6.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.5.0...v1.6.0
403
437
  [1.5.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.4.0...v1.5.0
package/README.md CHANGED
@@ -85,6 +85,7 @@ You can still pass a plain `NexusAIConfig` to `new NexusAI(...)` when you want f
85
85
  - add tools, agents, RAG context, evals, batch jobs, and queues
86
86
  - embed text through the same routing, caching, batching, budget, retry, and metrics as a completion
87
87
  - run long operations that survive a restart, with leases, retries, dead-lettering, and signed webhooks
88
+ - build stateful graphs with cycles, fan-out, subgraphs, human approval, and resumable checkpoints
88
89
  - trip routing away from a failing provider, and share one rate-limit budget across workers
89
90
  - reach the providers' half-price asynchronous batch tier behind one operation handle
90
91
  - persist generated media to disk or S3 with tenant isolation, retention, and checksums
@@ -421,6 +422,90 @@ const response = await ai.completeVerified(
421
422
  );
422
423
  ```
423
424
 
425
+ ## Graphs
426
+
427
+ A graph is nodes, edges, and typed state. Cycles, fan-out, subgraphs, and human approval are
428
+ supported shapes rather than workarounds, and every run is checkpointed, so it is resumable by
429
+ construction rather than after configuring a checkpointer.
430
+
431
+ ```ts
432
+ import { createGraph, appendList, counter, END, MemoryGraphCheckpointer } from 'nexus-ai-pro/graph';
433
+
434
+ const graph = createGraph({
435
+ channels: { messages: appendList<string>(), turns: counter() },
436
+ })
437
+ .addNode('research', async (ctx) => ({ messages: [await search(ctx.state.messages)], turns: 1 }))
438
+ .addNode('answer', async (ctx) => ({ messages: [await ai.complete(...).then((r) => r.content)] }))
439
+ .setEntry('research')
440
+ .addConditionalEdges('research', (state) => (state.turns >= 3 ? 'answer' : 'research'))
441
+ .addEdge('answer', END)
442
+ .compile({ checkpointer: new MemoryGraphCheckpointer() });
443
+
444
+ const result = await graph.invoke({ messages: ['who won?'] }, { threadId: 'q-42' });
445
+ ```
446
+
447
+ **Channels, not assignment.** Each state slot declares how writes combine — `lastValue`,
448
+ `appendList`, `appendSet`, `mergeObject`, `counter`, or your own `reducerChannel`. That is what makes
449
+ fan-out safe: two branches running in the same superstep can both write, and the channel decides
450
+ whether that means overwrite, append, or sum. Assignment would silently drop one branch's work.
451
+
452
+ **Cycles are first-class**, so `maxSteps` is what stands between a mistaken router and an infinite
453
+ loop. Exceeding it names the nodes still pending rather than hanging.
454
+
455
+ **Human in the loop.** A node calls `interrupt()`; the graph checkpoints and stops:
456
+
457
+ ```ts
458
+ .addNode('approve', (ctx) => ({
459
+ approved: ctx.interrupt<boolean>({ reason: 'Publish this draft?', payload: { chars: 1200 } }),
460
+ }))
461
+ ```
462
+
463
+ ```ts
464
+ const run = await graph.invoke(input, { threadId });
465
+ if (run.status === 'awaiting_input') {
466
+ // Hours later, in another process:
467
+ await graph.resumeWith(threadId, true);
468
+ }
469
+ ```
470
+
471
+ The interrupted node runs again from the top and `interrupt()` returns the supplied value instead of
472
+ throwing, so the node body reads as straight-line code either way. Keep the work before an interrupt
473
+ cheap, because it happens twice. A sibling branch that already finished is **not** re-run — its
474
+ writes were checkpointed before the graph suspended.
475
+
476
+ **Time travel and inspection.** Every superstep is a checkpoint:
477
+
478
+ ```ts
479
+ await graph.state(threadId); // latest checkpoint
480
+ await graph.history(threadId); // newest first
481
+ graph.resumeFrom(threadId, 3); // rewind and run forward
482
+ ```
483
+
484
+ **Durable by construction.** The default checkpointer is in-process. Point it at the operation store
485
+ that already backs durable operations and a thread survives a restart — a different worker resumes
486
+ what another suspended:
487
+
488
+ ```ts
489
+ import { OperationStoreCheckpointer } from 'nexus-ai-pro/graph';
490
+ import { RedisOperationStore } from 'nexus-ai-pro/operations/adapters';
491
+
492
+ const checkpointer = new OperationStoreCheckpointer(new RedisOperationStore(redis));
493
+ ```
494
+
495
+ Writes are compare-and-set on the record's sequence, so two workers advancing the same thread cannot
496
+ both win.
497
+
498
+ **Subgraphs.** A compiled graph is a node:
499
+
500
+ ```ts
501
+ parent.addNode('research', researchGraph.asNode());
502
+ ```
503
+
504
+ Channels shared by name are passed in and merged back; anything the parent does not declare stays
505
+ private to the subgraph.
506
+
507
+ The graph is not in the root import. It costs nothing to a user who does not build graphs.
508
+
424
509
  ## Provider Batch Tiers
425
510
 
426
511
  Both OpenAI and Anthropic sell an asynchronous tier at roughly half price, in exchange for a
@@ -428,6 +513,7 @@ completion window measured in hours. Local `runBatch()` concurrency cannot reach
428
513
  different API. `BatchManager` puts both behind one operation handle.
429
514
 
430
515
  ```ts
516
+ import { createGraph } from 'nexus-ai-pro/graph';
431
517
  import { BatchManager } from 'nexus-ai-pro/batch';
432
518
  import { OpenAIBatchProvider } from 'nexus-ai-pro/batch/openai';
433
519
 
@@ -0,0 +1,19 @@
1
+ import type { Channel } from '../types/graph.js';
2
+ /**
3
+ * The last write wins.
4
+ *
5
+ * The right default for a scalar a node computes fresh each time. Under fan-out the winner is
6
+ * whichever branch reduced last, so prefer an accumulating channel when several branches write the
7
+ * same slot and the outcome matters.
8
+ */
9
+ export declare function lastValue<T>(initial?: T): Channel<T>;
10
+ /** Concatenates every write, which is what a message history or an event log wants. */
11
+ export declare function appendList<T>(): Channel<T[]>;
12
+ /** Shallow-merges object writes, so two branches can each contribute their own keys. */
13
+ export declare function mergeObject<T extends Record<string, unknown>>(): Channel<T>;
14
+ /** Sums numeric writes, for a counter several branches increment. */
15
+ export declare function counter(initial?: number): Channel<number>;
16
+ /** Keeps distinct values in first-seen order. */
17
+ export declare function appendSet<T>(): Channel<T[]>;
18
+ /** Builds a channel from a plain reducer, for a rule none of the built-ins covers. */
19
+ export declare function reducerChannel<T>(reduce: (current: T | undefined, update: T) => T, initial?: () => T): Channel<T>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The last write wins.
3
+ *
4
+ * The right default for a scalar a node computes fresh each time. Under fan-out the winner is
5
+ * whichever branch reduced last, so prefer an accumulating channel when several branches write the
6
+ * same slot and the outcome matters.
7
+ */
8
+ export function lastValue(initial) {
9
+ return {
10
+ reduce: (_current, update) => update,
11
+ ...(initial === undefined ? {} : { initial: () => initial }),
12
+ };
13
+ }
14
+ /** Concatenates every write, which is what a message history or an event log wants. */
15
+ export function appendList() {
16
+ return {
17
+ reduce: (current, update) => [...(current ?? []), ...update],
18
+ initial: () => [],
19
+ };
20
+ }
21
+ /** Shallow-merges object writes, so two branches can each contribute their own keys. */
22
+ export function mergeObject() {
23
+ return {
24
+ reduce: (current, update) => ({ ...(current ?? {}), ...update }),
25
+ initial: () => ({}),
26
+ };
27
+ }
28
+ /** Sums numeric writes, for a counter several branches increment. */
29
+ export function counter(initial = 0) {
30
+ return {
31
+ reduce: (current, update) => (current ?? initial) + update,
32
+ initial: () => initial,
33
+ };
34
+ }
35
+ /** Keeps distinct values in first-seen order. */
36
+ export function appendSet() {
37
+ return {
38
+ reduce: (current, update) => {
39
+ const seen = new Set(current ?? []);
40
+ const merged = [...(current ?? [])];
41
+ for (const value of update) {
42
+ if (!seen.has(value)) {
43
+ seen.add(value);
44
+ merged.push(value);
45
+ }
46
+ }
47
+ return merged;
48
+ },
49
+ initial: () => [],
50
+ };
51
+ }
52
+ /** Builds a channel from a plain reducer, for a rule none of the built-ins covers. */
53
+ export function reducerChannel(reduce, initial) {
54
+ return { reduce, ...(initial ? { initial } : {}) };
55
+ }
@@ -0,0 +1,62 @@
1
+ import type { GraphCheckpoint, GraphCheckpointer } from '../types/graph.js';
2
+ import type { OperationRecord, OperationStore } from '../types/operations.js';
3
+ export interface MemoryGraphCheckpointerOptions {
4
+ /** Checkpoints kept per thread, newest first. Defaults to 50. */
5
+ maxPerThread?: number;
6
+ }
7
+ /**
8
+ * In-process checkpoint history.
9
+ *
10
+ * The default, and enough for a single process that wants interrupts, resume, and time travel
11
+ * without a store. It does not survive a restart; `OperationStoreCheckpointer` does.
12
+ */
13
+ export declare class MemoryGraphCheckpointer implements GraphCheckpointer {
14
+ private readonly threads;
15
+ private readonly maxPerThread;
16
+ constructor(options?: MemoryGraphCheckpointerOptions);
17
+ put(checkpoint: GraphCheckpoint): void;
18
+ get(threadId: string, step?: number): GraphCheckpoint | undefined;
19
+ history(threadId: string, limit?: number): GraphCheckpoint[];
20
+ delete(threadId: string): void;
21
+ /** Thread ids this checkpointer knows about. */
22
+ threadIds(): string[];
23
+ }
24
+ export interface OperationStoreCheckpointerOptions {
25
+ /** Checkpoints kept per thread. Defaults to 50. */
26
+ maxPerThread?: number;
27
+ }
28
+ /**
29
+ * Persists checkpoints through the `OperationStore` that already backs durable operations.
30
+ *
31
+ * This is what makes a graph durable without the graph module depending on the operations runtime:
32
+ * the store is imported as a type only, so `nexus-ai-pro/graph` stays small, and handing it a
33
+ * `RedisOperationStore` is all it takes to make a thread survive a restart.
34
+ *
35
+ * Each checkpoint is one record at `<threadId>#<step>`, with a head record at `<threadId>` naming
36
+ * the latest step. Two records per superstep, in exchange for time travel and a resume point that
37
+ * another process can read.
38
+ */
39
+ export declare class OperationStoreCheckpointer implements GraphCheckpointer {
40
+ private readonly store;
41
+ private readonly maxPerThread;
42
+ constructor(store: OperationStore<GraphCheckpoint | ThreadHead>, options?: OperationStoreCheckpointerOptions);
43
+ put(checkpoint: GraphCheckpoint): Promise<void>;
44
+ get(threadId: string, step?: number): Promise<GraphCheckpoint | undefined>;
45
+ history(threadId: string, limit?: number): Promise<GraphCheckpoint[]>;
46
+ delete(threadId: string): Promise<void>;
47
+ private readHead;
48
+ /**
49
+ * Writes one record, creating it when absent and otherwise updating in place.
50
+ *
51
+ * `update` is a compare-and-set on the record's sequence, so two workers advancing the same
52
+ * thread cannot both win; the loser simply re-reads on its next step.
53
+ */
54
+ private write;
55
+ }
56
+ interface ThreadHead {
57
+ kind: 'graph-thread';
58
+ latest: number;
59
+ steps: number[];
60
+ }
61
+ /** Re-exported for the adapter's signature, so a caller need not import the operations types. */
62
+ export type { OperationRecord, OperationStore };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * In-process checkpoint history.
3
+ *
4
+ * The default, and enough for a single process that wants interrupts, resume, and time travel
5
+ * without a store. It does not survive a restart; `OperationStoreCheckpointer` does.
6
+ */
7
+ export class MemoryGraphCheckpointer {
8
+ threads = new Map();
9
+ maxPerThread;
10
+ constructor(options = {}) {
11
+ this.maxPerThread = options.maxPerThread ?? 50;
12
+ }
13
+ put(checkpoint) {
14
+ const list = this.threads.get(checkpoint.threadId) ?? [];
15
+ // A resumed run can rewrite a step it already wrote, so replace rather than append blindly.
16
+ const existing = list.findIndex((item) => item.step === checkpoint.step);
17
+ if (existing >= 0)
18
+ list.splice(existing, 1);
19
+ list.push(clone(checkpoint));
20
+ list.sort((a, b) => a.step - b.step);
21
+ while (list.length > this.maxPerThread)
22
+ list.shift();
23
+ this.threads.set(checkpoint.threadId, list);
24
+ }
25
+ get(threadId, step) {
26
+ const list = this.threads.get(threadId);
27
+ if (!list?.length)
28
+ return undefined;
29
+ const found = step === undefined ? list[list.length - 1] : list.find((item) => item.step === step);
30
+ return found ? clone(found) : undefined;
31
+ }
32
+ history(threadId, limit = 20) {
33
+ const list = this.threads.get(threadId) ?? [];
34
+ return [...list].reverse().slice(0, limit).map(clone);
35
+ }
36
+ delete(threadId) {
37
+ this.threads.delete(threadId);
38
+ }
39
+ /** Thread ids this checkpointer knows about. */
40
+ threadIds() {
41
+ return [...this.threads.keys()];
42
+ }
43
+ }
44
+ /**
45
+ * Persists checkpoints through the `OperationStore` that already backs durable operations.
46
+ *
47
+ * This is what makes a graph durable without the graph module depending on the operations runtime:
48
+ * the store is imported as a type only, so `nexus-ai-pro/graph` stays small, and handing it a
49
+ * `RedisOperationStore` is all it takes to make a thread survive a restart.
50
+ *
51
+ * Each checkpoint is one record at `<threadId>#<step>`, with a head record at `<threadId>` naming
52
+ * the latest step. Two records per superstep, in exchange for time travel and a resume point that
53
+ * another process can read.
54
+ */
55
+ export class OperationStoreCheckpointer {
56
+ store;
57
+ maxPerThread;
58
+ constructor(store, options = {}) {
59
+ this.store = store;
60
+ this.maxPerThread = options.maxPerThread ?? 50;
61
+ }
62
+ async put(checkpoint) {
63
+ await this.write(stepId(checkpoint.threadId, checkpoint.step), checkpoint);
64
+ const head = await this.readHead(checkpoint.threadId);
65
+ const steps = [...new Set([...(head?.steps ?? []), checkpoint.step])].sort((a, b) => a - b);
66
+ while (steps.length > this.maxPerThread) {
67
+ const dropped = steps.shift();
68
+ await this.store.delete?.(stepId(checkpoint.threadId, dropped));
69
+ }
70
+ await this.write(checkpoint.threadId, { kind: 'graph-thread', latest: checkpoint.step, steps });
71
+ }
72
+ async get(threadId, step) {
73
+ const target = step ?? (await this.readHead(threadId))?.latest;
74
+ if (target === undefined)
75
+ return undefined;
76
+ const record = await this.store.read(stepId(threadId, target));
77
+ return record?.result && !isHead(record.result) ? record.result : undefined;
78
+ }
79
+ async history(threadId, limit = 20) {
80
+ const head = await this.readHead(threadId);
81
+ if (!head)
82
+ return [];
83
+ const wanted = [...head.steps].reverse().slice(0, limit);
84
+ const found = [];
85
+ for (const step of wanted) {
86
+ const checkpoint = await this.get(threadId, step);
87
+ if (checkpoint)
88
+ found.push(checkpoint);
89
+ }
90
+ return found;
91
+ }
92
+ async delete(threadId) {
93
+ const head = await this.readHead(threadId);
94
+ for (const step of head?.steps ?? [])
95
+ await this.store.delete?.(stepId(threadId, step));
96
+ await this.store.delete?.(threadId);
97
+ }
98
+ async readHead(threadId) {
99
+ const record = await this.store.read(threadId);
100
+ return record?.result && isHead(record.result) ? record.result : undefined;
101
+ }
102
+ /**
103
+ * Writes one record, creating it when absent and otherwise updating in place.
104
+ *
105
+ * `update` is a compare-and-set on the record's sequence, so two workers advancing the same
106
+ * thread cannot both win; the loser simply re-reads on its next step.
107
+ */
108
+ async write(id, value) {
109
+ const existing = await this.store.read(id);
110
+ const now = new Date().toISOString();
111
+ if (!existing) {
112
+ await this.store.create({
113
+ id,
114
+ status: 'succeeded',
115
+ attempt: 1,
116
+ maxAttempts: 1,
117
+ sequence: 0,
118
+ createdAt: now,
119
+ updatedAt: now,
120
+ kind: 'graph.checkpoint',
121
+ result: value,
122
+ });
123
+ return;
124
+ }
125
+ await this.store.update({ ...existing, result: value, sequence: existing.sequence + 1, updatedAt: now }, existing.sequence);
126
+ }
127
+ }
128
+ function isHead(value) {
129
+ return typeof value === 'object' && value !== null && value.kind === 'graph-thread';
130
+ }
131
+ function stepId(threadId, step) {
132
+ return `${threadId}#${step}`;
133
+ }
134
+ function clone(value) {
135
+ return structuredClone(value);
136
+ }
@@ -0,0 +1,50 @@
1
+ import type { InterruptRequest, PendingInterrupt } from '../types/graph.js';
2
+ export declare class GraphError extends Error {
3
+ readonly code: string;
4
+ readonly cause?: unknown | undefined;
5
+ constructor(message: string, code: string, cause?: unknown | undefined);
6
+ }
7
+ export declare class GraphValidationError extends GraphError {
8
+ constructor(message: string, cause?: unknown);
9
+ }
10
+ /**
11
+ * Thrown by `context.interrupt()` to suspend the graph.
12
+ *
13
+ * Control flow, not a failure: the runtime catches it, checkpoints, and reports `awaiting_input`.
14
+ * It escapes to the caller only when a node interrupts on a graph compiled without a checkpointer,
15
+ * where there would be nothing to resume from.
16
+ */
17
+ export declare class GraphInterrupt extends GraphError {
18
+ readonly request: InterruptRequest;
19
+ readonly node: string;
20
+ readonly step: number;
21
+ readonly index: number;
22
+ constructor(request: InterruptRequest, node: string, step: number, index: number);
23
+ }
24
+ /**
25
+ * Raised when a run exceeds its superstep budget.
26
+ *
27
+ * Cycles are a supported shape, so this is the only thing standing between a mistaken router and an
28
+ * infinite loop. The message names what was still pending.
29
+ */
30
+ export declare class GraphStepLimitError extends GraphError {
31
+ readonly maxSteps: number;
32
+ readonly pending: readonly string[];
33
+ constructor(maxSteps: number, pending: readonly string[]);
34
+ }
35
+ export declare class GraphNodeError extends GraphError {
36
+ readonly node: string;
37
+ readonly step: number;
38
+ constructor(node: string, step: number, cause: unknown);
39
+ }
40
+ export declare class GraphThreadNotFoundError extends GraphError {
41
+ readonly threadId: string;
42
+ constructor(threadId: string);
43
+ }
44
+ export declare class GraphNotInterruptedError extends GraphError {
45
+ readonly threadId: string;
46
+ readonly status: string;
47
+ constructor(threadId: string, status: string);
48
+ }
49
+ /** Stable key for one interrupt, so a replayed node picks up the answer it was given. */
50
+ export declare function interruptKey(pending: Pick<PendingInterrupt, 'node' | 'step' | 'index'>): string;
@@ -0,0 +1,85 @@
1
+ export class GraphError extends Error {
2
+ code;
3
+ cause;
4
+ constructor(message, code, cause) {
5
+ super(message);
6
+ this.code = code;
7
+ this.cause = cause;
8
+ this.name = 'GraphError';
9
+ }
10
+ }
11
+ export class GraphValidationError extends GraphError {
12
+ constructor(message, cause) {
13
+ super(message, 'GRAPH_VALIDATION_ERROR', cause);
14
+ this.name = 'GraphValidationError';
15
+ }
16
+ }
17
+ /**
18
+ * Thrown by `context.interrupt()` to suspend the graph.
19
+ *
20
+ * Control flow, not a failure: the runtime catches it, checkpoints, and reports `awaiting_input`.
21
+ * It escapes to the caller only when a node interrupts on a graph compiled without a checkpointer,
22
+ * where there would be nothing to resume from.
23
+ */
24
+ export class GraphInterrupt extends GraphError {
25
+ request;
26
+ node;
27
+ step;
28
+ index;
29
+ constructor(request, node, step, index) {
30
+ super(`Graph interrupted at "${node}": ${request.reason}`, 'GRAPH_INTERRUPT');
31
+ this.request = request;
32
+ this.node = node;
33
+ this.step = step;
34
+ this.index = index;
35
+ this.name = 'GraphInterrupt';
36
+ }
37
+ }
38
+ /**
39
+ * Raised when a run exceeds its superstep budget.
40
+ *
41
+ * Cycles are a supported shape, so this is the only thing standing between a mistaken router and an
42
+ * infinite loop. The message names what was still pending.
43
+ */
44
+ export class GraphStepLimitError extends GraphError {
45
+ maxSteps;
46
+ pending;
47
+ constructor(maxSteps, pending) {
48
+ super(`Graph exceeded ${maxSteps} supersteps without reaching END. Pending: ${pending.join(', ') || 'none'}. Raise maxSteps, or check that a conditional edge eventually routes to END.`, 'GRAPH_STEP_LIMIT');
49
+ this.maxSteps = maxSteps;
50
+ this.pending = pending;
51
+ this.name = 'GraphStepLimitError';
52
+ }
53
+ }
54
+ export class GraphNodeError extends GraphError {
55
+ node;
56
+ step;
57
+ constructor(node, step, cause) {
58
+ super(`Graph node "${node}" failed at step ${step}: ${cause instanceof Error ? cause.message : String(cause)}`, 'GRAPH_NODE_ERROR', cause);
59
+ this.node = node;
60
+ this.step = step;
61
+ this.name = 'GraphNodeError';
62
+ }
63
+ }
64
+ export class GraphThreadNotFoundError extends GraphError {
65
+ threadId;
66
+ constructor(threadId) {
67
+ super(`No checkpoint for thread "${threadId}". A thread is resumable only when the graph was compiled with a checkpointer and run with an explicit threadId.`, 'GRAPH_THREAD_NOT_FOUND');
68
+ this.threadId = threadId;
69
+ this.name = 'GraphThreadNotFoundError';
70
+ }
71
+ }
72
+ export class GraphNotInterruptedError extends GraphError {
73
+ threadId;
74
+ status;
75
+ constructor(threadId, status) {
76
+ super(`Thread "${threadId}" is ${status}, not awaiting input, so there is nothing to resume with`, 'GRAPH_NOT_INTERRUPTED');
77
+ this.threadId = threadId;
78
+ this.status = status;
79
+ this.name = 'GraphNotInterruptedError';
80
+ }
81
+ }
82
+ /** Stable key for one interrupt, so a replayed node picks up the answer it was given. */
83
+ export function interruptKey(pending) {
84
+ return `${pending.node}:${pending.step}:${pending.index}`;
85
+ }