nexus-ai-pro 2.0.0 → 2.1.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 (130) hide show
  1. package/API_STABILITY.md +27 -0
  2. package/CHANGELOG.md +123 -1
  3. package/MIGRATING.md +1 -1
  4. package/README.md +8 -4
  5. package/dist/agent/create-agent.d.ts +9 -1
  6. package/dist/agent/create-agent.js +40 -1
  7. package/dist/cli/graph.d.ts +9 -0
  8. package/dist/cli/graph.js +47 -0
  9. package/dist/cli.js +9 -1
  10. package/dist/core/nexus.d.ts +6 -2
  11. package/dist/core/nexus.js +3 -1
  12. package/dist/evals/judge.d.ts +11 -0
  13. package/dist/evals/judge.js +33 -0
  14. package/dist/evaluate/index.d.ts +1 -0
  15. package/dist/evaluate/index.js +1 -0
  16. package/dist/evaluate/provenance.d.ts +22 -0
  17. package/dist/evaluate/provenance.js +10 -0
  18. package/dist/evaluate/run.js +20 -4
  19. package/dist/graph/durability.d.ts +27 -0
  20. package/dist/graph/durability.js +66 -0
  21. package/dist/graph/errors.d.ts +30 -4
  22. package/dist/graph/errors.js +18 -2
  23. package/dist/graph/event-stream.d.ts +75 -0
  24. package/dist/graph/event-stream.js +289 -0
  25. package/dist/graph/graph.d.ts +41 -3
  26. package/dist/graph/graph.js +270 -40
  27. package/dist/graph/index.d.ts +4 -2
  28. package/dist/graph/index.js +3 -1
  29. package/dist/graph/lint.d.ts +57 -0
  30. package/dist/graph/lint.js +141 -0
  31. package/dist/graph/run-control.d.ts +28 -0
  32. package/dist/graph/run-control.js +26 -0
  33. package/dist/operations/runner.js +13 -15
  34. package/dist/ops/health.d.ts +24 -0
  35. package/dist/ops/health.js +38 -10
  36. package/dist/ops/index.d.ts +1 -1
  37. package/dist/ops/index.js +1 -1
  38. package/dist/ops/rate-limit-adapters.d.ts +23 -0
  39. package/dist/ops/rate-limit-adapters.js +34 -0
  40. package/dist/ops/rate-limiter.d.ts +12 -2
  41. package/dist/ops/rate-limiter.js +29 -0
  42. package/dist/server/assistant.d.ts +40 -0
  43. package/dist/server/assistant.js +31 -0
  44. package/dist/server/runs.d.ts +4 -0
  45. package/dist/server/runs.js +21 -1
  46. package/dist/testing/record.d.ts +42 -0
  47. package/dist/testing/record.js +57 -5
  48. package/dist/tracing/index.d.ts +3 -3
  49. package/dist/tracing/index.js +2 -2
  50. package/dist/tracing/instrument.js +28 -0
  51. package/dist/tracing/otlp.d.ts +91 -0
  52. package/dist/tracing/otlp.js +124 -0
  53. package/dist/tracing/rollups.d.ts +100 -0
  54. package/dist/tracing/rollups.js +139 -0
  55. package/dist/tracing/stores.d.ts +17 -0
  56. package/dist/tracing/stores.js +23 -0
  57. package/dist/tracing/tracer.d.ts +50 -4
  58. package/dist/tracing/tracer.js +85 -6
  59. package/dist/types/config.d.ts +9 -0
  60. package/dist/types/evaluate.d.ts +37 -0
  61. package/dist/types/graph.d.ts +343 -8
  62. package/dist/types/graph.js +3 -1
  63. package/dist/types/operations.d.ts +16 -2
  64. package/dist/types/server.d.ts +9 -0
  65. package/dist/types/tracing.d.ts +11 -0
  66. package/dist/version.d.ts +5 -0
  67. package/dist/version.js +1 -0
  68. package/dist-cjs/agent/create-agent.d.ts +9 -1
  69. package/dist-cjs/agent/create-agent.js +40 -1
  70. package/dist-cjs/cli/graph.d.ts +9 -0
  71. package/dist-cjs/cli/graph.js +50 -0
  72. package/dist-cjs/core/nexus.d.ts +6 -2
  73. package/dist-cjs/core/nexus.js +3 -1
  74. package/dist-cjs/evals/judge.d.ts +11 -0
  75. package/dist-cjs/evals/judge.js +33 -0
  76. package/dist-cjs/evaluate/index.d.ts +1 -0
  77. package/dist-cjs/evaluate/index.js +3 -1
  78. package/dist-cjs/evaluate/provenance.d.ts +22 -0
  79. package/dist-cjs/evaluate/provenance.js +14 -0
  80. package/dist-cjs/evaluate/run.js +20 -4
  81. package/dist-cjs/graph/durability.d.ts +27 -0
  82. package/dist-cjs/graph/durability.js +70 -0
  83. package/dist-cjs/graph/errors.d.ts +30 -4
  84. package/dist-cjs/graph/errors.js +20 -3
  85. package/dist-cjs/graph/event-stream.d.ts +75 -0
  86. package/dist-cjs/graph/event-stream.js +295 -0
  87. package/dist-cjs/graph/graph.d.ts +41 -3
  88. package/dist-cjs/graph/graph.js +269 -39
  89. package/dist-cjs/graph/index.d.ts +4 -2
  90. package/dist-cjs/graph/index.js +7 -1
  91. package/dist-cjs/graph/lint.d.ts +57 -0
  92. package/dist-cjs/graph/lint.js +144 -0
  93. package/dist-cjs/graph/run-control.d.ts +28 -0
  94. package/dist-cjs/graph/run-control.js +30 -0
  95. package/dist-cjs/operations/runner.js +13 -15
  96. package/dist-cjs/ops/health.d.ts +24 -0
  97. package/dist-cjs/ops/health.js +38 -10
  98. package/dist-cjs/ops/index.d.ts +1 -1
  99. package/dist-cjs/ops/rate-limit-adapters.d.ts +23 -0
  100. package/dist-cjs/ops/rate-limit-adapters.js +35 -1
  101. package/dist-cjs/ops/rate-limiter.d.ts +12 -2
  102. package/dist-cjs/ops/rate-limiter.js +30 -0
  103. package/dist-cjs/server/assistant.d.ts +40 -0
  104. package/dist-cjs/server/assistant.js +31 -0
  105. package/dist-cjs/server/runs.d.ts +4 -0
  106. package/dist-cjs/server/runs.js +21 -1
  107. package/dist-cjs/testing/record.d.ts +42 -0
  108. package/dist-cjs/testing/record.js +57 -5
  109. package/dist-cjs/tracing/index.d.ts +3 -3
  110. package/dist-cjs/tracing/index.js +4 -1
  111. package/dist-cjs/tracing/instrument.js +28 -0
  112. package/dist-cjs/tracing/otlp.d.ts +91 -0
  113. package/dist-cjs/tracing/otlp.js +129 -0
  114. package/dist-cjs/tracing/rollups.d.ts +100 -0
  115. package/dist-cjs/tracing/rollups.js +147 -0
  116. package/dist-cjs/tracing/stores.d.ts +17 -0
  117. package/dist-cjs/tracing/stores.js +24 -0
  118. package/dist-cjs/tracing/tracer.d.ts +50 -4
  119. package/dist-cjs/tracing/tracer.js +87 -6
  120. package/dist-cjs/types/config.d.ts +9 -0
  121. package/dist-cjs/types/evaluate.d.ts +37 -0
  122. package/dist-cjs/types/graph.d.ts +343 -8
  123. package/dist-cjs/types/graph.js +3 -1
  124. package/dist-cjs/types/operations.d.ts +16 -2
  125. package/dist-cjs/types/server.d.ts +9 -0
  126. package/dist-cjs/types/tracing.d.ts +11 -0
  127. package/dist-cjs/version.d.ts +5 -0
  128. package/dist-cjs/version.js +4 -0
  129. package/llms.txt +2 -2
  130. package/package.json +40 -1
