nexus-ai-pro 1.17.0 → 1.18.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 (54) hide show
  1. package/API_STABILITY.md +16 -0
  2. package/CHANGELOG.md +49 -1
  3. package/README.md +2 -0
  4. package/dist/operations/runner.js +11 -1
  5. package/dist/server/assistant.d.ts +52 -0
  6. package/dist/server/assistant.js +45 -0
  7. package/dist/server/cron.d.ts +69 -0
  8. package/dist/server/cron.js +172 -0
  9. package/dist/server/errors.d.ts +56 -0
  10. package/dist/server/errors.js +56 -0
  11. package/dist/server/events.d.ts +91 -0
  12. package/dist/server/events.js +103 -0
  13. package/dist/server/http.d.ts +34 -0
  14. package/dist/server/http.js +73 -0
  15. package/dist/server/index.d.ts +18 -0
  16. package/dist/server/index.js +8 -0
  17. package/dist/server/node.d.ts +43 -0
  18. package/dist/server/node.js +86 -0
  19. package/dist/server/remote.d.ts +73 -0
  20. package/dist/server/remote.js +163 -0
  21. package/dist/server/runs.d.ts +116 -0
  22. package/dist/server/runs.js +310 -0
  23. package/dist/server/server.d.ts +54 -0
  24. package/dist/server/server.js +248 -0
  25. package/dist/server/state.d.ts +44 -0
  26. package/dist/server/state.js +40 -0
  27. package/dist/types/server.d.ts +185 -0
  28. package/dist/types/server.js +1 -0
  29. package/dist-cjs/operations/runner.js +11 -1
  30. package/dist-cjs/server/assistant.d.ts +52 -0
  31. package/dist-cjs/server/assistant.js +49 -0
  32. package/dist-cjs/server/cron.d.ts +69 -0
  33. package/dist-cjs/server/cron.js +177 -0
  34. package/dist-cjs/server/errors.d.ts +56 -0
  35. package/dist-cjs/server/errors.js +66 -0
  36. package/dist-cjs/server/events.d.ts +91 -0
  37. package/dist-cjs/server/events.js +108 -0
  38. package/dist-cjs/server/http.d.ts +34 -0
  39. package/dist-cjs/server/http.js +81 -0
  40. package/dist-cjs/server/index.d.ts +18 -0
  41. package/dist-cjs/server/index.js +29 -0
  42. package/dist-cjs/server/node.d.ts +43 -0
  43. package/dist-cjs/server/node.js +89 -0
  44. package/dist-cjs/server/remote.d.ts +73 -0
  45. package/dist-cjs/server/remote.js +166 -0
  46. package/dist-cjs/server/runs.d.ts +116 -0
  47. package/dist-cjs/server/runs.js +314 -0
  48. package/dist-cjs/server/server.d.ts +54 -0
  49. package/dist-cjs/server/server.js +251 -0
  50. package/dist-cjs/server/state.d.ts +44 -0
  51. package/dist-cjs/server/state.js +45 -0
  52. package/dist-cjs/types/server.d.ts +185 -0
  53. package/dist-cjs/types/server.js +2 -0
  54. package/package.json +27 -1
package/API_STABILITY.md CHANGED
@@ -426,6 +426,22 @@ patch release. None is exported from the root.
426
426
  - The wording of gate reasons, of the note a forced promotion records, and of `formatPromptDiff()`
427
427
  output is for people and may improve in any release.
428
428
 
429
+ ## Agent server stage (1.18.0)
430
+
431
+ The `nexus-ai-pro/server` subpaths are experimental in production readiness, as the image and prompt
432
+ families are: their exported types, functions, and classes follow the 1.x semantic versioning rules,
433
+ and the experimental label does not permit an incompatible minor or patch release. Neither is exported
434
+ from the root.
435
+
436
+ - The routes listed in the server guide, their status codes, and the shape of `RunRecord`,
437
+ `ThreadRecord`, and `RunEvent` follow the 1.x rules. A minor release may add routes and fields.
438
+ - Event ids are per run and strictly increasing, which is what `Last-Event-ID` depends on. A client
439
+ that reconnects with an id never receives it again.
440
+ - The server is not a security boundary on its own: authentication, authorization beyond tenancy and
441
+ scopes, and rate limiting are the application's, through the hooks.
442
+ - Recovery re-runs a whole run rather than the step it died on, so an assistant with side effects
443
+ should be idempotent or be left at a single attempt.
444
+
429
445
  ## Deprecation process
