nexus-ai-pro 1.6.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.
Files changed (125) hide show
  1. package/API_STABILITY.md +89 -0
  2. package/CHANGELOG.md +112 -1
  3. package/README.md +311 -3
  4. package/SECURITY.md +1 -1
  5. package/dist/batch/anthropic.d.ts +26 -0
  6. package/dist/batch/anthropic.js +179 -0
  7. package/dist/batch/errors.d.ts +24 -0
  8. package/dist/batch/errors.js +46 -0
  9. package/dist/batch/index.d.ts +7 -0
  10. package/dist/batch/index.js +6 -0
  11. package/dist/batch/manager.d.ts +59 -0
  12. package/dist/batch/manager.js +275 -0
  13. package/dist/batch/mock.d.ts +41 -0
  14. package/dist/batch/mock.js +93 -0
  15. package/dist/batch/openai.d.ts +31 -0
  16. package/dist/batch/openai.js +169 -0
  17. package/dist/core/nexus.d.ts +5 -0
  18. package/dist/core/nexus.js +56 -14
  19. package/dist/embeddings/manager.js +6 -1
  20. package/dist/graph/channels.d.ts +19 -0
  21. package/dist/graph/channels.js +55 -0
  22. package/dist/graph/checkpointer.d.ts +62 -0
  23. package/dist/graph/checkpointer.js +136 -0
  24. package/dist/graph/errors.d.ts +50 -0
  25. package/dist/graph/errors.js +85 -0
  26. package/dist/graph/graph.d.ts +90 -0
  27. package/dist/graph/graph.js +418 -0
  28. package/dist/graph/index.d.ts +6 -0
  29. package/dist/graph/index.js +5 -0
  30. package/dist/images/asset-stores.d.ts +127 -0
  31. package/dist/images/asset-stores.js +377 -0
  32. package/dist/images/asset-support.d.ts +107 -0
  33. package/dist/images/asset-support.js +166 -0
  34. package/dist/images/assets.d.ts +5 -85
  35. package/dist/images/assets.js +7 -166
  36. package/dist/images/index.d.ts +1 -0
  37. package/dist/images/index.js +1 -0
  38. package/dist/images/manager.d.ts +4 -1
  39. package/dist/images/manager.js +14 -2
  40. package/dist/index.d.ts +7 -0
  41. package/dist/index.js +6 -0
  42. package/dist/ops/circuit-breaker.d.ts +89 -0
  43. package/dist/ops/circuit-breaker.js +207 -0
  44. package/dist/ops/family-telemetry.d.ts +52 -0
  45. package/dist/ops/family-telemetry.js +74 -0
  46. package/dist/ops/rate-limit-adapters.d.ts +62 -0
  47. package/dist/ops/rate-limit-adapters.js +81 -0
  48. package/dist/ops/rate-limiter.d.ts +15 -1
  49. package/dist/ops/rate-limiter.js +33 -2
  50. package/dist/router/auto-router.d.ts +8 -0
  51. package/dist/router/auto-router.js +15 -1
  52. package/dist/router/index.d.ts +1 -1
  53. package/dist/router/index.js +2 -2
  54. package/dist/router/types.d.ts +7 -0
  55. package/dist/telephony/manager.d.ts +5 -1
  56. package/dist/telephony/manager.js +18 -7
  57. package/dist/types/batch.d.ts +134 -0
  58. package/dist/types/batch.js +4 -0
  59. package/dist/types/config.d.ts +9 -0
  60. package/dist/types/graph.d.ts +164 -0
  61. package/dist/types/graph.js +12 -0
  62. package/dist/voice/manager.d.ts +5 -1
  63. package/dist/voice/manager.js +18 -7
  64. package/dist-cjs/batch/anthropic.d.ts +26 -0
  65. package/dist-cjs/batch/anthropic.js +183 -0
  66. package/dist-cjs/batch/errors.d.ts +24 -0
  67. package/dist-cjs/batch/errors.js +55 -0
  68. package/dist-cjs/batch/index.d.ts +7 -0
  69. package/dist-cjs/batch/index.js +21 -0
  70. package/dist-cjs/batch/manager.d.ts +59 -0
  71. package/dist-cjs/batch/manager.js +279 -0
  72. package/dist-cjs/batch/mock.d.ts +41 -0
  73. package/dist-cjs/batch/mock.js +97 -0
  74. package/dist-cjs/batch/openai.d.ts +31 -0
  75. package/dist-cjs/batch/openai.js +173 -0
  76. package/dist-cjs/core/nexus.d.ts +5 -0
  77. package/dist-cjs/core/nexus.js +56 -14
  78. package/dist-cjs/embeddings/manager.js +6 -1
  79. package/dist-cjs/graph/channels.d.ts +19 -0
  80. package/dist-cjs/graph/channels.js +63 -0
  81. package/dist-cjs/graph/checkpointer.d.ts +62 -0
  82. package/dist-cjs/graph/checkpointer.js +141 -0
  83. package/dist-cjs/graph/errors.d.ts +50 -0
  84. package/dist-cjs/graph/errors.js +96 -0
  85. package/dist-cjs/graph/graph.d.ts +90 -0
  86. package/dist-cjs/graph/graph.js +424 -0
  87. package/dist-cjs/graph/index.d.ts +6 -0
  88. package/dist-cjs/graph/index.js +29 -0
  89. package/dist-cjs/images/asset-stores.d.ts +127 -0
  90. package/dist-cjs/images/asset-stores.js +385 -0
  91. package/dist-cjs/images/asset-support.d.ts +107 -0
  92. package/dist-cjs/images/asset-support.js +185 -0
  93. package/dist-cjs/images/assets.d.ts +5 -85
  94. package/dist-cjs/images/assets.js +40 -199
  95. package/dist-cjs/images/index.d.ts +1 -0
  96. package/dist-cjs/images/index.js +4 -1
  97. package/dist-cjs/images/manager.d.ts +4 -1
  98. package/dist-cjs/images/manager.js +14 -2
  99. package/dist-cjs/index.d.ts +7 -0
  100. package/dist-cjs/index.js +46 -27
  101. package/dist-cjs/ops/circuit-breaker.d.ts +89 -0
  102. package/dist-cjs/ops/circuit-breaker.js +211 -0
  103. package/dist-cjs/ops/family-telemetry.d.ts +52 -0
  104. package/dist-cjs/ops/family-telemetry.js +78 -0
  105. package/dist-cjs/ops/rate-limit-adapters.d.ts +62 -0
  106. package/dist-cjs/ops/rate-limit-adapters.js +86 -0
  107. package/dist-cjs/ops/rate-limiter.d.ts +15 -1
  108. package/dist-cjs/ops/rate-limiter.js +33 -2
  109. package/dist-cjs/router/auto-router.d.ts +8 -0
  110. package/dist-cjs/router/auto-router.js +15 -1
  111. package/dist-cjs/router/index.d.ts +1 -1
  112. package/dist-cjs/router/index.js +2 -2
  113. package/dist-cjs/router/types.d.ts +7 -0
  114. package/dist-cjs/telephony/manager.d.ts +5 -1
  115. package/dist-cjs/telephony/manager.js +18 -7
  116. package/dist-cjs/types/batch.d.ts +134 -0
  117. package/dist-cjs/types/batch.js +8 -0
  118. package/dist-cjs/types/config.d.ts +9 -0
  119. package/dist-cjs/types/graph.d.ts +164 -0
  120. package/dist-cjs/types/graph.js +15 -0
  121. package/dist-cjs/voice/manager.d.ts +5 -1
  122. package/dist-cjs/voice/manager.js +18 -7
  123. package/package.json +109 -5
  124. package/NEXUS.md +0 -387
  125. package/ROADMAP.md +0 -444
