nexus-ai-pro 1.17.0 → 1.19.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 (92) hide show
  1. package/API_STABILITY.md +29 -0
  2. package/CHANGELOG.md +117 -1
  3. package/README.md +3 -0
  4. package/dist/graph/checkpointer.d.ts +5 -0
  5. package/dist/graph/checkpointer.js +9 -0
  6. package/dist/mcp/client.d.ts +6 -1
  7. package/dist/mcp/client.js +7 -0
  8. package/dist/mcp/server.js +4 -4
  9. package/dist/operations/runner.js +11 -1
  10. package/dist/security/index.js +3 -3
  11. package/dist/security/input-guard.js +6 -1
  12. package/dist/security/pii-detector.d.ts +1 -1
  13. package/dist/security/pii-detector.js +9 -3
  14. package/dist/security/prompt-hardening.d.ts +3 -0
  15. package/dist/security/semantic-injection-classifier.d.ts +6 -0
  16. package/dist/security/semantic-injection-classifier.js +3 -1
  17. package/dist/server/assistant.d.ts +52 -0
  18. package/dist/server/assistant.js +45 -0
  19. package/dist/server/cron.d.ts +69 -0
  20. package/dist/server/cron.js +172 -0
  21. package/dist/server/errors.d.ts +56 -0
  22. package/dist/server/errors.js +56 -0
  23. package/dist/server/events.d.ts +91 -0
  24. package/dist/server/events.js +103 -0
  25. package/dist/server/http.d.ts +34 -0
  26. package/dist/server/http.js +73 -0
  27. package/dist/server/index.d.ts +18 -0
  28. package/dist/server/index.js +8 -0
  29. package/dist/server/node.d.ts +43 -0
  30. package/dist/server/node.js +86 -0
  31. package/dist/server/remote.d.ts +73 -0
  32. package/dist/server/remote.js +163 -0
  33. package/dist/server/runs.d.ts +116 -0
  34. package/dist/server/runs.js +310 -0
  35. package/dist/server/server.d.ts +54 -0
  36. package/dist/server/server.js +248 -0
  37. package/dist/server/state.d.ts +44 -0
  38. package/dist/server/state.js +40 -0
  39. package/dist/tracing/instrument.js +2 -0
  40. package/dist/tracing/tracer.d.ts +4 -0
  41. package/dist/tracing/tracer.js +2 -0
  42. package/dist/types/graph.d.ts +2 -0
  43. package/dist/types/security.d.ts +74 -5
  44. package/dist/types/server.d.ts +185 -0
  45. package/dist/types/server.js +1 -0
  46. package/dist/workflow/chains.d.ts +3 -3
  47. package/dist/workflow/chains.js +18 -7
  48. package/dist-cjs/graph/checkpointer.d.ts +5 -0
  49. package/dist-cjs/graph/checkpointer.js +9 -0
  50. package/dist-cjs/mcp/client.d.ts +6 -1
  51. package/dist-cjs/mcp/client.js +7 -0
  52. package/dist-cjs/mcp/server.js +3 -3
  53. package/dist-cjs/operations/runner.js +11 -1
  54. package/dist-cjs/security/index.js +3 -3
  55. package/dist-cjs/security/input-guard.js +6 -1
  56. package/dist-cjs/security/pii-detector.d.ts +1 -1
  57. package/dist-cjs/security/pii-detector.js +9 -3
  58. package/dist-cjs/security/prompt-hardening.d.ts +3 -0
  59. package/dist-cjs/security/semantic-injection-classifier.d.ts +6 -0
  60. package/dist-cjs/security/semantic-injection-classifier.js +3 -1
  61. package/dist-cjs/server/assistant.d.ts +52 -0
  62. package/dist-cjs/server/assistant.js +49 -0
  63. package/dist-cjs/server/cron.d.ts +69 -0
  64. package/dist-cjs/server/cron.js +177 -0
  65. package/dist-cjs/server/errors.d.ts +56 -0
  66. package/dist-cjs/server/errors.js +66 -0
  67. package/dist-cjs/server/events.d.ts +91 -0
  68. package/dist-cjs/server/events.js +108 -0
  69. package/dist-cjs/server/http.d.ts +34 -0
  70. package/dist-cjs/server/http.js +81 -0
  71. package/dist-cjs/server/index.d.ts +18 -0
  72. package/dist-cjs/server/index.js +29 -0
  73. package/dist-cjs/server/node.d.ts +43 -0
  74. package/dist-cjs/server/node.js +89 -0
  75. package/dist-cjs/server/remote.d.ts +73 -0
  76. package/dist-cjs/server/remote.js +166 -0
  77. package/dist-cjs/server/runs.d.ts +116 -0
  78. package/dist-cjs/server/runs.js +314 -0
  79. package/dist-cjs/server/server.d.ts +54 -0
  80. package/dist-cjs/server/server.js +251 -0
  81. package/dist-cjs/server/state.d.ts +44 -0
  82. package/dist-cjs/server/state.js +45 -0
  83. package/dist-cjs/tracing/instrument.js +2 -0
  84. package/dist-cjs/tracing/tracer.d.ts +4 -0
  85. package/dist-cjs/tracing/tracer.js +2 -0
  86. package/dist-cjs/types/graph.d.ts +2 -0
  87. package/dist-cjs/types/security.d.ts +74 -5
  88. package/dist-cjs/types/server.d.ts +185 -0
  89. package/dist-cjs/types/server.js +2 -0
  90. package/dist-cjs/workflow/chains.d.ts +3 -3
  91. package/dist-cjs/workflow/chains.js +18 -7
  92. package/package.json +36 -5