package/API_STABILITY.md CHANGED
@@ -41,6 +41,33 @@ Both are covered by this policy:
41
41
  Deep imports into `dist`, `dist-cjs`, or `src` are not supported. Import only from `nexus-ai-pro` or
42
42
  one of its explicit subpaths.
43
43
 
44
+ ## 2.1 stage (2.1.0)
45
+
46
+ Everything 2.1 adds is stable from the start and additive: no default changes meaning.
47
+
48
+ - graph durability modes, `flush()`, `maxPendingWrites`, `onError` with `NodeFailure` and
49
+ `RecoveredFailure`, `nodeDefaults`, `NodeTimeout`, `context.heartbeat()`, `SendOptions`,
50
+ `RunControl` and `RunControlLike`, and `GraphDrainedError` (`nexus-ai-pro/graph`);
51
+ - `lintGraph()` and its finding types (`nexus-ai-pro/graph/lint`). New rules may be added in a minor
52
+ release; a rule's code and severity do not change within 2.x;
53
+ - `OperationContext.previousHeartbeat` and `OperationRecord.heartbeatDetails`;
54
+ - `AssistantRunContext.control`;
55
+ - the graph event stream: `events()`, `resumeEvents()`, `continueEvents()`, `GraphEventStream`, its
56
+ event and option types, `GraphStreamOverflowError`, `context.message()` and `context.tool()`, and
57
+ `subgraphEvents`. New projections may be added in a minor release;
58
+ - `TracerOptions.incremental` and `exporters`, `TraceExporter`, `traceparent` in and out, `flush()`,
59
+ `closeAbandonedRuns()`, `w3cTraceId()` and `w3cSpanId()` (`nexus-ai-pro/tracing`);
60
+ - `OtlpTraceExporter` and `runToOtlpSpan()` (`nexus-ai-pro/tracing/otlp`). Attributes may be added as
61
+ the GenAI conventions settle; the ones listed in the tracing guide stay;
62
+ - rollups (`nexus-ai-pro/tracing/rollups`), timed replay (`timing`, `pace`), evaluator provenance
63
+ (`withProvenance()`, `Experiment.evaluators` and `framework`, `LLMJudge.asEvaluator()`),
64
+ `createAgent({ streamTokens })`, and `graphAssistant(graph, { events })`;
65
+ - `rateLimit.algorithm` and `burst`, `RateLimitStore.gcra()`, `RateLimitDecision`, and `gcraDecide()`;
66
+ `health.observationTtlMs`, `ProviderHealthStatus`, a snapshot's `status` and `stale`, and
67
+ `checkProviders({ staleOnly })`.
68
+
69
+ The studio's `rollups` source follows the studio's experimental label.
70
+
44
71
  ## 2.0 stage (2.0.0)
45
72
 
46
73
  2.0 removes what 1.x deprecated and promotes the surfaces that have settled.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,127 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.1.0] - 2026-10-04