package/API_STABILITY.md CHANGED
@@ -158,6 +158,95 @@ only when the client exposes `eval`; without it the compare-and-set degrades to
158
158
  that narrows but does not close the race, and that limitation is documented rather than hidden.
159
159
  Webhook delivery is best-effort and never fails the operation it describes.
160
160
 
161
+ ## Resilience API stage
162
+
163
+ `CircuitBreaker`, the `RateLimitStore` contract, `MemoryRateLimitStore`, `RedisRateLimitStore`, and
164
+ the `ops/circuit-breaker` and `ops/rate-limit-adapters` subpaths are public and follow the 1.x rules,
165
+ together with `circuitBreaker` and `rateLimit.store` in `NexusAIConfig`.
166
+
167
+ `RateLimiter.check()` keeps its synchronous signature and in-memory behavior; `checkAsync()` is the
168
+ additive store-aware path. `NexusRateLimitError` gained an optional `resetAt` and a
169
+ `retryAfterSeconds` accessor. `RouterContext` gained an optional `openCircuits`, and `Router.route()`
170
+ an optional trailing parameter, both additive.
171
+
172
+ Which provider a breaker excludes at a given moment is a runtime decision, not a compatibility
173
+ guarantee: thresholds, scoring, and the exact ordering of candidates can change in a minor release.
174
+ What is guaranteed is the state machine — closed, open after a threshold is crossed, half-open after
175
+ the cooldown, closed again only after the configured probe successes — and that routing still selects
176
+ a provider when every circuit is open rather than failing the request untried.
177
+
178
+ Breaker state is per process by design. Two workers can disagree about a provider, and neither the
179
+ breaker nor its snapshot promises cluster-wide consensus. Rate-limit counters are the opposite: they
180
+ are only shared when a store is configured, and the default in-memory counter gives each worker its
181
+ own budget.
182
+
183
+ ## Batch API stage
184
+
185
+ `BatchManager`, the `BatchProvider` contract, the OpenAI and Anthropic adapters, the deterministic
186
+ mock, and the `nexus-ai-pro/batch`, `batch/openai`, `batch/anthropic`, and `batch/mock` subpaths are
187
+ public and follow the 1.x rules, as are the normalized request, ref, state, and result shapes.
188
+
189
+ `BatchJobRef` is guaranteed to stay JSON-serializable and sufficient on its own to poll, collect, or
190
+ cancel a batch. That is what makes a persisted ref survive a restart, and narrowing it would break
191
+ every stored ref, so it requires a major release.
192
+
193
+ Two behaviors are guaranteed. Results are matched by `customId`, never by position, and a duplicate
194
+ `customId` is refused before submission. And a failed batch returns no items rather than fabricated
195
+ ones.
196
+
197
+ Provider vocabulary is not normalized beyond `BatchJobStatus`. Anthropic reports only `in_progress`
198
+ and `ended`, with the real outcome on the per-item results; that adapter maps `ended` to `completed`
199
+ and lets item errors carry the detail rather than inventing a batch-level failure. Payloads under
200
+ `raw`, provider quotas, completion windows, and the discount rate itself are provider-controlled and
201
+ may change without a major release.
202
+
203
+ ## Asset store stage
204
+
205
+ `FilesystemAssetStore`, `S3AssetStore`, the `S3LikeClient` contract, and the
206
+ `nexus-ai-pro/images/stores` subpath are public under the 1.x rules and implement the existing
207
+ `AssetStore` contract unchanged. The shared contract, errors, and validation moved to an internal
208
+ `asset-support` module and are re-exported from `images/assets`, so every existing import resolves
209
+ as before.
210
+
211
+ All three stores guarantee that a missing asset and one owned by another tenant are
212
+ indistinguishable, that stored bytes carry a SHA-256 checksum, and that a lapsed asset is unreadable
213
+ before it is purged. `S3AssetStore.purgeExpired()` lists the record prefix on every call; on a large
214
+ bucket prefer the provider lifecycle rules. Neither durable store coordinates concurrent deletes
215
+ across processes.
216
+
217
+ ## Model registry generation stage
218
+
219
+ `data/models/*.json` is the versioned source of truth in the repository, and
220
+ `scripts/generate-model-registry.mjs` emits `src/models/generated.ts` from it.
221
+
222
+ Neither is published, and neither is public API. They duplicate `KNOWN_MODELS` exactly, so shipping
223
+ them would add roughly 310KB to every install for data nothing reads. Generation is a build-time
224
+ guarantee about how the registry is maintained, not a runtime surface: the resolver continues to read
225
+ `KNOWN_MODELS`, and a test asserts the two match. Switching the resolver over is a later change, so
226
+ that introducing generation cannot alter pricing behavior in the same release.
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
+
161
250
  ## Deprecation process