430
446
 
431
447
  Deprecated APIs are marked with `@deprecated` in declarations and described in the changelog. Removals
package/CHANGELOG.md CHANGED
@@ -4,6 +4,53 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.18.0] - 2026-09-25
8
+
9
+ A server you can host. Assistants get an HTTP surface — threads, background runs, resumable event
10
+ streams, and cron jobs — on top of the durability that already existed: a run is an operation, so it
11
+ outlives the request that started it and the worker that was running it.
12
+
13
+ ### Added
14
+
15
+ - **The agent server** (`nexus-ai-pro/server`, experimental). `createAgentServer()` serves assistants,
16
+ threads, runs, and cron jobs as one `Request`-to-`Response` handler. `toNodeListener()` runs it on
17
+ Node's `http`, and as Express or Fastify middleware.
18
+ - **Assistants are structural.** `graphAssistant()` serves a compiled graph, mapping threads to graph
19
+ threads; `functionAssistant()` serves a plain function. The entry point imports neither runtime.
20
+ - **Runs are durable operations**, so leases, heartbeats, retries, idempotency, dead-lettering, and
21
+ webhooks come from the operations family. A run abandoned by a crashed worker is re-claimed by
22
+ another replica through `start()` or `runs.recover()`.
23
+ - **Resumable streaming.** `GET /runs/:id/events` is a server-sent event stream whose events carry
24
+ their log id, so a client reconnects with `Last-Event-ID` and gets exactly what it missed.
25
+ `MemoryRunEventLog` covers one replica; `RedisRunEventLog` lets a client reconnect to any replica.
26
+ - **A busy thread is configurable**: `reject`, `enqueue`, `interrupt`, or `rollback`, per server or
27
+ per run.
28
+ - **Cron jobs.** `CronScheduler` fires `{ everyMs }` or five-field cron schedules, parsed by
29
+ `parseCron()`. Every replica ticks and every firing carries the idempotency key `<job>:<slot>`, so a
30
+ job runs once however many replicas are up, with no lock and no leader election.
31
+ - **Authentication and tenancy hooks.** `authenticate` returns a `Principal`; threads, runs, and cron
32
+ jobs record their tenant, and another tenant's resource is a `404`.
33
+ - **`createRemoteGraph()`** (`nexus-ai-pro/server/remote`), the client: `invoke()`, `stream()` with
34
+ reconnection, `resume()`, and `asNode()`, which makes a deployed assistant a node in a local graph.
35
+ - **Deployment templates**: `deploy/Dockerfile`, `deploy/compose.yaml` for two replicas behind Redis,
36
+ and an nginx configuration that does not buffer event streams. `examples/agent-server.ts` is a
37
+ runnable server.
38
+
39
+ ### Changed
40
+
41
+ - **Every feature's exports are documented in that feature's guide.** A guide can now claim exports by
42
+ source directory, so features reachable only from the root — guardrails, grounding, workflows,
43
+ metrics, conformance, routing — no longer pile into the client guide, which drops from 249 exports
44
+ to 141. Retrieval, citations, and verification get a guide of their own, and the agents, batch,
45
+ caching, and client guides now explain every export they cover.
46
+
47
+ ### Fixed
48
+
49
+ - An operation whose record was settled by another worker — after its lease lapsed and the work was
50
+ taken over — crashed the original worker with an unhandled `OperationTransitionError` from its
51
+ heartbeat. The heartbeat now stops and fails its local run instead, and a heartbeat that throws for
52
+ any other reason no longer becomes an unhandled rejection.
53
+
7
54
  ## [1.17.0] - 2026-09-23
8
55
 
9
56
  Prompt versioning, and documentation you can navigate. Prompts are defined with typed variables,
@@ -894,7 +941,8 @@ shared policy vocabulary is in place for a future release that revisits this.
894
941
  - URL fetching rejects unsafe private-network targets and limits response reads.
895
942
  - CLI and audit findings no longer disclose detected secret values.
896
943
 