8
+
9
+ Fault-tolerant graphs and crash-safe observability. A graph now chooses when its checkpoints are
10
+ written, recovers from a node whose retries ran out, tells a hung task from a slow one, and stops
11
+ cleanly when its worker has to go. Traces survive the process that wrote them and reach any
12
+ OpenTelemetry backend, every consumer of a run reads one event stream, and dashboards read hourly
13
+ totals instead of every trace. Nothing changes until it is opted into: the defaults keep their 2.0
14
+ meaning.
15
+
16
+ Two pieces planned for 2.3 come early: a rate limiter with no window edge to burst across, and
17
+ provider health that expires.
18
+
19
+ ### Added
20
+
21
+ - **Durability modes.** `compile({ durability })` takes `sync` (the default), `async`, or `exit`.
22
+ `async` writes checkpoints in the background, in order, while the next superstep runs, and waits
23
+ for them before the run returns, pauses, fails, or drains. `maxPendingWrites` (8) bounds the writes
24
+ in flight. `exit` writes only where the run stops. `flush()` waits for every run's writes.
25
+ - **Recovery after retries.** A node's `onError` receives a `NodeFailure` once its retries run out
26
+ and returns an update or a `Command`, such as a route to a refund node. The decision is
27
+ checkpointed, as `recovered` on the next checkpoint, before anything it routes to runs, so a crash
28
+ after it resumes into the same path. `compile({ nodeDefaults })` sets retries, timeouts, and
29
+ `onError` for every node.
30
+ - **Run and idle timeouts.** `timeout: { runMs, idleMs }` caps an attempt and the time between signs
31
+ of progress. `context.heartbeat()`, `emit()`, and `report()` refresh the idle timer.
32
+ `GraphNodeTimeoutError.kind` says which limit fired. A `Send` can override the timeout of the task
33
+ it creates, and the override is checkpointed with the task.
34
+ - **Cooperative drain.** `RunControl`, passed as `control` to a run, stops it after its current
35
+ superstep with its checkpoint written, ending in `GraphDrainedError`; `continue()` resumes it
36
+ anywhere. The agent server drains its graph runs this way when a replica drains with a queue, so
37
+ they are handed off at once instead of after the drain timeout.
38
+ - **`lintGraph()`** on the new `nexus-ai-pro/graph/lint`, and `nexus graph lint`: static cycles,
39
+ retried nodes that are not `idempotent` or have no timeout, deferred durability with side effects,
40
+ an in-process checkpointer under a deployment, unbounded concurrency, and unchecked routes. Nodes
41
+ can declare `idempotent: true`, and `describe()` reports durability, the checkpointer, and each
42
+ node's retries, timeouts, and recovery.
43
+ - **Progress that survives a retry.** An operation's `context.heartbeat(details)` stores `details` as
44
+ `heartbeatDetails`, and the next attempt reads them as `context.previousHeartbeat`.
45
+ - A `task_failed` graph event, and a semantics table in the graph guide that says what each
46
+ execution primitive repeats after a crash.
47
+ - **Crash-safe traces.** `new Tracer({ incremental: true })` writes each run when it starts and when it
48
+ ends, so a crashed run stays in the store with every run beneath it that finished, and a live run
49
+ can be watched. Sampling is decided at the root's start, and an unsampled trace is still kept whole
50
+ when tail sampling wants it. `closeAbandonedRuns()` closes what a dead process left running, as a
51
+ `RunAbandoned` error. `tracer.flush()` waits for writes and exports.
52
+ - **Traces across processes.** `handle.traceparent()` and `StartRunOptions.traceparent` carry a trace
53
+ through a W3C `traceparent` header: an operation's `traceContext`, a remote call, any OpenTelemetry
54
+ service. `w3cTraceId()` and `w3cSpanId()` give the ids every process derives alike.
55
+ - **OpenTelemetry export**, on the new `nexus-ai-pro/tracing/otlp`. `TracerOptions.exporters` takes any
56
+ `TraceExporter`, and `OtlpTraceExporter` sends runs to a collector as OTLP/HTTP JSON with no SDK:
57
+ model calls as `chat <model>` client spans with the GenAI semantic conventions, tools, agents, and
58
+ graph nodes with theirs, and Nexus-only facts under `nexus.*`. `runToOtlpSpan()` is the conversion.
59
+ - **One event stream.** `graph.events()`, `resumeEvents()`, and `continueEvents()` run a graph as
60
+ typed events with projections — `values`, `updates`, `messages`, `tools`, `tasks`, `checkpoints`,
61
+ `custom` — read together or one at a time through `messages()`, `tools()`, `values()`, and
62
+ `updates()`, with `result` for the outcome. Nodes stream model output with `context.message()`
63
+ and report tool calls with `context.tool()`. Subgraph events come through with a `namespace` when
64
+ asked, and `GraphRunOptions.subgraphEvents` does the same for `onEvent`.
65
+ - **Bounded readers.** Every reader of a stream has its own buffer (`maxBuffered`, 1,000), and the run
66
+ waits for the slowest at each superstep. Within a step, `overflow` coalesces model chunks and state
67
+ snapshots (the default), drops the oldest or newest, or fails with `GraphStreamOverflowError`.
68
+ `streamStats()` reports the peak and what was merged or dropped.
69
+ - **Agents stream.** `createAgent()` reports its model's output and every tool call on the stream,
70
+ token by token with `streamTokens` and a client with `stream()`. `traceGraph()` records the tool
71
+ calls as `tool` runs inside their node. `graphAssistant(graph, { events })` records chosen
72
+ projections on each server run.
73
+ - **Rollups**, on the new `nexus-ai-pro/tracing/rollups`: `rollupTraceStore()` adds every finished run
74
+ to hourly rows per kind, model, provider, and tenant, in `MemoryRollupStore` or any `RollupStore`,
75
+ with latency buckets for `rollupPercentile()` and `sumRollups()` to total them. The studio's costs
76
+ view reads them when given `rollups`.
77
+ - **Timed replay.** `recordingFetch({ timing: true })` keeps each chunk of a response and when it came,
78
+ and `replayFetch({ pace })` replays it at once, at the original pace, scaled, or released chunk by
79
+ chunk by a function.
80
+ - **GCRA rate limiting.** `rateLimit.algorithm: 'gcra'` spreads calls at `maxRequests` per `windowMs`
81
+ with at most `burst` at once, so a burst across a window edge can no longer pass twice the limit.
82
+ Both stores decide it through `gcra()`; `RedisRateLimitStore` runs it as one Lua script on Redis's
83
+ clock. `gcraDecide()` is the arithmetic, for a store of your own.
84
+ - **Health that expires.** `health.observationTtlMs` makes a provider with no recent calls `unknown`,
85
+ routed to as one never seen instead of kept down by an old failure or up by an old success.
86
+ Snapshots carry a `status` (`healthy`, `degraded`, `unhealthy`, `unknown`) and `stale`, and
87
+ `ai.checkProviders({ staleOnly: true })` refreshes the unknown ones.
88
+ - **Evaluator provenance.** An experiment records each evaluator's position, name, the score keys it
89
+ produced, and what `withProvenance()` declared — version, judge model, prompt version, temperature,
90
+ and rubric — and the framework version that ran it. `LLMJudge.asEvaluator()` brings the judge to
91
+ `evaluate()`, declaring its own provenance.
92
+ - A weekly `live-providers` workflow runs real conformance against every provider with a key in the
93
+ repository's secrets, DeepSeek included, and opens an issue when one breaks.
94
+
95
+ ### Fixed
96
+
97
+ - **A manual operation heartbeat no longer strands the result.** `context.heartbeat()` wrote the
98
+ record behind the runner's back, so the runner's final write failed its sequence check and a
99
+ successful result was not saved. Heartbeats now go through the runner, one at a time.
100
+ - `continue()` on a finished thread left it marked `running` with an empty result. It now leaves it
101
+ completed and returns its state.
102
+ - A run started under a parent from another tracer, or another process, was never written: its trace
103
+ waited for a root that was not there. It is now written as its trace's root in this process.
104
+
105
+ ### Proof
106
+
107
+ - A crash-injection suite kills a run before, during, and after every checkpoint write and inside
108
+ nodes, before and after their side effects, in `sync` and `async` mode, on the memory, operation
109
+ store, SQLite, Postgres, and Redis checkpointers. A fresh process always reaches the state of an
110
+ uninterrupted run.
111
+ - A saga test: a charge exhausts its retries, `onError` routes to a refund, the process dies during
112
+ the refund, and the resumed run refunds without charging again.
113
+ - A property test generates 40 graphs with branches, loops, `Send`, retries, and interrupts, and
114
+ replays each from every checkpoint of its history to the same state.
115
+ - The graph benchmark runs 50 supersteps against an 8 ms store: `async` finishes in about half the
116
+ time of `sync`, in the same state, and CI fails if it stops beating it by a quarter.
117
+ - A long agent's process dies at its fourteenth step: a second process reads every finished step from
118
+ the same JSONL store, and `closeAbandonedRuns()` closes the rest.
119
+ - A server and a worker each export to a local OTLP collector, and the collector receives one trace,
120
+ the worker's run under the server's, with the GenAI attributes on the model call.
121
+ - A reader a hundred times slower than a node that streams 100,000 chunks never holds more than its
122
+ buffer, under every overflow policy, and coalescing loses no text.
123
+ - The studio's costs view answers over a month of a million runs, held as rollups, in under 200 ms,
124
+ reading the traces once.
125
+ - In the two milliseconds around a window's edge, fixed windows pass 199 calls of a 100-call limit and
126
+ GCRA passes 100. A provider down on Monday and idle since is `unknown`, and routed to, on Wednesday.
127
+
7
128
  ## [2.0.0] - 2026-10-02