package/API_STABILITY.md CHANGED
@@ -426,6 +426,35 @@ 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
+
445
+ ## Studio stage (1.19.0)
446
+
447
+ `nexus-ai-pro-studio` is a separate package, experimental in production readiness. It is versioned
448
+ with nexus-ai-pro and declares the core versions it works with as a peer dependency.
449
+
450
+ - The `nexus-studio` command, its flags, and the shape of the config module's default export follow
451
+ the 1.x rules. A minor release may add sources and flags.
452
+ - `createStudio()`, `startStudio()`, and the exported types follow the 1.x rules.
453
+ - The JSON routes behind the page are for the page and may change in any release; build on the
454
+ stores themselves rather than on the studio's routes.
455
+ - The access rules are a guarantee: loopback binding by default, a token on every request, the token
456
+ in a header for every change, and loopback hosts only unless others are allowed.
457
+
429
458
  ## Deprecation process
430
459
 
431
460
  Deprecated APIs are marked with `@deprecated` in declarations and described in the changelog. Removals
package/CHANGELOG.md CHANGED
@@ -4,6 +4,120 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.19.0] - 2026-09-28
8
+
9
+ A studio to look at it all. `nexus-ai-pro-studio` is a separate package — so the core install never
10
+ carries a UI — that shows what an application records, from its own stores, on the developer's
11
+ machine, with no hosted service.
12
+
13
+ ### Added
14
+
15
+ - **`nexus-ai-pro-studio`**, experimental, with the `nexus-studio` command. Given a config module
16
+ whose default export lists the application's stores, it serves eight views:
17
+ - **Traces**: filtered run lists, run trees with inputs, outputs, cost, and latency, trace
18
+ comparison, and feedback on a run.
19
+ - **Threads**: each graph's diagram with the next nodes highlighted, state at any step, and fork,
20
+ edit, and resume.
21
+ - **Inbox**: every thread waiting for a human and every review item waiting for a grade.
22
+ - **Experiments**: experiment and dataset lists, and comparisons with per-metric intervals and a
23
+ verdict.
24
+ - **Prompts**: versions, labels, history, diffs, gated promotion, rollback, and a playground.
25
+ - **Costs**: cost per day and per model, the most expensive runs, and budgets.
26
+ - **Health**: provider health, local and shared circuit state, metrics, and cache statistics.
27
+ - **Operations**: the operation queue by status, and asset store totals.
28
+ - **Locked down for a local tool.** It binds `127.0.0.1`, requires a token that moves from the URL
29
+ into an HTTP-only same-site cookie, requires the token in a header for every change, refuses
30
+ non-loopback `Host` headers against DNS rebinding, and serves every page under a strict content
31
+ security policy with every value inserted as text.
32
+ - `startStudio()` and `createStudio()` start it from code or mount it in an existing server.
33
+ - `npm run studio:demo` starts it on a demo application with something in every view.
34
+ - The operation-store checkpointer — the one behind Redis and Postgres — lists its threads through
35
+ `threadIds()`, as the in-memory one already did, and `GraphCheckpointer` declares it as optional.
36
+
37
+ ### Fixed
38
+
39
+ - **Secret detection on input now detects under every action.** It only scanned when the action
40
+ was `mask`. Under the default action, `block`, and under `flag`, a request carrying a private
41
+ key, a cloud or payment key, a token, or a secret assignment went out with no finding. Such a
42
+ request is now blocked by default, at every level but `off`. Set `input.secrets.action` to `flag`
43
+ to record secrets without blocking.
44
+ - The PII action `remove` deletes the matched data. It was accepted but behaved like `flag`, so the
45
+ data stayed in the request.
46
+ - The semantic injection check no longer re-embeds its example attacks on every request.
47
+ - `McpServer` answers an unknown method with the JSON-RPC method-not-found code, and an unknown
48
+ resource with invalid-params, instead of an internal error, so a client can tell an unsupported
49
+ feature from a failure.
50
+ - `McpClient` requests made together before the handshake finished each sent `initialize`; they now
51
+ share one handshake.
52
+ - `summarizeVerifyFormat()`'s verify step sent the summary to the model with no instruction, so the
53
+ model answered the summary instead of restating it, and that reply was what got checked against
54
+ the sources. It now asks for a restatement against the sources. The final format is applied only in
55
+ the last step, and a verify step that was skipped is no longer recorded in `steps`.
56
+ - `traceModelClient()` records the model and provider that answered on the run itself. It recorded
57
+ the model the request asked for — often `auto` — and left `provider` empty, so a `provider` query
58
+ never matched a model run and per-model views showed `auto`. `FinishRunOptions` gains `model` and
59
+ `provider` for the same purpose in your own instrumentation.
60
+
61
+ ### Deprecated
62
+
63
+ - `InjectionDetectionConfig.sensitivity` and `ToolPolicyConfig.requiresApproval` have never been
64
+ read. Both will be removed in 2.0; approval before a tool runs belongs to the agent loop.
65
+
66
+ ### Changed
67
+
68
+ - Nineteen guides under `docs/` — agents, batch, caching, core, graphs, grounding, MCP, memory,
69
+ Postgres, prompts, providers, resilience, security, server, telephony, testing, tracing, voice, and
70
+ workflows — now explain each export they list in their text, not only in the generated reference.
71
+ The security configuration's nested settings have doc comments.
72
+ - The documentation coverage check also covers the studio package's exports.
73
+
74
+ ## [1.18.0] - 2026-09-25
75
+
76
+ A server you can host. Assistants get an HTTP surface — threads, background runs, resumable event
77
+ streams, and cron jobs — on top of the durability that already existed: a run is an operation, so it
78
+ outlives the request that started it and the worker that was running it.
79
+
80
+ ### Added
81
+
82
+ - **The agent server** (`nexus-ai-pro/server`, experimental). `createAgentServer()` serves assistants,
83
+ threads, runs, and cron jobs as one `Request`-to-`Response` handler. `toNodeListener()` runs it on
84
+ Node's `http`, and as Express or Fastify middleware.
85
+ - **Assistants are structural.** `graphAssistant()` serves a compiled graph, mapping threads to graph
86
+ threads; `functionAssistant()` serves a plain function. The entry point imports neither runtime.
87
+ - **Runs are durable operations**, so leases, heartbeats, retries, idempotency, dead-lettering, and
88
+ webhooks come from the operations family. A run abandoned by a crashed worker is re-claimed by
89
+ another replica through `start()` or `runs.recover()`.
90
+ - **Resumable streaming.** `GET /runs/:id/events` is a server-sent event stream whose events carry
91
+ their log id, so a client reconnects with `Last-Event-ID` and gets exactly what it missed.
92
+ `MemoryRunEventLog` covers one replica; `RedisRunEventLog` lets a client reconnect to any replica.
93
+ - **A busy thread is configurable**: `reject`, `enqueue`, `interrupt`, or `rollback`, per server or
94
+ per run.
95
+ - **Cron jobs.** `CronScheduler` fires `{ everyMs }` or five-field cron schedules, parsed by
96
+ `parseCron()`. Every replica ticks and every firing carries the idempotency key `<job>:<slot>`, so a
97
+ job runs once however many replicas are up, with no lock and no leader election.
98
+ - **Authentication and tenancy hooks.** `authenticate` returns a `Principal`; threads, runs, and cron
99
+ jobs record their tenant, and another tenant's resource is a `404`.
100
+ - **`createRemoteGraph()`** (`nexus-ai-pro/server/remote`), the client: `invoke()`, `stream()` with
101
+ reconnection, `resume()`, and `asNode()`, which makes a deployed assistant a node in a local graph.
102
+ - **Deployment templates**: `deploy/Dockerfile`, `deploy/compose.yaml` for two replicas behind Redis,
103
+ and an nginx configuration that does not buffer event streams. `examples/agent-server.ts` is a
104
+ runnable server.
105
+
106
+ ### Changed
107
+
108
+ - **Every feature's exports are documented in that feature's guide.** A guide can now claim exports by
109
+ source directory, so features reachable only from the root — guardrails, grounding, workflows,
110
+ metrics, conformance, routing — no longer pile into the client guide, which drops from 249 exports
111
+ to 141. Retrieval, citations, and verification get a guide of their own, and the agents, batch,
112
+ caching, and client guides now explain every export they cover.
113
+
114
+ ### Fixed
115
+
116
+ - An operation whose record was settled by another worker — after its lease lapsed and the work was
117
+ taken over — crashed the original worker with an unhandled `OperationTransitionError` from its
118
+ heartbeat. The heartbeat now stops and fails its local run instead, and a heartbeat that throws for
119
+ any other reason no longer becomes an unhandled rejection.
120
+
7
121
  ## [1.17.0] - 2026-09-23