897
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.17.0...HEAD
944
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.18.0...HEAD
945
+ [1.18.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.17.0...v1.18.0
898
946
  [1.17.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.16.0...v1.17.0
899
947
  [1.16.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.15.0...v1.16.0
900
948
  [1.15.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.14.0...v1.15.0
package/README.md CHANGED
@@ -118,12 +118,14 @@ from the doc comments. The guides live in the repository, so these links go to G
118
118
  | [Resilience and observability](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/resilience.md) | Circuit breaking, distributed rate limits, metrics, logs, and audit |
119
119
  | [Batch tiers](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/batch.md) | The providers' discounted batch APIs, resumable from any process |
120
120
  | [Caching](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/caching.md) | Exact and semantic response caches, with memory, Redis, and SQLite adapters |
121
+ | [Grounding](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/grounding.md) | Retrieval, citations, verification, self-consistency, and knowledge graphs |
121
122
  | [Embeddings and retrieval](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/embeddings.md) | Embeddings as a routed operation family, and RAG helpers |
122
123
  | [Images (experimental)](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/images.md) | Generation and masked edits over three backends, input safety, moderation, asset stores |
123
124
  | [Voice](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/voice.md) | Transcription, speech, voice turns, and voice sessions |
124
125
  | [Realtime voice](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/realtime.md) | Browser and server realtime sessions with barge-in, tools, and exports |
125
126
  | [Telephony](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/telephony.md) | Calls, webhooks, phone numbers, and phone agents on realtime sessions |
126
127
  | [Testing](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/testing.md) | Recording and replaying provider traffic, and conformance suites |
128
+ | [Agent server](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/server.md) | Self-hosted HTTP server for assistants: threads, durable runs, resumable streams, cron |
127
129
  | [Workflows](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/workflows.md) | Ready-made chains and domain workflows |
128
130
  | [Command line](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/cli.md) | The `nexus` command |
129
131
  | [Packaging and install weight](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md) | Every entry point and what it costs |
@@ -233,6 +233,10 @@ export class OperationRunner {
233
233
  handle.settleFailure(new OperationLeaseLostError(latest.id, current.lease.owner));
234
234
  return;
235
235
  }
236
+ if (isTerminalOperationStatus(current.status)) {
237
+ handle.settleFailure(new OperationLeaseLostError(latest.id, current.lease?.owner ?? 'another worker'));
238
+ return;
239
+ }
236
240
  const renewed = this.advance(current, 'running');
237
241
  renewed.lease = {
238
242
  owner: this.owner,
@@ -243,7 +247,13 @@ export class OperationRunner {
243
247
  if (await this.store.update(renewed, current.sequence))
244
248
  latest = renewed;
245
249
  };
246
- const timer = setInterval(() => void beat(), this.heartbeatMs);
250
+ const timer = setInterval(() => {
251
+ void beat().catch((error) => {
252
+ stopped = true;
253
+ clearInterval(timer);
254
+ handle.settleFailure(error);
255
+ });
256
+ }, this.heartbeatMs);
247
257
  timer.unref?.();
248
258
  return {
249
259
  stop: () => {
@@ -0,0 +1,52 @@
1
+ import type { AssistantRunContext, ServerAssistant } from '../types/server.js';
2
+ /**
3
+ * The part of a compiled graph the adapter uses.
4
+ *
5
+ * Structural, so the server entry point never imports the graph runtime: an application that serves
6
+ * plain functions does not load it, and one that serves graphs already has it.
7
+ */
8
+ export interface GraphLike {
9
+ /** Runs an input, yielding one event per superstep. */
10
+ stream(input: unknown, options: {
11
+ threadId?: string;
12
+ signal?: AbortSignal;
13
+ metadata?: Record<string, unknown>;
14
+ }): AsyncIterable<unknown>;
15
+ /** Answers an interrupt and continues the thread. */
16
+ resume(threadId: string, value: unknown, options: {
17
+ signal?: AbortSignal;
18
+ metadata?: Record<string, unknown>;
19
+ }): AsyncIterable<unknown>;
20
+ /** The thread's checkpoint, at its latest step or at one given. */
21
+ state(threadId: string, step?: number): Promise<{
22
+ step?: number;
23
+ state?: unknown;
24
+ } | undefined>;
25
+ /** Writes values into a thread's state, which is how a rollback is applied. */
26
+ updateState?(threadId: string, values: Record<string, unknown>, options?: {
27
+ asNode?: string;
28
+ }): Promise<unknown>;
29
+ }
30
+ /** Options for `graphAssistant()`. */
31
+ export interface GraphAssistantOptions {
32
+ /** What the assistant is, reported by the assistants endpoint. */
33
+ description?: string;
34
+ /** Metadata recorded on every checkpoint the server's runs create. */
35
+ metadata?: Record<string, unknown>;
36
+ }
37
+ /**
38
+ * Serves a compiled graph as an assistant.
39
+ *
40
+ * Threads map to graph threads, so state, interrupts, history, and checkpoints are the graph's own;
41
+ * the server adds only the record of which run owns a thread. A graph compiled without a
42
+ * checkpointer still works for stateless runs, but cannot resume or roll back.
43
+ */
44
+ export declare function graphAssistant(graph: GraphLike, options?: GraphAssistantOptions): ServerAssistant;
45
+ /**
46
+ * Serves a plain function as an assistant, for work that is not a graph.
47
+ *
48
+ * The function may return a value or yield events; the last value it yields is the run's output.
49
+ */
50
+ export declare function functionAssistant(run: (input: unknown, context: AssistantRunContext) => AsyncIterable<unknown> | Promise<unknown> | unknown, options?: {
51
+ description?: string;
52
+ }): ServerAssistant;
@@ -0,0 +1,45 @@
1
+ export function graphAssistant(graph, options = {}) {
2
+ return {
3
+ description: options.description,
4
+ stream(input, context) {
5
+ return graph.stream(input ?? {}, {
6
+ threadId: context.threadId,
7
+ signal: context.signal,
8
+ metadata: { ...options.metadata, runId: context.runId },
9
+ });
10
+ },
11
+ resume(threadId, value, context) {
12
+ return graph.resume(threadId, value, {
13
+ signal: context.signal,
14
+ metadata: { ...options.metadata, runId: context.runId },
15
+ });
16
+ },
17
+ async state(threadId) {
18
+ return (await graph.state(threadId))?.state;
19
+ },
20
+ async step(threadId) {
21
+ return (await graph.state(threadId))?.step;
22
+ },
23
+ async restore(threadId, step) {
24
+ if (!graph.updateState)
25
+ return;
26
+ const checkpoint = await graph.state(threadId, step);
27
+ if (!checkpoint?.state)
28
+ return;
29
+ await graph.updateState(threadId, checkpoint.state);
30
+ },
31
+ };
32
+ }
33
+ export function functionAssistant(run, options = {}) {
34
+ return {
35
+ description: options.description,
36
+ async *stream(input, context) {
37
+ const result = run(input, context);
38
+ if (result !== null && typeof result === 'object' && Symbol.asyncIterator in result) {
39
+ yield* result;
40
+ return;
41
+ }
42
+ yield { type: 'done', status: 'succeeded', state: await result };
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,69 @@
1
+ import type { CronRecord, RunRecord, ServerStateStore } from '../types/server.js';
2
+ /** A cron job that is due, with the slot it is due for. */
3
+ export interface DueCronJob extends CronRecord {
4
+ /** The firing this is: the epoch millisecond of the slot, which makes the run idempotent. */
5
+ slot: number;
6
+ }
7
+ /** Options for the cron scheduler. */
8
+ export interface CronSchedulerOptions {
9
+ /** Starts a run for a due job. */
10
+ start?: (job: DueCronJob) => Promise<RunRecord>;
11
+ /** Where jobs are recorded. Defaults to memory; share it to schedule across replicas. */
12
+ state?: ServerStateStore;
13
+ /** How often due jobs are looked for, in milliseconds. Defaults to 30 seconds. */
14
+ tickMs?: number;
15
+ /** Receives errors from a firing, which never stop the scheduler. */
16
+ onError?: (error: unknown, job: CronRecord) => void;
17
+ /** Replaces the system clock, for tests. */
18
+ now?: () => Date;
19
+ }
20
+ /**
21
+ * Fires scheduled runs.
22
+ *
23
+ * Every replica ticks, and every firing is submitted with the idempotency key `<job>:<slot>`, so the
24
+ * operation store decides which replica's submission wins and the job runs once however many
25
+ * replicas are up. That is the whole coordination mechanism: no lock, no leader election, and a
26
+ * replica that misses a tick simply does not win that slot.
27
+ */
28
+ export declare class CronScheduler {
29
+ private readonly options;
30
+ private readonly state;
31
+ private readonly now;
32
+ private timer;
33
+ private lastTick;
34
+ constructor(options?: CronSchedulerOptions);
35
+ /** Adds a job, validating its schedule. */
36
+ add(job: Omit<CronRecord, 'id' | 'createdAt'> & {
37
+ id?: string;
38
+ createdAt?: string;
39
+ }): Promise<CronRecord>;
40
+ /** Reads a job. */
41
+ get(id: string): Promise<CronRecord | undefined>;
42
+ /** Every job. */
43
+ list(): Promise<CronRecord[]>;
44
+ /** Removes a job. */
45
+ remove(id: string): Promise<void>;
46
+ /** Starts ticking. Ticks are skipped while one is still running. */
47
+ start(): void;
48
+ /** Stops ticking. */
49
+ stop(): void;
50
+ /**
51
+ * Fires every job due since the last tick. Call it directly to drive the scheduler from a test or
52
+ * from an external scheduler such as Kubernetes.
53
+ */
54
+ tick(at?: Date): Promise<DueCronJob[]>;
55
+ }
56
+ interface CronFields {
57
+ minute: number[];
58
+ hour: number[];
59
+ day: number[];
60
+ month: number[];
61
+ weekday: number[];
62
+ }
63
+ /**
64
+ * Parses the five-field cron syntax: `*`, numbers, `a-b` ranges, `a,b` lists, and `*​/n` steps.
65
+ *
66
+ * Times are UTC, so a schedule means the same thing on every replica whatever its timezone is set to.
67
+ */
68
+ export declare function parseCron(expression: string): CronFields;
69
+ export {};
@@ -0,0 +1,172 @@
1
+ import { BadRequestError } from './errors.js';
2
+ import { MemoryServerStore } from './state.js';
3
+ const CRONS = ['nexus', 'server', 'crons'];
4
+ export class CronScheduler {
5
+ options;
6
+ state;
7
+ now;
8
+ timer;
9
+ lastTick = 0;
10
+ constructor(options = {}) {
11
+ this.options = options;
12
+ this.state = options.state ?? new MemoryServerStore();
13
+ this.now = options.now ?? (() => new Date());
14
+ }
15
+ async add(job) {
16
+ if ('cron' in job.schedule)
17
+ parseCron(job.schedule.cron);
18
+ else if (!(job.schedule.everyMs > 0))
19
+ throw new BadRequestError('"everyMs" must be a positive number');
20
+ const record = {
21
+ ...job,
22
+ id: job.id?.trim() || `cron-${Math.random().toString(36).slice(2, 10)}`,
23
+ createdAt: job.createdAt ?? this.now().toISOString(),
24
+ };
25
+ await this.state.put(CRONS, record.id, record);
26
+ return record;
27
+ }
28
+ async get(id) {
29
+ return this.state.get(CRONS, id);
30
+ }
31
+ async list() {
32
+ return this.state.list(CRONS, { limit: 500 });
33
+ }
34
+ async remove(id) {
35
+ await this.state.delete(CRONS, id);
36
+ }
37
+ start() {
38
+ if (this.timer)
39
+ return;
40
+ this.lastTick = this.now().getTime();
41
+ const tickMs = this.options.tickMs ?? 30_000;
42
+ let running = false;
43
+ this.timer = setInterval(() => {
44
+ if (running)
45
+ return;
46
+ running = true;
47
+ void this.tick().finally(() => {
48
+ running = false;
49
+ });
50
+ }, tickMs);
51
+ this.timer.unref?.();
52
+ }
53
+ stop() {
54
+ if (this.timer)
55
+ clearInterval(this.timer);
56
+ this.timer = undefined;
57
+ }
58
+ async tick(at = this.now()) {
59
+ const now = at.getTime();
60
+ const since = this.lastTick || now - (this.options.tickMs ?? 30_000);
61
+ this.lastTick = now;
62
+ const fired = [];
63
+ for (const job of await this.list()) {
64
+ if (job.paused)
65
+ continue;
66
+ const slot = dueSlot(job, since, now);
67
+ if (slot === undefined)
68
+ continue;
69
+ const due = { ...job, slot };
70
+ try {
71
+ const run = await this.options.start?.(due);
72
+ await this.state.put(CRONS, job.id, {
73
+ ...job,
74
+ lastRunAt: new Date(slot).toISOString(),
75
+ lastRunId: run?.id ?? job.lastRunId,
76
+ });
77
+ fired.push(due);
78
+ }
79
+ catch (error) {
80
+ this.options.onError?.(error, job);
81
+ }
82
+ }
83
+ return fired;
84
+ }
85
+ }
86
+ function dueSlot(job, since, now) {
87
+ if ('everyMs' in job.schedule) {
88
+ const every = job.schedule.everyMs;
89
+ const last = job.lastRunAt ? Date.parse(job.lastRunAt) : undefined;
90
+ const previous = last ?? since;
91
+ if (now - previous < every)
92
+ return undefined;
93
+ return Math.floor(now / every) * every;
94
+ }
95
+ const fields = parseCron(job.schedule.cron);
96
+ const lastRun = job.lastRunAt ? Date.parse(job.lastRunAt) : 0;
97
+ for (let minute = Math.floor(now / 60_000) * 60_000; minute > since - 60_000 && minute > lastRun; minute -= 60_000) {
98
+ if (matches(fields, new Date(minute)))
99
+ return minute;
100
+ }
101
+ return undefined;
102
+ }
103
+ export function parseCron(expression) {
104
+ const parts = expression.trim().split(/\s+/);
105
+ if (parts.length !== 5) {
106
+ throw new BadRequestError(`"${expression}" is not a 5-field cron expression`, 'BAD_CRON');
107
+ }
108
+ const ranges = [
109
+ [0, 59],
110
+ [0, 23],
111
+ [1, 31],
112
+ [1, 12],
113
+ [0, 6],
114
+ ];
115
+ const fields = parts.map((part, index) => {
116
+ const [min, max] = ranges[index];
117
+ return field(part, min, max, expression);
118
+ });
119
+ return {
120
+ minute: fields[0],
121
+ hour: fields[1],
122
+ day: fields[2],
123
+ month: fields[3],
124
+ weekday: fields[4].map((value) => value % 7),
125
+ };
126
+ }
127
+ function field(part, min, max, expression) {
128
+ const values = new Set();
129
+ for (const item of part.split(',')) {
130
+ const [range, stepText] = item.split('/');
131
+ const step = stepText === undefined ? 1 : Number(stepText);
132
+ if (!Number.isInteger(step) || step < 1)
133
+ throw new BadRequestError(`"${expression}" has an invalid step`, 'BAD_CRON');
134
+ let start = min;
135
+ let end = max;
136
+ if (range !== '*' && range !== undefined && range !== '') {
137
+ const bounds = range.split('-').map(Number);
138
+ start = bounds[0];
139
+ end = bounds.length > 1 ? bounds[1] : bounds[0];
140
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < min || end > max || end < start) {
141
+ throw new BadRequestError(`"${expression}" has a field outside ${min}-${max}`, 'BAD_CRON');
142
+ }
143
+ if (stepText !== undefined && bounds.length === 1)
144
+ end = max;
145
+ }
146
+ for (let value = start; value <= end; value += step)
147
+ values.add(value);
148
+ }
149
+ return [...values].sort((a, b) => a - b);
150
+ }
151
+ function matches(fields, at) {
152
+ if (!fields.minute.includes(at.getUTCMinutes()))
153
+ return false;
154
+ if (!fields.hour.includes(at.getUTCHours()))
155
+ return false;
156
+ if (!fields.month.includes(at.getUTCMonth() + 1))
157
+ return false;
158
+ const byDay = isRestricted(fields.day, 1, 31);
159
+ const byWeekday = isRestricted(fields.weekday, 0, 6);
160
+ const dayMatches = fields.day.includes(at.getUTCDate());
161
+ const weekdayMatches = fields.weekday.includes(at.getUTCDay());
162
+ if (byDay && byWeekday)
163
+ return dayMatches || weekdayMatches;
164
+ if (byDay)
165
+ return dayMatches;
166
+ if (byWeekday)
167
+ return weekdayMatches;
168
+ return true;
169
+ }
170
+ function isRestricted(values, min, max) {
171
+ return values.length !== max - min + 1;
172
+ }
@@ -0,0 +1,56 @@
1
+ /** Base class for server errors. Each carries the HTTP status the server answers with. */
2
+ export declare class ServerError extends Error {
3
+ /** Stable code, such as `THREAD_BUSY`, sent in the error body. */
4
+ readonly code: string;
5
+ /** HTTP status the server answers with. */
6
+ readonly status: number;
7
+ constructor(message: string,
8
+ /** Stable code, such as `THREAD_BUSY`, sent in the error body. */
9
+ code: string,
10
+ /** HTTP status the server answers with. */
11
+ status: number);
12
+ }
13
+ /** Raised when a request names something that does not exist, or belongs to another tenant. */
14
+ export declare class NotFoundError extends ServerError {
15
+ constructor(what: string, id: string);
16
+ }
17
+ /** Raised when a request is malformed: bad JSON, a missing field, or an unknown assistant. */
18
+ export declare class BadRequestError extends ServerError {
19
+ constructor(message: string, code?: string);
20
+ }
21
+ /** Raised when a request carries no usable credentials. */
22
+ export declare class UnauthorizedError extends ServerError {
23
+ constructor(message?: string);
24
+ }
25
+ /** Raised when a principal is known but not allowed to do this. */
26
+ export declare class ForbiddenError extends ServerError {
27
+ /** The scope the route required. */
28
+ readonly scope: string;
29
+ constructor(
30
+ /** The scope the route required. */
31
+ scope: string);
32
+ }
33
+ /** Raised when a thread is already running something and the busy policy is `reject`. */
34
+ export declare class ThreadBusyError extends ServerError {
35
+ /** The thread. */
36
+ readonly threadId: string;
37
+ /** The run already in flight. */
38
+ readonly runId: string;
39
+ constructor(
40
+ /** The thread. */
41
+ threadId: string,
42
+ /** The run already in flight. */
43
+ runId: string);
44
+ }
45
+ /** Raised when an assistant cannot do what a request needs, such as resuming or rolling back. */
46
+ export declare class AssistantCapabilityError extends ServerError {
47
+ /** The assistant. */
48
+ readonly assistant: string;
49
+ /** What it cannot do. */
50
+ readonly capability: string;
51
+ constructor(
52
+ /** The assistant. */
53
+ assistant: string,
54
+ /** What it cannot do. */
55
+ capability: string);
56
+ }
@@ -0,0 +1,56 @@
1
+ export class ServerError extends Error {
2
+ code;
3
+ status;
4
+ constructor(message, code, status) {
5
+ super(message);
6
+ this.code = code;
7
+ this.status = status;
8
+ this.name = 'ServerError';
9
+ }
10
+ }
11
+ export class NotFoundError extends ServerError {
12
+ constructor(what, id) {
13
+ super(`${what} "${id}" was not found`, 'NOT_FOUND', 404);
14
+ this.name = 'NotFoundError';
15
+ }
16
+ }
17
+ export class BadRequestError extends ServerError {
18
+ constructor(message, code = 'BAD_REQUEST') {
19
+ super(message, code, 400);
20
+ this.name = 'BadRequestError';
21
+ }
22
+ }
23
+ export class UnauthorizedError extends ServerError {
24
+ constructor(message = 'Authentication is required') {
25
+ super(message, 'UNAUTHORIZED', 401);
26
+ this.name = 'UnauthorizedError';
27
+ }
28
+ }
29
+ export class ForbiddenError extends ServerError {
30
+ scope;
31
+ constructor(scope) {
32
+ super(`This request requires the "${scope}" scope`, 'FORBIDDEN', 403);
33
+ this.scope = scope;
34
+ this.name = 'ForbiddenError';
35
+ }
36
+ }
37
+ export class ThreadBusyError extends ServerError {
38
+ threadId;
39
+ runId;
40
+ constructor(threadId, runId) {
41
+ super(`Thread "${threadId}" is already running "${runId}"`, 'THREAD_BUSY', 409);
42
+ this.threadId = threadId;
43
+ this.runId = runId;
44
+ this.name = 'ThreadBusyError';
45
+ }
46
+ }
47
+ export class AssistantCapabilityError extends ServerError {
48
+ assistant;
49
+ capability;
50
+ constructor(assistant, capability) {
51
+ super(`Assistant "${assistant}" does not support ${capability}`, 'ASSISTANT_CAPABILITY', 409);
52
+ this.assistant = assistant;
53
+ this.capability = capability;
54
+ this.name = 'AssistantCapabilityError';
55
+ }
56
+ }
@@ -0,0 +1,91 @@
1
+ import type { RunEvent, RunEventLog } from '../types/server.js';
2
+ /** Options for the in-memory run event log. */
3
+ export interface MemoryRunEventLogOptions {
4
+ /** Events kept per run before the oldest are dropped. Defaults to 1,000. */
5
+ maxEventsPerRun?: number;
6
+ /** Runs kept before the least recently written is dropped. Defaults to 1,000. */
7
+ maxRuns?: number;
8
+ }
9
+ /**
10
+ * Run events in process memory, with waiters woken as events arrive.
11
+ *
12
+ * The default, and all a single-process server needs: a client that reconnects reads what it missed
13
+ * from here. Across replicas the log has to be shared, which is what `RedisRunEventLog` is for.
14
+ */
15
+ export declare class MemoryRunEventLog implements RunEventLog {
16
+ private readonly runs;
17
+ private readonly waiters;
18
+ private readonly maxEvents;
19
+ private readonly maxRuns;
20
+ constructor(options?: MemoryRunEventLogOptions);
21
+ /** Appends an event, numbering it, and wakes anything waiting on this run. */
22
+ append(runId: string, event: Omit<RunEvent, 'id' | 'runId'>): RunEvent;
23
+ /** Events of a run after an id, oldest first. */
24
+ read(runId: string, options?: {
25
+ after?: number;
26
+ limit?: number;
27
+ }): RunEvent[];
28
+ /** Resolves as soon as an event after `after` exists, or when the wait is aborted or times out. */
29
+ wait(runId: string, after: number, options?: {
30
+ signal?: AbortSignal;
31
+ timeoutMs?: number;
32
+ }): Promise<void>;
33
+ /** Forgets a run's events. */
34
+ clear(runId: string): void;
35
+ }
36
+ /**
37
+ * The Redis commands the event log needs, in `ioredis` argument order.
38
+ *
39
+ * Structural, as with the other Redis adapters, so no Redis client becomes a dependency.
40
+ */
41
+ export interface RedisEventLogLikeClient {
42
+ /** Appends to a list and returns its new length, which numbers the event. */
43
+ rpush(key: string, value: string): Promise<number | unknown> | number | unknown;
44
+ /** Reads a range of a list. */
45
+ lrange(key: string, start: number, stop: number): Promise<string[]> | string[];
46
+ /** Trims a list to a range. */
47
+ ltrim(key: string, start: number, stop: number): Promise<unknown> | unknown;
48
+ /** Sets a key's lifetime in seconds. */
49
+ expire(key: string, seconds: number): Promise<unknown> | unknown;
50
+ /** Deletes a key. */
51
+ del(key: string): Promise<unknown> | unknown;
52
+ }
53
+ /** Options for the Redis run event log. */
54
+ export interface RedisRunEventLogOptions {
55
+ /** Key prefix. Defaults to `nexus-ai-pro:runs:`. */
56
+ prefix?: string;
57
+ /** Events kept per run. Defaults to 1,000. */
58
+ maxEventsPerRun?: number;
59
+ /** How long a run's events live after the last append, in seconds. Defaults to one day. */
60
+ ttlSeconds?: number;
61
+ /** How often a waiting reader looks again, in milliseconds. Defaults to 250. */
62
+ pollIntervalMs?: number;
63
+ }
64
+ /**
65
+ * Run events in Redis, so a client can reconnect to any replica and resume.
66
+ *
67
+ * One list per run, whose length numbers each event, which is what makes ids stable without a second
68
+ * counter. Waiting readers poll rather than subscribe, so the log needs no second connection and no
69
+ * pub/sub delivery guarantees; the poll interval is the only latency a reconnecting client sees.
70
+ */
71
+ export declare class RedisRunEventLog implements RunEventLog {
72
+ private readonly client;
73
+ private readonly options;
74
+ private readonly prefix;
75
+ constructor(client: RedisEventLogLikeClient, options?: RedisRunEventLogOptions);
76
+ /** Appends an event, numbered by the list's new length. */
77
+ append(runId: string, event: Omit<RunEvent, 'id' | 'runId'>): Promise<RunEvent>;
78
+ /** Events of a run after an id, oldest first. */
79
+ read(runId: string, options?: {
80
+ after?: number;
81
+ limit?: number;
82
+ }): Promise<RunEvent[]>;
83
+ /** Polls until an event after `after` exists, or the wait is aborted or times out. */
84
+ wait(runId: string, after: number, options?: {
85
+ signal?: AbortSignal;
86
+ timeoutMs?: number;
87
+ }): Promise<void>;
88
+ /** Forgets a run's events. */
89
+ clear(runId: string): Promise<void>;
90
+ private key;
91
+ }