nexus-ai-pro 1.7.0 → 1.9.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 +22 -0
- package/CHANGELOG.md +63 -1
- package/README.md +170 -0
- package/dist/embeddings/manager.js +1 -1
- package/dist/embeddings/models.js +1 -1
- package/dist/graph/channels.d.ts +19 -0
- package/dist/graph/channels.js +55 -0
- package/dist/graph/checkpointer.d.ts +62 -0
- package/dist/graph/checkpointer.js +136 -0
- package/dist/graph/errors.d.ts +50 -0
- package/dist/graph/errors.js +85 -0
- package/dist/graph/graph.d.ts +90 -0
- package/dist/graph/graph.js +418 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.js +5 -0
- package/dist/optimizer/cost-budget.d.ts +20 -0
- package/dist/optimizer/cost-budget.js +30 -0
- package/dist/optimizer/cost.d.ts +5 -7
- package/dist/optimizer/cost.js +6 -18
- package/dist/types/graph.d.ts +164 -0
- package/dist/types/graph.js +12 -0
- package/dist-cjs/embeddings/manager.js +2 -2
- package/dist-cjs/embeddings/models.js +4 -4
- package/dist-cjs/graph/channels.d.ts +19 -0
- package/dist-cjs/graph/channels.js +63 -0
- package/dist-cjs/graph/checkpointer.d.ts +62 -0
- package/dist-cjs/graph/checkpointer.js +141 -0
- package/dist-cjs/graph/errors.d.ts +50 -0
- package/dist-cjs/graph/errors.js +96 -0
- package/dist-cjs/graph/graph.d.ts +90 -0
- package/dist-cjs/graph/graph.js +424 -0
- package/dist-cjs/graph/index.d.ts +6 -0
- package/dist-cjs/graph/index.js +29 -0
- package/dist-cjs/optimizer/cost-budget.d.ts +20 -0
- package/dist-cjs/optimizer/cost-budget.js +36 -0
- package/dist-cjs/optimizer/cost.d.ts +5 -7
- package/dist-cjs/optimizer/cost.js +13 -24
- package/dist-cjs/types/graph.d.ts +164 -0
- package/dist-cjs/types/graph.js +15 -0
- package/package.json +17 -2
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,66 @@ 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.9.0] - 2026-09-15
|
|
10
|
+
|
|
11
|
+
Install weight, made measurable and enforced. Every export subpath now has a size budget that
|
|
12
|
+
CI holds it to, the README publishes what each import costs, and enforcing a cost budget no
|
|
13
|
+
longer loads the model catalogue. No public API changed.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **A per-subpath size budget, enforced in CI.** `npm run size:check` measures the transitive import
|
|
18
|
+
graph of every one of the 70 export subpaths and fails when an entry point grows past its recorded
|
|
19
|
+
budget. The package already budgeted the whole tarball, which is exactly why per-entry growth went
|
|
20
|
+
unnoticed across three releases while the total budget was raised three times: a shared module
|
|
21
|
+
pulled into an otherwise small entry point does not move the tarball at all, it only moves what a
|
|
22
|
+
consumer pays to import one piece. `npm run size:update` rewrites the budgets and regenerates the
|
|
23
|
+
README table.
|
|
24
|
+
- **A published size table.** The README now carries what each import actually costs, generated from
|
|
25
|
+
the build rather than written by hand, and the check fails if it goes stale. The claim is now
|
|
26
|
+
verifiable before installing.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Enforcing a cost budget no longer loads the model catalogue.** `assertWithinCostBudget`,
|
|
31
|
+
`CostBudgetError`, `formatCost`, and `DEFAULT_CURRENCY` moved to `optimizer/cost-budget.ts`, which
|
|
32
|
+
has no registry dependency; `optimizer/cost.ts` re-exports all four, so no import changes and the
|
|
33
|
+
error classes stay identical rather than becoming copies. Comparing two numbers had been dragging
|
|
34
|
+
30 KB of chat-model data along with it.
|
|
35
|
+
- **`nexus-ai-pro/embeddings` is 29% smaller**, 129 KB down to 91 KB, and no longer reaches
|
|
36
|
+
`types/providers.js` at all. A test pins that, not just the budget.
|
|
37
|
+
|
|
38
|
+
## [1.8.0] - 2026-09-14
|
|
39
|
+
|
|
40
|
+
Graphs. Nodes, edges, cycles, fan-out and subgraphs over typed state, with every superstep
|
|
41
|
+
checkpointed so a run is resumable, inspectable and interruptible by construction rather than
|
|
42
|
+
after opting in, on a subpath that costs 5% of the root import.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **Typed state graphs.** `nexus-ai-pro/graph` adds nodes, edges, conditional edges, cycles, fan-out,
|
|
47
|
+
and subgraphs over typed state channels: explicit nodes and edges, persistent graph state,
|
|
48
|
+
checkpoint and resume, human-in-the-loop interrupts, and complex branching.
|
|
49
|
+
- **Durable by construction.** Every superstep is checkpointed, so a run is resumable without opting
|
|
50
|
+
into a checkpointer first. `MemoryGraphCheckpointer` covers a single process;
|
|
51
|
+
`OperationStoreCheckpointer` persists through the `OperationStore` that already backs durable
|
|
52
|
+
operations, which makes a Redis-backed thread a one-line change and lets a different worker resume
|
|
53
|
+
what another suspended. The store is imported as a type only, so the graph subpath stays small.
|
|
54
|
+
- **Human in the loop.** A node calls `context.interrupt()`; the graph checkpoints and reports
|
|
55
|
+
`awaiting_input`, and `resume()` supplies the value. On replay the same call returns that value
|
|
56
|
+
instead of throwing, keyed by node, step, and position, so one node can ask several questions
|
|
57
|
+
across several resumes. A sibling branch that already completed is not re-run.
|
|
58
|
+
- **Channels with reducers** — `lastValue`, `appendList`, `appendSet`, `mergeObject`, `counter`, and
|
|
59
|
+
`reducerChannel` — so two branches writing the same slot in one superstep combine rather than
|
|
60
|
+
clobber.
|
|
61
|
+
- **Time travel.** `state()`, `history()`, and `resumeFrom(step)` read and rewind a thread.
|
|
62
|
+
- **Compile-time validation.** An edge to an unknown node, a node nothing routes to, a duplicate node
|
|
63
|
+
name, and a reserved name are all refused at `compile()` rather than at run time.
|
|
64
|
+
- New subpath `nexus-ai-pro/graph`. It is deliberately absent from the root import, so a user who
|
|
65
|
+
does not build graphs pays nothing for it.
|
|
66
|
+
|
|
7
67
|
## [1.7.0] - 2026-09-08
|
|
8
68
|
|
|
9
69
|
Batch economics, distributed limits, and durable assets: the rest of the theme *work that
|
|
@@ -397,7 +457,9 @@ shared policy vocabulary is in place for a future release that revisits this.
|
|
|
397
457
|
- URL fetching rejects unsafe private-network targets and limits response reads.
|
|
398
458
|
- CLI and audit findings no longer disclose detected secret values.
|
|
399
459
|
|
|
400
|
-
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.
|
|
460
|
+
[Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.9.0...HEAD
|
|
461
|
+
[1.9.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.8.0...v1.9.0
|
|
462
|
+
[1.8.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.7.0...v1.8.0
|
|
401
463
|
[1.7.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.6.0...v1.7.0
|
|
402
464
|
[1.6.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.5.0...v1.6.0
|
|
403
465
|
[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
|
|
|
@@ -1538,6 +1624,90 @@ import { createTelephonyRealtimeBridge } from 'nexus-ai-pro/telephony/realtime-b
|
|
|
1538
1624
|
|
|
1539
1625
|
Provider SDKs are optional peer dependencies. The package ships ESM and CommonJS builds, supports Node.js 22+, and is marked with `sideEffects: false`. Only the entry points listed in the package export map are public; deep imports into `dist`, `dist-cjs`, or `src` are unsupported.
|
|
1540
1626
|
|
|
1627
|
+
### What each import actually costs
|
|
1628
|
+
|
|
1629
|
+
Importing one piece loads one piece. The figures below are the transitive import graph of each entry
|
|
1630
|
+
point, generated from the build by `npm run size:check`, which fails CI when an entry point grows
|
|
1631
|
+
past its budget — so the numbers stay true rather than aspirational.
|
|
1632
|
+
|
|
1633
|
+
<!-- size-table:start -->
|
|
1634
|
+
| Import | Size | Share of root |
|
|
1635
|
+
| --- | --- | --- |
|
|
1636
|
+
| `nexus-ai-pro` | 606 KB | 100% |
|
|
1637
|
+
| `nexus-ai-pro/config` | 465 KB | 77% |
|
|
1638
|
+
| `nexus-ai-pro/core` | 459 KB | 76% |
|
|
1639
|
+
| `nexus-ai-pro/realtime` | 157 KB | 26% |
|
|
1640
|
+
| `nexus-ai-pro/batch` | 118 KB | 19% |
|
|
1641
|
+
| `nexus-ai-pro/realtime/session` | 94 KB | 16% |
|
|
1642
|
+
| `nexus-ai-pro/embeddings` | 91 KB | 15% |
|
|
1643
|
+
| `nexus-ai-pro/providers/groq` | 79 KB | 13% |
|
|
1644
|
+
| `nexus-ai-pro/providers/mistral` | 79 KB | 13% |
|
|
1645
|
+
| `nexus-ai-pro/providers/azure-openai` | 78 KB | 13% |
|
|
1646
|
+
| `nexus-ai-pro/providers/openrouter` | 78 KB | 13% |
|
|
1647
|
+
| `nexus-ai-pro/providers/deepseek` | 77 KB | 13% |
|
|
1648
|
+
| `nexus-ai-pro/providers/llamacpp` | 77 KB | 13% |
|
|
1649
|
+
| `nexus-ai-pro/providers/lmstudio` | 77 KB | 13% |
|
|
1650
|
+
| `nexus-ai-pro/providers/openai` | 77 KB | 13% |
|
|
1651
|
+
| `nexus-ai-pro/providers/anthropic` | 70 KB | 12% |
|
|
1652
|
+
| `nexus-ai-pro/providers/google` | 66 KB | 11% |
|
|
1653
|
+
| `nexus-ai-pro/images` | 63 KB | 10% |
|
|
1654
|
+
| `nexus-ai-pro/providers/ollama` | 58 KB | 10% |
|
|
1655
|
+
| `nexus-ai-pro/batch/openai` | 52 KB | 9% |
|
|
1656
|
+
| `nexus-ai-pro/batch/anthropic` | 52 KB | 9% |
|
|
1657
|
+
| `nexus-ai-pro/operations` | 49 KB | 8% |
|
|
1658
|
+
| `nexus-ai-pro/batch/mock` | 47 KB | 8% |
|
|
1659
|
+
| `nexus-ai-pro/providers/cohere` | 46 KB | 8% |
|
|
1660
|
+
| `nexus-ai-pro/realtime/openai-webrtc` | 46 KB | 8% |
|
|
1661
|
+
| `nexus-ai-pro/security` | 40 KB | 7% |
|
|
1662
|
+
| `nexus-ai-pro/models` | 35 KB | 6% |
|
|
1663
|
+
| `nexus-ai-pro/graph` | 29 KB | 5% |
|
|
1664
|
+
| `nexus-ai-pro/realtime/openai-websocket` | 29 KB | 5% |
|
|
1665
|
+
| `nexus-ai-pro/evals` | 23 KB | 4% |
|
|
1666
|
+
| `nexus-ai-pro/images/stores` | 22 KB | 4% |
|
|
1667
|
+
| `nexus-ai-pro/telephony/twilio` | 21 KB | 3% |
|
|
1668
|
+
| `nexus-ai-pro/telephony` | 20 KB | 3% |
|
|
1669
|
+
| `nexus-ai-pro/realtime/mock` | 19 KB | 3% |
|
|
1670
|
+
| `nexus-ai-pro/voice` | 18 KB | 3% |
|
|
1671
|
+
| `nexus-ai-pro/images/openai` | 18 KB | 3% |
|
|
1672
|
+
| `nexus-ai-pro/embeddings/adapters` | 17 KB | 3% |
|
|
1673
|
+
| `nexus-ai-pro/realtime/conversation` | 16 KB | 3% |
|
|
1674
|
+
| `nexus-ai-pro/images/assets` | 14 KB | 2% |
|
|
1675
|
+
| `nexus-ai-pro/context` | 13 KB | 2% |
|
|
1676
|
+
| `nexus-ai-pro/operations/adapters` | 13 KB | 2% |
|
|
1677
|
+
| `nexus-ai-pro/realtime/tools` | 13 KB | 2% |
|
|
1678
|
+
| `nexus-ai-pro/workflows` | 13 KB | 2% |
|
|
1679
|
+
| `nexus-ai-pro/images/mock` | 12 KB | 2% |
|
|
1680
|
+
| `nexus-ai-pro/optimizer` | 11 KB | 2% |
|
|
1681
|
+
| `nexus-ai-pro/voice/session` | 11 KB | 2% |
|
|
1682
|
+
| `nexus-ai-pro/providers` | 10 KB | 2% |
|
|
1683
|
+
| `nexus-ai-pro/providers/base` | 10 KB | 2% |
|
|
1684
|
+
| `nexus-ai-pro/embeddings/models` | 10 KB | 2% |
|
|
1685
|
+
| `nexus-ai-pro/voice/openai` | 9 KB | 1% |
|
|
1686
|
+
| `nexus-ai-pro/ops/circuit-breaker` | 8 KB | 1% |
|
|
1687
|
+
| `nexus-ai-pro/realtime/openai-server` | 8 KB | 1% |
|
|
1688
|
+
| `nexus-ai-pro/capabilities` | 8 KB | 1% |
|
|
1689
|
+
| `nexus-ai-pro/telephony/realtime-bridge` | 7 KB | 1% |
|
|
1690
|
+
| `nexus-ai-pro/providers/errors` | 5 KB | 0.8% |
|
|
1691
|
+
| `nexus-ai-pro/embeddings/mock` | 5 KB | 0.8% |
|
|
1692
|
+
| `nexus-ai-pro/cache/semantic-cache` | 5 KB | 0.8% |
|
|
1693
|
+
| `nexus-ai-pro/evals/judge` | 5 KB | 0.8% |
|
|
1694
|
+
| `nexus-ai-pro/operations/webhooks` | 4 KB | 0.7% |
|
|
1695
|
+
| `nexus-ai-pro/ops/rate-limit-adapters` | 3 KB | 0.5% |
|
|
1696
|
+
| `nexus-ai-pro/cache/memory-cache` | 3 KB | 0.5% |
|
|
1697
|
+
| `nexus-ai-pro/cache` | 2 KB | 0.3% |
|
|
1698
|
+
| `nexus-ai-pro/cache/adapters` | 2 KB | 0.3% |
|
|
1699
|
+
| `nexus-ai-pro/rag` | 2 KB | 0.3% |
|
|
1700
|
+
| `nexus-ai-pro/jobs` | 2 KB | 0.3% |
|
|
1701
|
+
| `nexus-ai-pro/jobs/durable-adapters` | 2 KB | 0.3% |
|
|
1702
|
+
| `nexus-ai-pro/jobs/queue` | 2 KB | 0.3% |
|
|
1703
|
+
| `nexus-ai-pro/streaming` | 1 KB | 0.2% |
|
|
1704
|
+
| `nexus-ai-pro/providers/type-guards` | 1 KB | 0.2% |
|
|
1705
|
+
| `nexus-ai-pro/jobs/batch` | 1 KB | 0.2% |
|
|
1706
|
+
<!-- size-table:end -->
|
|
1707
|
+
|
|
1708
|
+
A capability that only works by importing the whole runtime is treated as a design problem, not an
|
|
1709
|
+
acceptable cost.
|
|
1710
|
+
|
|
1541
1711
|
## Examples
|
|
1542
1712
|
|
|
1543
1713
|
```bash
|
|
@@ -3,7 +3,7 @@ import { AuditLogger } from '../ops/audit-logger.js';
|
|
|
3
3
|
import { MetricsCollector } from '../ops/metrics.js';
|
|
4
4
|
import { RateLimiter } from '../ops/rate-limiter.js';
|
|
5
5
|
import { NexusProviderError, createTimeoutProviderError, isAbortError } from '../providers/errors.js';
|
|
6
|
-
import { assertWithinCostBudget } from '../optimizer/cost.js';
|
|
6
|
+
import { assertWithinCostBudget } from '../optimizer/cost-budget.js';
|
|
7
7
|
import { Tokenizer } from '../utils/tokenizer.js';
|
|
8
8
|
import { generateRequestId } from '../utils/ids.js';
|
|
9
9
|
import { EmbeddingCapabilityError, EmbeddingError, EmbeddingProviderError, EmbeddingProviderNotFoundError, EmbeddingProviderResponseError, EmbeddingValidationError, } from './errors.js';
|
|
@@ -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;
|