8
122
 
9
123
  Prompt versioning, and documentation you can navigate. Prompts are defined with typed variables,
@@ -894,7 +1008,9 @@ shared policy vocabulary is in place for a future release that revisits this.
894
1008
  - URL fetching rejects unsafe private-network targets and limits response reads.
895
1009
  - CLI and audit findings no longer disclose detected secret values.
896
1010
 
897
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.17.0...HEAD
1011
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.19.0...HEAD
1012
+ [1.19.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.18.0...v1.19.0
1013
+ [1.18.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.17.0...v1.18.0
898
1014
  [1.17.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.16.0...v1.17.0
899
1015
  [1.16.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.15.0...v1.16.0
900
1016
  [1.15.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v1.14.0...v1.15.0
package/README.md CHANGED
@@ -118,12 +118,15 @@ 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
+ | [Studio](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/studio.md) | A local UI for traces, threads, approvals, experiments, prompts, costs, and health, as a separate package |
129
+ | [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
130
  | [Workflows](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/workflows.md) | Ready-made chains and domain workflows |
128
131
  | [Command line](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/cli.md) | The `nexus` command |
129
132
  | [Packaging and install weight](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/packaging.md) | Every entry point and what it costs |
@@ -66,6 +66,11 @@ export declare class OperationStoreCheckpointer implements GraphCheckpointer {
66
66
  history(threadId: string, limit?: number): Promise<GraphCheckpoint[]>;
67
67
  /** Deletes a thread's checkpoints and its head. */
68
68
  delete(threadId: string): Promise<void>;
69
+ /**
70
+ * Every thread with a checkpoint, read from the head records. Empty when the store cannot list,
71
+ * since finding heads any other way would mean scanning every key.
72
+ */
73
+ threadIds(): Promise<string[]>;
69
74
  private readHead;
70
75
  /**
71
76
  * Writes one record, creating it when absent and otherwise updating in place.
@@ -86,6 +86,15 @@ export class OperationStoreCheckpointer {
86
86
  await this.store.delete?.(stepId(threadId, step));
87
87
  await this.store.delete?.(threadId);
88
88
  }
89
+ async threadIds() {
90
+ if (!this.store.list)
91
+ return [];
92
+ const records = await this.store.list();
93
+ return records
94
+ .filter((record) => record.result !== undefined && isHead(record.result))
95
+ .map((record) => record.id)
96
+ .sort();
97
+ }
89
98
  async readHead(threadId) {
90
99
  const record = await this.store.read(threadId);
91
100
  return record?.result && isHead(record.result) ? record.result : undefined;
@@ -62,13 +62,18 @@ export declare class McpClient {
62
62
  private readonly pending;
63
63
  private nextId;
64
64
  private initialized;
65
+ private connecting;
65
66
  private serverInfo;
66
67
  constructor(transport: McpTransport, options?: McpClientOptions);
67
- /** Performs the handshake. Called automatically by the first request that needs it. */
68
+ /**
69
+ * Performs the handshake and returns the server's name and version. Called automatically by the
70
+ * first request that needs it; requests made together share one handshake.
71
+ */
68
72
  connect(): Promise<{
69
73
  name?: string;
70
74
  version?: string;
71
75
  }>;
76
+ private handshake;
72
77
  /** Lists the server's tools. */
73
78
  listTools(): Promise<McpToolDescriptor[]>;
74
79
  /**
@@ -5,6 +5,7 @@ export class McpClient {
5
5
  pending = new Map();
6
6
  nextId = 1;
7
7
  initialized = false;
8
+ connecting;
8
9
  serverInfo = {};
9
10
  constructor(transport, options = {}) {
10
11
  this.transport = transport;
@@ -15,6 +16,12 @@ export class McpClient {
15
16
  async connect() {
16
17
  if (this.initialized)
17
18
  return this.serverInfo;
19
+ this.connecting ??= this.handshake().finally(() => {
20
+ this.connecting = undefined;
21
+ });
22
+ return this.connecting;
23
+ }
24
+ async handshake() {
18
25
  await this.transport.start?.();
19
26
  const result = (await this.request('initialize', {
20
27
  protocolVersion: MCP_PROTOCOL_VERSION,
@@ -1,4 +1,4 @@
1
- import { isNotification, isRequest, JSON_RPC_ERRORS, LineDecoder, MCP_PROTOCOL_VERSION, textContent, } from './protocol.js';
1
+ import { isNotification, isRequest, JSON_RPC_ERRORS, LineDecoder, MCP_PROTOCOL_VERSION, McpError, textContent, } from './protocol.js';
2
2
  export class McpServer {
3
3
  options;
4
4
  tools = new Map();
@@ -35,7 +35,7 @@ export class McpServer {
35
35
  jsonrpc: '2.0',
36
36
  id: message.id,
37
37
  error: {
38
- code: JSON_RPC_ERRORS.internal,
38
+ code: error instanceof McpError ? error.code : JSON_RPC_ERRORS.internal,
39
39
  message: error instanceof Error ? error.message : String(error),
40
40
  },
41
41
  });
@@ -89,7 +89,7 @@ export class McpServer {
89
89
  const uri = String(params.uri ?? '');
90
90
  const resource = this.options.resources?.find((item) => item.uri === uri);
91
91
  if (!resource)
92
- throw new Error(`Unknown resource "${uri}"`);
92
+ throw new McpError(`Unknown resource "${uri}"`, JSON_RPC_ERRORS.invalidParams);
93
93
  const value = await resource.read();
94
94
  return {
95
95
  contents: [
@@ -102,7 +102,7 @@ export class McpServer {
102
102
  };
103
103
  }
104
104
  default:
105
- throw new Error(`Unknown method "${request.method}"`);
105
+ throw new McpError(`Unknown method "${request.method}"`, JSON_RPC_ERRORS.methodNotFound);
106
106
  }
107
107
  }
108
108
  }
@@ -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: () => {
@@ -63,9 +63,9 @@ export class SecurityPipeline {
63
63
  findings.push(...piiFindings);
64
64
  guardrailsApplied.push('pii-detection');
65
65
  const action = piiConfig.action || (level === 'strict' || level === 'paranoid' ? 'mask' : 'flag');
66
- if (piiFindings.length > 0 && action === 'mask') {
67
- safeRequest = this.piiDetector.mask(safeRequest, piiConfig);
68
- guardrailsApplied.push('pii-masking');
66
+ if (piiFindings.length > 0 && (action === 'mask' || action === 'remove')) {
67
+ safeRequest = this.piiDetector.mask(safeRequest, { ...piiConfig, action });
68
+ guardrailsApplied.push(action === 'remove' ? 'pii-removal' : 'pii-masking');
69
69
  }
70
70
  }
71
71
  const blockingFindings = this.getBlockingFindings(findings, level, normalized);
@@ -103,8 +103,13 @@ export class InputGuard {
103
103
  });
104
104
  return output;
105
105
  };
106
- if (action !== 'mask')
106
+ if (action !== 'mask') {
107
+ request.messages.forEach((message, index) => {
108
+ for (const text of this.extractText(message))
109
+ maskText(text, `messages.${index}.content`);
110
+ });
107
111
  return request;
112
+ }
108
113
  return {
109
114
  ...request,
110
115
  messages: request.messages.map((message, index) => ({
@@ -4,7 +4,7 @@ import type { PIIConfig, SecurityFinding } from '../types/security.js';
4
4
  export declare class PIIDetector {
5
5
  /** Finds personal data in a request's messages. */
6
6
  detect(request: CompletionRequest, config?: PIIConfig): SecurityFinding[];
7
- /** Returns the request with personal data masked. */
7
+ /** Returns the request with personal data masked, or removed when the action is `remove`. */
8
8
  mask(request: CompletionRequest, config?: PIIConfig): CompletionRequest;
9
9
  private maskText;
10
10
  private extractText;
@@ -35,22 +35,28 @@ export class PIIDetector {
35
35
  mask(request, config = {}) {
36
36
  const enabledTypes = config.detect || ['email', 'phone', 'credit-card', 'ip-address', 'aws-key', 'private-key'];
37
37
  const maskChar = config.maskChar || '█';
38
+ const remove = config.action === 'remove';
38
39
  return {
39
40
  ...request,
40
41
  messages: request.messages.map((message) => ({
41
42
  ...message,
42
43
  content: typeof message.content === 'string'
43
- ? this.maskText(message.content, enabledTypes, maskChar, config.preserveFormat !== false)
44
+ ? this.maskText(message.content, enabledTypes, maskChar, config.preserveFormat !== false, remove)
44
45
  : message.content.map((part) => part.type === 'text'
45
- ? { ...part, text: this.maskText(part.text, enabledTypes, maskChar, config.preserveFormat !== false) }
46
+ ? {
47
+ ...part,
48
+ text: this.maskText(part.text, enabledTypes, maskChar, config.preserveFormat !== false, remove),
49
+ }
46
50
  : part),
47
51
  })),
48
52
  };
49
53
  }
50
- maskText(text, types, maskChar, preserveFormat) {
54
+ maskText(text, types, maskChar, preserveFormat, remove = false) {
51
55
  let masked = text;
52
56
  for (const type of types) {
53
57
  masked = masked.replace(PII_PATTERNS[type], (value) => {
58
+ if (remove)
59
+ return '';
54
60
  if (!preserveFormat)
55
61
  return maskChar.repeat(Math.min(value.length, 12));
56
62
  return value.replace(/[A-Za-z0-9]/g, maskChar);
@@ -1,6 +1,9 @@
1
1
  import type { CompletionRequest } from '../types/messages.js';
2
+ /** Options for `hardenPrompt()`. */
2
3
  export interface PromptHardeningOptions {
4
+ /** What user text is wrapped in. Defaults to `"""`. */
3
5
  delimiter?: string;
6
+ /** The system message placed first, telling the model to treat delimited text as data. */
4
7
  systemInstruction?: string;
5
8
  }
6
9
  /**
@@ -1,10 +1,15 @@
1
1
  import type { CompletionRequest } from '../types/messages.js';
2
2
  import type { SecurityFinding } from '../types/security.js';
3
3
  import { type EmbeddingProvider } from '../hallucination/retrieval.js';
4
+ /** Options for `SemanticInjectionClassifier`. */
4
5
  export interface SemanticInjectionClassifierOptions {
6
+ /** `false` turns `detect()` off. */
5
7
  enabled?: boolean;
8
+ /** Similarity at which a prompt is flagged, from 0 to 1. Defaults to 0.78; above 0.88 is critical. */
6
9
  threshold?: number;
10
+ /** Embeds text for `detect()`. Defaults to hashed term vectors; `detectSync()` always uses those. */
7
11
  embed?: EmbeddingProvider;
12
+ /** Example attacks to compare against, instead of the built-in ones. */
8
13
  examples?: string[];
9
14
  }
10
15
  /**
@@ -16,6 +21,7 @@ export declare class SemanticInjectionClassifier {
16
21
  private examples;
17
22
  private embed;
18
23
  private exampleEmbeddings?;
24
+ private hashedExamples?;
19
25
  constructor(options?: SemanticInjectionClassifierOptions);
20
26
  /** Finds injection attempts, using the configured embedding function. */
21
27
  detect(request: CompletionRequest): Promise<SecurityFinding[]>;
@@ -12,6 +12,7 @@ export class SemanticInjectionClassifier {
12
12
  examples;
13
13
  embed;
14
14
  exampleEmbeddings;
15
+ hashedExamples;
15
16
  constructor(options = {}) {
16
17
  this.options = options;
17
18
  this.examples = options.examples || DEFAULT_ATTACK_EXAMPLES;
@@ -43,7 +44,8 @@ export class SemanticInjectionClassifier {
43
44
  detectSync(request) {
44
45
  const findings = [];
45
46
  const threshold = this.options.threshold ?? 0.78;
46
- const exampleEmbeddings = createHashEmbeddings(this.examples);
47
+ this.hashedExamples ??= createHashEmbeddings(this.examples);
48
+ const exampleEmbeddings = this.hashedExamples;
47
49
  for (const [index, message] of request.messages.entries()) {
48
50
  for (const text of extractText(message)) {
49
51
  const [embedding] = createHashEmbeddings([text]);
@@ -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 {};