8
129
 
9
130
  Consolidation. One lifecycle runs every call of every family, the root import holds the core client
@@ -1373,7 +1494,8 @@ shared policy vocabulary is in place for a future release that revisits this.
1373
1494
  - URL fetching rejects unsafe private-network targets and limits response reads.
1374
1495
  - CLI and audit findings no longer disclose detected secret values.
1375
1496
 
1376
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.0.0...HEAD
1497
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.1.0...HEAD
1498
+ [2.1.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.0.0...v2.1.0
1377
1499
  [2.0.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.25.0...v2.0.0
1378
1500
  [1.25.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.24.0...v1.25.0
1379
1501
  [1.24.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.23.0...v1.24.0
package/MIGRATING.md CHANGED
@@ -69,7 +69,7 @@ It cannot split a namespace import (`import * as nexus from 'nexus-ai-pro'`) or
69
69
  so it lists them. Import each name you use from its subpath instead. Every subpath, and what it costs,
70
70
  is in the [packaging guide](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md).
71
71
 
72
- The root import now costs about 323 KB, down from 572 KB.
72
+ The root import now costs about 325 KB, down from 572 KB.
73
73
 
74
74
  ## Validators and Node types are optional
75
75
 
package/README.md CHANGED
@@ -8,7 +8,7 @@ team can share.
8
8
  Import only what you use. The root import is the client and nothing else, and every other capability
9
9
  has its own entry point with a size budget CI enforces; [the packaging guide](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md)
10
10
  publishes what each one costs. The package has no required dependencies: a graph-only application
11
- loads 51 KB, and installs nothing but this package.
11
+ loads 73 KB, and installs nothing but this package.
12
12
 
13
13
  - NPM: https://www.npmjs.com/package/nexus-ai-pro
14
14
  - GitHub: https://github.com/mkhitar-abrahamyan/nexus-ai
@@ -243,12 +243,16 @@ Shipped so far:
243
243
  a codemod that moves your imports;
244
244
  - 2.0 consolidation: one lifecycle for every call of every family, a shared spend budget, a root
245
245
  import that holds only the client, no required dependencies, and a model registry checked against
246
- every provider.
246
+ every provider;
247
+ - 2.1 fault tolerance and crash-safe observability: checkpoints written on your terms, recovery after
248
+ retries, idle timeouts and a clean drain, traces that survive a crash and reach any OpenTelemetry
249
+ backend, one event stream, and dashboard rollups.
247
250
 
248
251
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
249
252
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
250
- Next, the image family leaves experimental once recorded live conformance passes on all three
251
- backends.
253
+ Next, 2.2 removes the scaling cliffs the guides document: versioned database migrations, indexed
254
+ queue dispatch, a circuit breaker that counts across workers, a routing index, and one tenant model. The [roadmap](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/ROADMAP.md) lists the
255
+ releases after it.
252
256
 
253
257
  ## Before Production
254
258
 
@@ -2,7 +2,7 @@ import type { OperationLifecycleLike } from '../types/lifecycle.js';
2
2
  import { type CompiledGraph } from '../graph/graph.js';
3
3
  import type { GraphCheckpointer, NodeContext } from '../types/graph.js';
4
4
  import type { CompletionRequest, Message, ToolDefinition } from '../types/messages.js';
5
- import type { NexusResponse } from '../types/response.js';
5
+ import type { NexusResponse, StreamChunk } from '../types/response.js';
6
6
  import type { Store } from '../types/store.js';
7
7
  /**
8
8
  * An agent as a graph.
@@ -17,6 +17,8 @@ import type { Store } from '../types/store.js';
17
17
  export interface AgentModelClient {
18
18
  /** Runs one completion. */
19
19
  complete(request: CompletionRequest): Promise<NexusResponse>;
20
+ /** Streams one completion. Used when the agent is created with `streamTokens`. */
21
+ stream?(request: CompletionRequest): AsyncIterable<StreamChunk>;
20
22
  }
21
23
  /** A tool call the agent is about to make. */
22
24
  export interface AgentToolCall {
@@ -108,6 +110,12 @@ export interface CreateAgentOptions {
108
110
  * `ai.lifecycle`. Its model calls are operations of their own.
109
111
  */
110
112
  lifecycle?: OperationLifecycleLike;
113
+ /**
114
+ * Streams the model's output token by token onto the run's `messages` events, through the client's
115
+ * `stream()`. Without it, or with a client that cannot stream, each answer arrives as one message
116
+ * event once the model returns. Middleware sees the assembled response either way.
117
+ */
118
+ streamTokens?: boolean;
111
119
  }
112
120
  declare const agentChannels: () => {
113
121
  messages: import("../types/graph.js").Channel<Message[]>;
@@ -37,7 +37,12 @@ export function createAgent(options) {
37
37
  for (const item of middleware) {
38
38
  request = (await item.beforeModel?.({ request, state, store: options.store })) ?? request;
39
39
  }
40
- let response = await options.client.complete(request);
40
+ let response = options.streamTokens && options.client.stream
41
+ ? await streamed(options.client.stream(request), (chunk) => context.message(chunk))
42
+ : await options.client.complete(request);
43
+ if (!(options.streamTokens && options.client.stream) && response.content) {
44
+ context.message({ content: response.content });
45
+ }
41
46
  for (const item of middleware) {
42
47
  response = (await item.afterModel?.({ response, state, store: options.store })) ?? response;
43
48
  }
@@ -88,9 +93,13 @@ export function createAgent(options) {
88
93
  args = verdict.args;
89
94
  }
90
95
  const run = async () => executor.execute(description.name, args);
96
+ context.tool({ phase: 'start', id: call.id, name: description.name, args });
91
97
  const result = await middleware.reduceRight((next, item) => item.wrapToolCall
92
98
  ? () => Promise.resolve(item.wrapToolCall?.({ ...description, args }, next))
93
99
  : next, run)();
100
+ context.tool(result.ok
101
+ ? { phase: 'result', id: call.id, name: description.name, result: result.result }
102
+ : { phase: 'error', id: call.id, name: description.name, error: result.error ?? 'The tool failed' });
94
103
  return { messages: [toolMessage(call, result)] };
95
104
  }, { ends: ['model'] })
96
105
  .setEntry('model')
@@ -156,3 +165,33 @@ function parseArguments(raw) {
156
165
  return { ok: false, error: `Tool arguments are not valid JSON (${reason}); the tool was not run` };
157
166
  }
158
167
  }
168
+ async function streamed(chunks, onChunk) {
169
+ let content = '';
170
+ const toolCalls = [];
171
+ let meta = {};
172
+ for await (const chunk of chunks) {
173
+ if (chunk.type === 'text' && chunk.content) {
174
+ content += chunk.content;
175
+ onChunk({ kind: 'text', content: chunk.content });
176
+ }
177
+ else if (chunk.type === 'reasoning' && chunk.content) {
178
+ onChunk({ kind: 'reasoning', content: chunk.content });
179
+ }
180
+ else if (chunk.type === 'tool_call' && chunk.toolCall) {
181
+ toolCalls.push(chunk.toolCall);
182
+ }
183
+ else if (chunk.type === 'done') {
184
+ meta = chunk.meta ?? meta;
185
+ }
186
+ else if (chunk.type === 'error') {
187
+ throw new Error(chunk.error ?? 'The model stream failed');
188
+ }
189
+ }
190
+ return {
191
+ content,
192
+ role: 'assistant',
193
+ ...(toolCalls.length ? { toolCalls } : {}),
194
+ finishReason: toolCalls.length ? 'tool_calls' : 'stop',
195
+ meta: meta,
196
+ };
197
+ }
@@ -0,0 +1,9 @@
1
+ import { type ParsedArgs } from './args.js';
2
+ /**
3
+ * `nexus graph lint <module>[#export]`.
4
+ *
5
+ * The module exports a compiled graph, or a description of one as `describe()` returns it, as the
6
+ * named export, as `graph`, or as its default. Exits 1 when a finding is an error, or with `--strict`
7
+ * when any finding is a warning too.
8
+ */
9
+ export declare function runGraphCommand(subcommand: string | undefined, args: ParsedArgs): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import { lintGraph } from '../graph/lint.js';
2
+ import { CliUsageError, flagBool, isRecord, listFlag, loadModule, printTable, writeJson, } from './args.js';
3
+ export async function runGraphCommand(subcommand, args) {
4
+ if (subcommand !== 'lint') {
5
+ throw new CliUsageError(`Unknown graph command "${subcommand ?? ''}". Use lint.`);
6
+ }
7
+ const [target] = args.positionals;
8
+ if (!target)
9
+ throw new CliUsageError('nexus graph lint needs a module: nexus graph lint ./graph.mjs[#exportName].');
10
+ const [file, exportName] = target.split('#');
11
+ const loaded = await loadModule(file);
12
+ const graph = (exportName ? loaded[exportName] : (loaded.graph ?? loaded.default ?? loaded));
13
+ if (!isLintable(graph)) {
14
+ throw new CliUsageError(`${target} must export a compiled graph, or a graph description, ${exportName ? `as "${exportName}"` : 'as `graph` or its default'}.`);
15
+ }
16
+ const findings = lintGraph(graph, {
17
+ deployed: flagBool(args.flags, 'deployed'),
18
+ ignore: listFlag(args.flags, 'ignore'),
19
+ });
20
+ const failing = findings.filter((finding) => finding.severity === 'error' || (flagBool(args.flags, 'strict') && finding.severity === 'warning'));
21
+ if (flagBool(args.flags, 'json')) {
22
+ writeJson({ findings, failed: failing.length > 0 });
23
+ }
24
+ else if (findings.length === 0) {
25
+ console.log('No findings.');
26
+ }
27
+ else {
28
+ printTable(findings.map((finding) => ({
29
+ severity: finding.severity,
30
+ code: finding.code,
31
+ node: finding.node ?? '',
32
+ message: finding.message,
33
+ })), ['severity', 'code', 'node', 'message']);
34
+ console.log('');
35
+ for (const finding of findings)
36
+ console.log(`${finding.code}: ${finding.fix}`);
37
+ }
38
+ if (failing.length > 0)
39
+ process.exitCode = 1;
40
+ }
41
+ function isLintable(value) {
42
+ if (!isRecord(value))
43
+ return false;
44
+ if (typeof value.describe === 'function')
45
+ return true;
46
+ return Array.isArray(value.nodes) && Array.isArray(value.edges) && Array.isArray(value.dynamic);
47
+ }
package/dist/cli.js CHANGED
@@ -54,6 +54,12 @@ async function main() {
54
54
  await runMigrateCommand(parsed);
55
55
  return;
56
56
  }
57
+ case 'graph': {
58
+ const [subcommand, ...args] = parsed.positionals;
59
+ const { runGraphCommand } = await import('./cli/graph.js');
60
+ await runGraphCommand(subcommand, { positionals: args, flags: parsed.flags });
61
+ return;
62
+ }
57
63
  case 'deploy': {
58
64
  const [subcommand, ...args] = parsed.positionals;
59
65
  const { runDeployCommand } = await import('./cli/deploy.js');
@@ -402,6 +408,7 @@ Usage:
402
408
  nexus traces export --store <runs.jsonl|store.mjs> [--out runs.jsonl] [same filters as list]
403
409
  nexus db sql [--adapters operations,store,traces,evaluation,circuits,prompts] [--vector-dimensions 1536]
404
410
  nexus migrate [paths...] [--write] [--check] [--json]
411
+ nexus graph lint <module>[#export] [--deployed] [--strict] [--ignore CODE,CODE] [--json]
405
412
  nexus deploy status [assistant] --url <server> [--token t] [--json]
406
413
  nexus deploy canary <assistant> <revision> <percent> --url <server> [--reason r] [--version n]
407
414
  nexus deploy promote <assistant> <revision> --url <server> [--reason r] [--version n]
@@ -413,7 +420,8 @@ Evaluation modules for "eval run" export { target, dataset, evaluators, summary?
413
420
  "eval gate" exits 1 when a metric got worse beyond noise, a new failure appeared, or the datasets differ.
414
421
  Trace store modules export any TraceStore as { store }, such as a PostgresTraceStore over the application's own pool.
415
422
  "deploy" reads NEXUS_SERVER_URL and NEXUS_SERVER_TOKEN when --url and --token are not given.
416
- "migrate" moves 1.x root imports to their 2.0 subpaths and reports removed options; --check exits 1 while any remain.`);
423
+ "migrate" moves 1.x root imports to their 2.0 subpaths and reports removed options; --check exits 1 while any remain.
424
+ "graph lint" reads a compiled graph's shape and exits 1 on an error finding, or on a warning with --strict.`);
417
425
  }
418
426
  main().catch((error) => {
419
427
  const message = error instanceof Error ? error.message : String(error);
@@ -248,9 +248,13 @@ export declare class NexusAI {
248
248
  /** Health of every provider seen so far, when health tracking is on. */
249
249
  getProviderHealth(): import("../ops/health.js").ProviderHealthSnapshot[];
250
250
  /**
251
- * Calls each provider's health check and records the result.
251
+ * Calls each provider's health check and records the result. With `staleOnly`, only the providers
252
+ * whose health is `unknown` are checked: never called, or not since `health.observationTtlMs`. Run
253
+ * it on a schedule to keep an idle provider's health current.
252
254
  */
253
- checkProviders(): Promise<Array<{
255
+ checkProviders(options?: {
256
+ staleOnly?: boolean;
257
+ }): Promise<Array<{
254
258
  providerName: string;
255
259
  ok: boolean;
256
260
  error?: string;
@@ -547,9 +547,11 @@ export class NexusAI {
547
547
  getProviderHealth() {
548
548
  return this.health.snapshot();
549
549
  }
550
- async checkProviders() {
550
+ async checkProviders(options = {}) {
551
551
  const results = [];
552
552
  for (const [providerName, provider] of this.providers) {
553
+ if (options.staleOnly && !this.health.isUnknown(providerName))
554
+ continue;
553
555
  const started = Date.now();
554
556
  try {
555
557
  const ok = await provider.healthCheck();
@@ -1,3 +1,4 @@
1
+ import type { EvaluationContext, Evaluator } from '../types/evaluate.js';
1
2
  import type { CompletionRequest } from '../types/messages.js';
2
3
  import type { NexusResponse } from '../types/response.js';
3
4
  import type { EvalCase, EvalJudge } from './runner.js';
@@ -75,6 +76,16 @@ export declare class LLMJudge<Response = NexusResponse> {
75
76
  constructor(options: LLMJudgeOptions<Response>);
76
77
  /** Judges one answer. */
77
78
  evaluate(input: LLMJudgeInput): Promise<LLMJudgeResult>;
79
+ /**
80
+ * The judge as an evaluator for `evaluate()`, scoring each output under `key` (`judge` by
81
+ * default). `input` turns an evaluation context into what the judge sees; by default the output is
82
+ * the answer, the example's expected output the reference, and its inputs the question. The
83
+ * experiment records the judge's model, temperature, rubric, and a hash of its instructions.
84
+ */
85
+ asEvaluator(options?: {
86
+ key?: string;
87
+ input?: (context: EvaluationContext) => LLMJudgeInput;
88
+ }): Evaluator;
78
89
  /**
79
90
  * Adapts the judge to an `EvalRunner` case, optionally with a mapper that picks what the judge
80
91
  * sees.
@@ -1,3 +1,5 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { withProvenance } from '../evaluate/provenance.js';
1
3
  const DEFAULT_SYSTEM_PROMPT = [
2
4
  'You are a careful evaluation judge.',
3
5
  'Return only JSON with these fields: score, passed, rationale, labels.',
@@ -43,6 +45,34 @@ export class LLMJudge {
43
45
  modelUsed: modelOf(response),
44
46
  });
45
47
  }
48
+ asEvaluator(options = {}) {
49
+ const evaluator = async (context) => {
50
+ const input = options.input?.(context) ?? {
51
+ actual: text(context.output),
52
+ ...(context.example.expected === undefined ? {} : { expected: text(context.example.expected) }),
53
+ query: text(context.example.inputs),
54
+ };
55
+ const verdict = await this.evaluate(input);
56
+ return {
57
+ key: options.key ?? 'judge',
58
+ score: verdict.score,
59
+ passed: verdict.passed,
60
+ ...(verdict.rationale ? { comment: verdict.rationale } : {}),
61
+ };
62
+ };
63
+ return withProvenance(evaluator, {
64
+ name: options.key ?? 'judge',
65
+ judge: {
66
+ model: this.options.model,
67
+ promptVersion: createHash('sha256')
68
+ .update(this.options.systemPrompt ?? 'default')
69
+ .digest('hex')
70
+ .slice(0, 12),
71
+ temperature: this.options.temperature ?? 0,
72
+ },
73
+ ...(this.options.rubric ? { rubric: this.options.rubric } : {}),
74
+ });
75
+ }
46
76
  asEvalJudge(mapper) {
47
77
  return async (response, testCase) => {
48
78
  const input = mapper
@@ -150,3 +180,6 @@ function clamp(value, [min, max]) {
150
180
  function midpoint([min, max]) {
151
181
  return min + (max - min) / 2;
152
182
  }
183
+ function text(value) {
184
+ return typeof value === 'string' ? value : JSON.stringify(value ?? '');
185
+ }
@@ -5,3 +5,4 @@ export { AnnotationQueue, type AnnotationQueueOptions, evaluateOnline, type Onli
5
5
  export { evaluate, type EvaluateOptions, type EvaluationTarget, stats, summarize } from './run.js';
6
6
  export { type CachedOutput, type EvaluationCache, evaluationCacheKey, FileEvaluationCache, fingerprintOf, MemoryEvaluationCache, type MemoryEvaluationCacheOptions, } from './cache.js';
7
7
  export type { Dataset, DatasetExample, DatasetStore, EvaluationContext, EvaluationScore, Evaluator, ExampleResult, Experiment, ExperimentStore, MetricSummary, SummaryEvaluator, } from '../types/evaluate.js';
8
+ export { withProvenance } from './provenance.js';
@@ -4,3 +4,4 @@ export { completed, contains, embeddingSimilarity, exactMatch, mustNotMatch, pai
4
4
  export { AnnotationQueue, evaluateOnline, } from './review.js';
5
5
  export { evaluate, stats, summarize } from './run.js';
6
6
  export { evaluationCacheKey, FileEvaluationCache, fingerprintOf, MemoryEvaluationCache, } from './cache.js';
7
+ export { withProvenance } from './provenance.js';
@@ -0,0 +1,22 @@
1
+ import type { Evaluator, EvaluatorProvenance } from '../types/evaluate.js';
2
+ /**
3
+ * Says where an evaluator's scores come from: its name and version, the judge model and prompt
4
+ * version behind it, and its rubric. `evaluate()` records it on the experiment, so a score of 0.87
5
+ * can be traced to exactly what produced it, and two experiments scored by different judges are not
6
+ * compared as if they were alike.
7
+ *
8
+ * Returns the same evaluator, marked; it runs exactly as before.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const grounded = withProvenance(groundedness, {
13
+ * name: 'groundedness',
14
+ * version: '3',
15
+ * judge: { model: 'claude-sonnet-5-5', promptVersion: 'grounded-v3', temperature: 0 },
16
+ * rubric: 'Every claim is supported by a cited passage.',
17
+ * });
18
+ * ```
19
+ */
20
+ export declare function withProvenance<I = unknown, O = unknown>(evaluator: Evaluator<I, O>, provenance: Omit<EvaluatorProvenance, 'keys' | 'position'>): Evaluator<I, O>;
21
+ /** What an evaluator declared through `withProvenance()`, if anything. */
22
+ export declare function declaredProvenance(evaluator: unknown): Omit<EvaluatorProvenance, 'keys' | 'position'> | undefined;
@@ -0,0 +1,10 @@
1
+ const PROVENANCE = Symbol('nexus.evaluate.provenance');
2
+ export function withProvenance(evaluator, provenance) {
3
+ Object.defineProperty(evaluator, PROVENANCE, { value: provenance, configurable: true });
4
+ return evaluator;
5
+ }
6
+ export function declaredProvenance(evaluator) {
7
+ if (typeof evaluator !== 'function')
8
+ return undefined;
9
+ return evaluator[PROVENANCE];
10
+ }
@@ -1,5 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { NEXUS_VERSION } from '../version.js';
2
3
  import { evaluationCacheKey } from './cache.js';
4
+ import { declaredProvenance } from './provenance.js';
3
5
  export async function evaluate(target, dataset, evaluators, options = {}) {
4
6
  if (dataset.examples.length === 0)
5
7
  throw new RangeError(`Dataset "${dataset.name}" has no examples`);
@@ -14,8 +16,9 @@ export async function evaluate(target, dataset, evaluators, options = {}) {
14
16
  example,
15
17
  run,
16
18
  })));
19
+ const produced = evaluators.map(() => new Set());
17
20
  await runConcurrently(jobs, options.concurrency ?? 4, async ({ example, run }) => {
18
- const result = await evaluateOne(target, example, run, evaluators, options);
21
+ const result = await evaluateOne(target, example, run, evaluators, options, produced);
19
22
  results.push(result);
20
23
  options.onResult?.(result);
21
24
  }, options.signal);
@@ -35,6 +38,16 @@ export async function evaluate(target, dataset, evaluators, options = {}) {
35
38
  results,
36
39
  metrics: summarize(results),
37
40
  summary,
41
+ evaluators: evaluators.map((evaluator, position) => {
42
+ const declared = declaredProvenance(evaluator);
43
+ return {
44
+ ...declared,
45
+ position,
46
+ name: declared?.name ?? (evaluator.name || `evaluator-${position}`),
47
+ keys: [...(produced[position] ?? [])],
48
+ };
49
+ }),
50
+ framework: { name: 'nexus-ai-pro', version: NEXUS_VERSION },
38
51
  ...(options.metadata ? { metadata: options.metadata } : {}),
39
52
  ...(options.cache
40
53
  ? {
@@ -57,7 +70,7 @@ function repetitionsOf(example, setting) {
57
70
  }
58
71
  return count;
59
72
  }
60
- async function evaluateOne(target, example, run, evaluators, options) {
73
+ async function evaluateOne(target, example, run, evaluators, options, produced) {
61
74
  const started = Date.now();
62
75
  let output;
63
76
  let error;
@@ -97,9 +110,12 @@ async function evaluateOne(target, example, run, evaluators, options) {
97
110
  });
98
111
  }
99
112
  const scores = [];
100
- for (const evaluator of evaluators) {
113
+ for (const [position, evaluator] of evaluators.entries()) {
101
114
  try {
102
- scores.push(...normalizeScores(await evaluator(context), evaluator));
115
+ const made = normalizeScores(await evaluator(context), evaluator);
116
+ for (const score of made)
117
+ produced[position]?.add(score.key);
118
+ scores.push(...made);
103
119
  }
104
120
  catch (caught) {
105
121
  scores.push({
@@ -0,0 +1,27 @@
1
+ import type { DurabilityMode } from '../types/graph.js';
2
+ /**
3
+ * Writes one run's checkpoints under a durability mode.
4
+ *
5
+ * `sync` writes each one before returning. `async` chains writes behind each other, so they land in
6
+ * order, and makes the run wait only when `maxPending` are already in flight. `exit` keeps only the
7
+ * latest and writes it when the run stops. A write that fails is kept and thrown from the next call,
8
+ * so a background failure still fails the run.
9
+ */
10
+ export declare class CheckpointWriter<T> {
11
+ private readonly write;
12
+ private readonly mode;
13
+ private readonly maxPending;
14
+ private chain;
15
+ private readonly inFlight;
16
+ private failure;
17
+ private unwritten;
18
+ constructor(write: (checkpoint: T) => Promise<void>, mode: DurabilityMode, maxPending?: number);
19
+ /**
20
+ * Hands over a checkpoint. A `durable` one is persisted, with everything before it, before this
21
+ * resolves, whatever the mode: a pause, a failure, the end of a run, or a recovery decision.
22
+ */
23
+ persist(checkpoint: T, durable?: boolean): Promise<void>;
24
+ /** Waits for every write handed over so far, and writes what `exit` mode was holding. */
25
+ flush(): Promise<void>;
26
+ private throwFailure;
27
+ }