162
251
 
163
252
  Deprecated APIs are marked with `@deprecated` in declarations and described in the changelog. Removals
package/CHANGELOG.md CHANGED
@@ -4,6 +4,115 @@ 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
+
40
+ ## [1.7.0] - 2026-09-08
41
+
42
+ Batch economics, distributed limits, and durable assets: the rest of the theme *work that
43
+ outlives a process*. The half-price asynchronous tier is reachable, one rate-limit budget covers
44
+ every worker, a failing provider leaves routing until it recovers, generated media persists
45
+ outside the process, and every operation family reports into the same metrics and audit log.
46
+
47
+ ### Added
48
+
49
+ - **Circuit breaking.** `CircuitBreaker` consumes the same attempt signals as `ProviderHealthMonitor`
50
+ and trips routing away from a failing provider entirely, rather than merely ranking it lower. Two
51
+ independent triggers: consecutive failures, and a failure rate over a rolling window that catches a
52
+ provider failing half its calls without ever failing several in a row. After a cooldown the circuit
53
+ admits a limited number of probes; a success closes it, a failure reopens it and restarts the
54
+ cooldown. Exposed through `circuitBreaker` config, `ai.getCircuitBreakerStatus()`,
55
+ `ai.resetCircuitBreaker()`, and the `nexus-ai-pro/ops/circuit-breaker` subpath.
56
+ - **Distributed rate limiting.** A `RateLimitStore` contract with `MemoryRateLimitStore` and
57
+ `RedisRateLimitStore`, set through `rateLimit.store`, so one budget covers every worker instead of
58
+ each process getting the full limit. The Redis store does the increment and the expiry in one
59
+ atomic Lua call when the client exposes `eval`, and re-arms a missing TTL on the fallback path so a
60
+ crash between `INCR` and `PEXPIRE` cannot block a key forever. Completions and embeddings share the
61
+ limiter, so they share the budget.
62
+ - `NexusRateLimitError` now carries `resetAt` and a `retryAfterSeconds` accessor, so a gateway can
63
+ answer with a real `Retry-After` header.
64
+ - New subpaths `nexus-ai-pro/ops/circuit-breaker` and `nexus-ai-pro/ops/rate-limit-adapters`.
65
+ - **Provider batch tiers.** `BatchManager` puts OpenAI Batch and Anthropic Message Batches behind one
66
+ operation handle, reaching the roughly half-price asynchronous tier that local `runBatch()`
67
+ concurrency cannot. Results are matched by a required `customId` rather than by position, since a
68
+ batch provider does not guarantee output order; a duplicate id is refused before submission.
69
+ Polling backs off up to a configurable ceiling, an idempotency key replays instead of submitting
70
+ twice, and `resume()` collects a batch from its `BatchJobRef` alone so a restarted worker can
71
+ finish what another submitted. Ships with a deterministic `MockBatchProvider`.
72
+ - **Filesystem and S3 asset stores.** `FilesystemAssetStore` and `S3AssetStore` implement the
73
+ existing `AssetStore` contract, with tenant isolation, retention, SHA-256 checksums, and signing.
74
+ A missing asset and one owned by another tenant are indistinguishable, because a distinguishable
75
+ error leaks the existence of another tenant asset. Both write bytes and a JSON sidecar per asset
76
+ rather than a shared index, so concurrent writers do not contend and a torn write loses at most
77
+ one record. `S3LikeClient` is structural, so S3, R2, and MinIO all work without an SDK dependency.
78
+ - **Generated model registry.** `data/models/*.json` is now the versioned source of truth, and
79
+ `scripts/generate-model-registry.mjs` emits `src/models/generated.ts` from it. The generator
80
+ validates required fields, price signs, context bounds, status values, and dangling aliases;
81
+ `npm run registry:check` runs inside `npm run check` so committed data and output cannot drift.
82
+ The runtime still reads `KNOWN_MODELS`, and a test asserts the two match exactly. The generated
83
+ module and its data are build-time artifacts and are not published: they duplicate `KNOWN_MODELS`
84
+ exactly, so shipping them would add roughly 310KB to every install for data nothing reads.
85
+ - New subpaths `nexus-ai-pro/batch`, `batch/openai`, `batch/anthropic`, `batch/mock`, and
86
+ `nexus-ai-pro/images/stores`.
87
+ - **Cross-family observability.** Image, voice, and telephony operations now report into the same
88
+ metrics collector, audit log, and rate limiter as completions and embeddings, closing the gap where
89
+ an application running phone agents and image generation had observability for only part of its
90
+ spend. Metrics carry `family` and `operation` labels, and one rate-limit budget now covers every
91
+ family. `FamilyTelemetry` is exported for a family an application adds itself.
92
+
93
+ ### Changed
94
+
95
+ - `RateLimiter` gained `checkAsync()` for the store-aware path. `check()` keeps its synchronous
96
+ signature and behavior, and the runtime only awaits when a store is configured, so a request
97
+ without one pays no extra microtask.
98
+ - `ImageManager`, `VoiceManager`, and `TelephonyManager` accept an optional second constructor
99
+ argument carrying the shared observability objects. It defaults to empty, so constructing one
100
+ standalone is unchanged and simply records nothing.
101
+ - The shared asset contract, errors, and validation moved from `images/assets` to an internal
102
+ `asset-support` module so the filesystem, S3, and memory stores cannot drift on what a valid asset
103
+ is. `images/assets` re-exports all of it, so existing imports are unchanged.
104
+ - Documentation consolidated from nine files to seven. `NEXUS.md` and `EXPLANATION.md` were four
105
+ releases stale and largely restated the README; their unique content moved to `CONTRIBUTING.md`
106
+ (real-provider test setup, the public-feature checklist, the manual smoke checklist) and to a new
107
+ README "Known Limitations" section. The published package now carries only `README.md`,
108
+ `API_STABILITY.md`, `CHANGELOG.md`, `SECURITY.md`, and `LICENSE`; `ROADMAP.md` is a design proposal
109
+ and stays on GitHub.
110
+ - `SECURITY.md` no longer names a specific supported version line. It claimed `0.9.x` while 1.6.0 was
111
+ the published release, so the wording is now version-independent and cannot go stale again.
112
+ - `RouterContext` gained an optional `openCircuits`, and `Router.route()` an optional trailing
113
+ parameter. When every candidate's circuit is open the router routes anyway: that usually means a
114
+ shared dependency is down, and one attempt beats a certain failure with no attempt at all.
115
+
7
116
  ## [1.6.0] - 2026-09-03
8
117
 
9
118
  Durable operations, the first half of the theme *work that outlives a process*. Long-running work now
@@ -321,7 +430,9 @@ shared policy vocabulary is in place for a future release that revisits this.
321
430
  - URL fetching rejects unsafe private-network targets and limits response reads.
322
431
  - CLI and audit findings no longer disclose detected secret values.
323
432
 
324
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.6.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
435
+ [1.7.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.6.0...v1.7.0
325
436
  [1.6.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.5.0...v1.6.0
326
437
  [1.5.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.4.0...v1.5.0
327
438
  [1.4.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.3.0...v1.4.0
package/README.md CHANGED
@@ -6,8 +6,8 @@ Use the whole pipeline for production AI features, or turn pieces off when you o
6
6
 
7
7
  - NPM: https://www.npmjs.com/package/nexus-ai-pro
8
8
  - GitHub: https://github.com/mkhitar-abrahamyan/nexus-ai
9
- - Full technical notes and manual test cases: [NEXUS.md](./NEXUS.md)
10
- - Image generation status and remaining infrastructure work: [ROADMAP.md](./ROADMAP.md)
9
+ - Contributing, testing, and release procedure: [CONTRIBUTING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/CONTRIBUTING.md)
10
+ - Delivered work and what is planned next: [ROADMAP.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md)
11
11
 
12
12
  ## Install
13
13
 
@@ -85,6 +85,10 @@ 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
89
+ - trip routing away from a failing provider, and share one rate-limit budget across workers
90
+ - reach the providers' half-price asynchronous batch tier behind one operation handle
91
+ - persist generated media to disk or S3 with tenant isolation, retention, and checksums
88
92
  - build persistent realtime voice agents with interruption, live tools, and normalized conversation state
89
93
  - keep TypeScript types around every request and response
90
94
 
@@ -418,6 +422,284 @@ const response = await ai.completeVerified(
418
422
  );
419
423
  ```
420
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
+
509
+ ## Provider Batch Tiers
510
+
511
+ Both OpenAI and Anthropic sell an asynchronous tier at roughly half price, in exchange for a
512
+ completion window measured in hours. Local `runBatch()` concurrency cannot reach it — it is a
513
+ different API. `BatchManager` puts both behind one operation handle.
514
+
515
+ ```ts
516
+ import { createGraph } from 'nexus-ai-pro/graph';
517
+ import { BatchManager } from 'nexus-ai-pro/batch';
518
+ import { OpenAIBatchProvider } from 'nexus-ai-pro/batch/openai';
519
+
520
+ const batch = new BatchManager({
521
+ providers: { openai: new OpenAIBatchProvider({ apiKey: process.env.OPENAI_API_KEY! }) },
522
+ defaultProvider: 'openai',
523
+ });
524
+
525
+ const handle = await batch.submit({
526
+ model: 'gpt-5.4-mini',
527
+ idempotencyKey: `nightly-${date}`,
528
+ items: documents.map((document) => ({
529
+ customId: document.id,
530
+ request: { model: 'gpt-5.4-mini', messages: [{ role: 'user', content: document.text }] },
531
+ })),
532
+ });
533
+
534
+ console.log(handle.id); // persist this, do not block a request on the result
535
+ ```
536
+
537
+ `customId` is required, not optional. A batch provider does not guarantee output order, and matching
538
+ results by position is exactly the bug that silently mislabels every row. Duplicates are refused
539
+ before submission for the same reason.
540
+
541
+ The handle settles when the provider finishes, which can be hours later, so treat it as a background
542
+ operation:
543
+
544
+ ```ts
545
+ const result = await handle.result();
546
+
547
+ result.status; // completed | failed | expired | cancelled
548
+ result.counts; // { total, completed, failed }
549
+ result.items; // one entry per customId, with response or error
550
+ result.cost.amount; // priced per item, then discounted at the provider's batch rate
551
+ ```
552
+
553
+ A mixed batch is normal: individual items carry their own `error` while the batch still reports
554
+ `completed`. Nothing is invented for a failed batch — `items` comes back empty rather than padded.
555
+
556
+ **Surviving a restart.** Everything after `submit` takes only a `BatchJobRef`, which is
557
+ JSON-serializable. A worker that never submitted the batch can collect it:
558
+
559
+ ```ts
560
+ const ref = { id: savedBatchId, provider: 'openai' };
561
+
562
+ await batch.status(ref); // provider-side state, without waiting
563
+ const result = await batch.resume(ref);
564
+ await batch.cancel(ref);
565
+ ```
566
+
567
+ Polling backs off from the configured interval up to `maxPollIntervalMs`, so a 24-hour batch does
568
+ not generate 2,880 polls while a fast one is still caught by the first few short intervals.
569
+
570
+ ## Asset Stores
571
+
572
+ `AssetStore` has three implementations. They share one contract, so retention, tenant isolation, and
573
+ checksums behave identically:
574
+
575
+ ```ts
576
+ import { MemoryAssetStore } from 'nexus-ai-pro/images/assets';
577
+ import { FilesystemAssetStore, S3AssetStore } from 'nexus-ai-pro/images/stores';
578
+
579
+ const store = new FilesystemAssetStore({ directory: '/var/lib/app/assets', defaultTtlSeconds: 86_400 });
580
+
581
+ const s3 = new S3AssetStore({
582
+ client, // structural: S3, R2, MinIO, or a test double
583
+ bucket: 'generated-media',
584
+ prefix: 'nexus-assets/',
585
+ });
586
+ ```
587
+
588
+ A missing asset and one owned by another tenant are **indistinguishable** — both return `undefined`
589
+ rather than a permission error, because a distinguishable error leaks the existence of another
590
+ tenant's asset.
591
+
592
+ Both durable stores write two objects per asset: the bytes, and a JSON sidecar holding the
593
+ descriptor, tenant, and expiry. A single shared index would be a write-contention point and a
594
+ corruption blast radius; per-asset sidecars let concurrent writers proceed and lose at most one
595
+ record. `purgeExpired()` is exact but costs a directory listing, so on a large S3 bucket prefer the
596
+ provider's own lifecycle rules and keep this as the fallback.
597
+
598
+ ## Model Registry Generation
599
+
600
+ The registry is generated from versioned provider data in `data/models/`, not hand-edited. The
601
+ Claude 5 reasoning bug fixed in 1.4.0 was exactly the kind of error a hand-written literal of 100+
602
+ models invites.
603
+
604
+ ```bash
605
+ npm run registry:generate # data/models/*.json -> src/models/generated.ts
606
+ npm run registry:check # fails if the committed output is stale
607
+ ```
608
+
609
+ `registry:check` runs as part of `npm run check`, so committed data and committed output cannot
610
+ drift apart. The generator validates required fields, price signs, context bounds, status values,
611
+ and that every alias points at a model that exists — a dangling alias otherwise fails only when a
612
+ request happens to use it.
613
+
614
+ The runtime still reads the hand-written `KNOWN_MODELS`; a test asserts the generated registry
615
+ matches it exactly. Swapping the runtime over is deliberately a separate change, so introducing the
616
+ generator cannot quietly alter pricing data in the same release.
617
+
618
+ `src/models/generated.ts` and `data/` are build-time artifacts and are **not** published. They
619
+ duplicate `KNOWN_MODELS` exactly, and shipping them in both builds would add roughly 310KB to every
620
+ install for data nothing reads. Both live in the repository, where a diff is what you actually want.
621
+
622
+ ## Resilience: Circuit Breaking and Distributed Limits
623
+
624
+ Health monitoring ranks a struggling provider lower. A circuit breaker is the stronger step: while a
625
+ circuit is open the provider is removed from routing entirely, so a hard-down provider stops
626
+ absorbing one failed attempt per request.
627
+
628
+ ```ts
629
+ const ai = new NexusAI({
630
+ providers: { openai: { apiKey: process.env.OPENAI_API_KEY! }, anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! } },
631
+ health: { enabled: true },
632
+ circuitBreaker: {
633
+ enabled: true,
634
+ failureThreshold: 5, // consecutive failures that open the circuit
635
+ failureRateThreshold: 0.5, // or half the calls failing in the window
636
+ minimumThroughput: 10,
637
+ resetTimeoutMs: 30_000, // cooldown before a probe
638
+ halfOpenMaxCalls: 1,
639
+ onStateChange: (event) => logger.warn('circuit', event),
640
+ },
641
+ });
642
+
643
+ ai.getCircuitBreakerStatus(); // state, failure rate, retryAt per provider
644
+ ai.resetCircuitBreaker('openai');
645
+ ```
646
+
647
+ Two independent triggers, because they catch different failures. A consecutive count catches a
648
+ provider that is hard down. A failure *rate* catches one that fails half its calls without ever
649
+ failing several in a row — invisible to a consecutive counter. The rate is only considered once
650
+ `minimumThroughput` calls have been seen, since one failure out of two is not evidence of anything.
651
+
652
+ After the cooldown the circuit goes half-open and admits a limited number of probes. A successful
653
+ probe closes it; a failed probe reopens it and restarts the cooldown. If *every* circuit is open the
654
+ router routes anyway — that usually means a shared dependency is down, and one attempt beats a
655
+ certain failure with no attempt at all.
656
+
657
+ `isFailure` keeps errors that are not the provider's fault out of the calculation:
658
+
659
+ ```ts
660
+ circuitBreaker: {
661
+ enabled: true,
662
+ isFailure: (error) => !(error instanceof Error && error.name === 'AbortError'),
663
+ }
664
+ ```
665
+
666
+ **Distributed rate limiting.** The built-in limiter is process-local, which multiplies the real
667
+ limit by the number of workers. Pointing it at a shared store fixes that, and the same budget then
668
+ covers completions and embeddings alike:
669
+
670
+ ```ts
671
+ import { RedisRateLimitStore } from 'nexus-ai-pro/ops/rate-limit-adapters';
672
+
673
+ const ai = new NexusAI({
674
+ providers: { openai: { apiKey: process.env.OPENAI_API_KEY! } },
675
+ rateLimit: {
676
+ enabled: true,
677
+ maxRequests: 100,
678
+ windowMs: 60_000,
679
+ key: 'userId',
680
+ store: new RedisRateLimitStore(redis),
681
+ },
682
+ });
683
+ ```
684
+
685
+ Prefer a client exposing `eval`: the increment and the expiry then happen in one atomic round trip.
686
+ Without it the store falls back to `INCR` plus `PEXPIRE` and re-arms any missing TTL it sees, so a
687
+ crash between the two calls cannot block a key forever.
688
+
689
+ `NexusRateLimitError` carries `resetAt` and `retryAfterSeconds`, so a gateway can answer with a
690
+ real `Retry-After` header:
691
+
692
+ ```ts
693
+ catch (error) {
694
+ if (error instanceof NexusRateLimitError) {
695
+ response.setHeader('Retry-After', String(error.retryAfterSeconds ?? 60));
696
+ }
697
+ }
698
+ ```
699
+
700
+ Omitting `store` keeps the original synchronous in-memory path, which costs no extra microtask per
701
+ request.
702
+
421
703
  ## Durable Operations
422
704
 
423
705
  Long-running work — an image render, a batch, anything asynchronous — runs through one lifecycle:
@@ -427,6 +709,11 @@ covering the rest. `OperationRunner` owns it end to end, so a crashed worker doe
427
709
  The default store is in-process, so the small case needs no infrastructure:
428
710
 
429
711
  ```ts
712
+ import { BatchManager } from 'nexus-ai-pro/batch';
713
+ import { OpenAIBatchProvider } from 'nexus-ai-pro/batch/openai';
714
+ import { FilesystemAssetStore, S3AssetStore } from 'nexus-ai-pro/images/stores';
715
+ import { CircuitBreaker } from 'nexus-ai-pro/ops/circuit-breaker';
716
+ import { RedisRateLimitStore } from 'nexus-ai-pro/ops/rate-limit-adapters';
430
717
  import { OperationRunner } from 'nexus-ai-pro/operations';
431
718
 
432
719
  const runner = new OperationRunner<string>({ retry: { maxAttempts: 3, baseDelayMs: 500 } });
@@ -1385,12 +1672,33 @@ npm run test:conformance:real
1385
1672
 
1386
1673
  ## Roadmap
1387
1674
 
1675
+ See [ROADMAP.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) for what has
1676
+ shipped and what is planned. It is a design proposal, not a compatibility promise; the guarantees
1677
+ live in [API_STABILITY.md](./API_STABILITY.md).
1678
+
1388
1679
  1.4.0 closed the completion-request gap: prompt caching, reasoning controls, tool and sampling
1389
1680
  controls, structured usage with numeric cost, and capability negotiation with registry provenance.
1390
1681
 
1391
1682
  Next is durable execution — an operation state machine, provider batch APIs, distributed rate
1392
1683
  limiting and circuit breaking, first-class embeddings, and filesystem/S3 asset stores — followed by
1393
- image portability and promotion out of experimental. See [ROADMAP.md](./ROADMAP.md).
1684
+ image portability and promotion out of experimental. See
1685
+ [ROADMAP.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md).
1686
+
1687
+ ## Known Limitations
1688
+
1689
+ - Voice, telephony, images, local models, and custom providers are optional layers; enable only what
1690
+ you use.
1691
+ - Audio and video preprocessing is limited, and provider modality support differs.
1692
+ - Bundled model metadata is a routing and estimation convenience, not a pricing contract.
1693
+ - Process-memory cache, queues, and asset storage are not enough for a distributed deployment; use
1694
+ the Redis, BullMQ, filesystem, or S3 adapters instead.
1695
+ - Default hash embeddings suit tests and demos, not strong semantic search. Register a real
1696
+ embedding provider for production retrieval.
1697
+ - NLI verification is an interface; bring a specialized verifier for high-confidence entailment.
1698
+ - Guardrails reduce risk but do not replace application authorization, provider-side moderation, or
1699
+ human review of high-impact actions.
1700
+ - Circuit-breaker state is per process, and realtime sessions are not yet routed through the metrics,
1701
+ audit, and rate-limit path that every other family uses.
1394
1702
 
1395
1703
  ## Production Notes
1396
1704
 
package/SECURITY.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Supported versions
4
4
 
5
- Security fixes are provided for the latest published minor release. Users should upgrade to the newest `0.9.x` patch as fixes become available.
5
+ Security fixes are provided for the latest published minor release. Upgrade to the newest patch of that line as fixes become available; older minor releases do not receive backports.
6
6
 
7
7
  ## Reporting a vulnerability
8
8
 
@@ -0,0 +1,26 @@
1
+ import type { BatchJobRef, BatchJobState, BatchOutputItem, BatchProvider, BatchProviderCallContext, BatchProviderInfo, BatchSubmitRequest } from '../types/batch.js';
2
+ export interface AnthropicBatchProviderOptions {
3
+ apiKey: string;
4
+ baseUrl?: string;
5
+ version?: string;
6
+ headers?: Record<string, string>;
7
+ fetch?: typeof fetch;
8
+ /** Applied to any item that names no model. */
9
+ defaultModel?: string;
10
+ maxTokens?: number;
11
+ }
12
+ /** Anthropic Message Batches: half price, 24-hour window. */
13
+ export declare class AnthropicBatchProvider implements BatchProvider {
14
+ private readonly options;
15
+ readonly info: BatchProviderInfo;
16
+ constructor(options: AnthropicBatchProviderOptions);
17
+ submit(request: BatchSubmitRequest, context: BatchProviderCallContext): Promise<BatchJobRef>;
18
+ poll(ref: BatchJobRef, context: BatchProviderCallContext): Promise<BatchJobState>;
19
+ results(ref: BatchJobRef, context: BatchProviderCallContext): Promise<BatchOutputItem[]>;
20
+ cancel(ref: BatchJobRef, context: BatchProviderCallContext): Promise<BatchJobState>;
21
+ private toState;
22
+ private toOutputItem;
23
+ private request;
24
+ private requestText;
25
+ private send;
26
+ }