nexus-ai-pro 2.18.0 → 2.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 (59) hide show
  1. package/API_STABILITY.md +15 -0
  2. package/CHANGELOG.md +45 -1
  3. package/README.md +7 -2
  4. package/dist/capabilities/negotiate.js +5 -0
  5. package/dist/decisions/completion.d.ts +48 -0
  6. package/dist/decisions/completion.js +195 -0
  7. package/dist/decisions/core.d.ts +525 -0
  8. package/dist/decisions/core.js +493 -0
  9. package/dist/decisions/index.d.ts +9 -0
  10. package/dist/decisions/index.js +3 -0
  11. package/dist/decisions/integrations.d.ts +142 -0
  12. package/dist/decisions/integrations.js +170 -0
  13. package/dist/decisions/mock.d.ts +32 -0
  14. package/dist/decisions/mock.js +47 -0
  15. package/dist/decisions/typesafe.d.ts +45 -0
  16. package/dist/decisions/typesafe.js +144 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/providers/google.js +28 -0
  19. package/dist/providers/ollama.js +23 -0
  20. package/dist/providers/openai.d.ts +6 -0
  21. package/dist/providers/openai.js +26 -0
  22. package/dist/types/lifecycle.d.ts +1 -1
  23. package/dist/types/messages.d.ts +9 -0
  24. package/dist/types/providers.d.ts +2 -0
  25. package/dist/types/response.d.ts +17 -0
  26. package/dist/types/tracing.d.ts +1 -1
  27. package/dist/version.d.ts +1 -1
  28. package/dist/version.js +1 -1
  29. package/dist/workflow/chains.d.ts +13 -1
  30. package/dist/workflow/chains.js +40 -0
  31. package/dist-cjs/capabilities/negotiate.js +5 -0
  32. package/dist-cjs/decisions/completion.d.ts +48 -0
  33. package/dist-cjs/decisions/completion.js +198 -0
  34. package/dist-cjs/decisions/core.d.ts +525 -0
  35. package/dist-cjs/decisions/core.js +509 -0
  36. package/dist-cjs/decisions/index.d.ts +9 -0
  37. package/dist-cjs/decisions/index.js +27 -0
  38. package/dist-cjs/decisions/integrations.d.ts +142 -0
  39. package/dist-cjs/decisions/integrations.js +178 -0
  40. package/dist-cjs/decisions/mock.d.ts +32 -0
  41. package/dist-cjs/decisions/mock.js +51 -0
  42. package/dist-cjs/decisions/typesafe.d.ts +45 -0
  43. package/dist-cjs/decisions/typesafe.js +148 -0
  44. package/dist-cjs/index.d.ts +1 -1
  45. package/dist-cjs/providers/google.js +28 -0
  46. package/dist-cjs/providers/ollama.js +23 -0
  47. package/dist-cjs/providers/openai.d.ts +6 -0
  48. package/dist-cjs/providers/openai.js +26 -0
  49. package/dist-cjs/types/lifecycle.d.ts +1 -1
  50. package/dist-cjs/types/messages.d.ts +9 -0
  51. package/dist-cjs/types/providers.d.ts +2 -0
  52. package/dist-cjs/types/response.d.ts +17 -0
  53. package/dist-cjs/types/tracing.d.ts +1 -1
  54. package/dist-cjs/version.d.ts +1 -1
  55. package/dist-cjs/version.js +1 -1
  56. package/dist-cjs/workflow/chains.d.ts +13 -1
  57. package/dist-cjs/workflow/chains.js +40 -0
  58. package/llms.txt +2 -1
  59. package/package.json +41 -2
package/API_STABILITY.md CHANGED
@@ -41,6 +41,21 @@ 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.19 stage (2.19.0)
45
+
46
+ Everything 2.19 adds is stable from the start and additive, unless this section says otherwise.
47
+
48
+ New and stable:
49
+ - in the root: `CompletionRequest.logprobs`, `NexusResponse.logprobs`, `TokenLogprob`, and
50
+ `ModelCapabilities.logprobs`;
51
+ - in `nexus-ai-pro/workflows`: `ClassifyRouteOptions.decisions`.
52
+
53
+ New and experimental, until the live workflow passes against TypeSafe AI's Jev and a model that
54
+ reports log probabilities: the typed decisions family, `nexus-ai-pro/decisions`,
55
+ `nexus-ai-pro/decisions/typesafe`, and `nexus-ai-pro/decisions/mock`.
56
+
57
+ Changed on purpose: `OperationFamily` and `RunKind` widen with `decision`.
58
+
44
59
  ## 2.18 stage (2.18.0)
45
60
 
46
61
  Everything 2.18 adds is stable from the start and additive, unless this section says otherwise.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,49 @@ Notable changes to this project are recorded here. The format follows [Keep a Ch
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.19.0] - 2026-10-24
8
+
9
+ Typed decisions: a yes or no, a choice, or a score, answered with probabilities and a confidence read
10
+ from them, from TypeSafe AI's Jev or from any model, acted on when sure and sent to a person when not.
11
+
12
+ ### Added
13
+
14
+ - **Typed decisions.** `nexus-ai-pro/decisions` asks a `DecisionRequest`'s questions of one state: a
15
+ `YesNoQuestion`, a `ChoiceQuestion` of up to 255 options, or a `ScoreQuestion` of up to ten levels,
16
+ each answered in isolation with its probabilities and a confidence read the same way from any
17
+ provider. Answers are typed by their questions, so a choice's answer names only its options.
18
+ `DecisionManager` refuses a malformed question before it is sent, fails over, caches, traces each
19
+ decision as a run of kind `decision`, and runs it as an operation of the client, family
20
+ `decision`, budgeted at its model's price and on the tenant's key.
21
+ - **TypeSafe AI's Jev.** `TypeSafeDecisionProvider`, on `nexus-ai-pro/decisions/typesafe`, speaks the
22
+ System One API: every question of a decision in one call, its refusals naming the field at fault,
23
+ priced at $42 per billion input tokens with output free.
24
+ - **Decisions from any model.** `completionDecisions()` asks a completion model each question for a
25
+ one-token code, and reads its probabilities from that token's log probabilities where the provider
26
+ reports them, or counts them over samples where it does not, or refuses to.
27
+ - **Gates and calibration.** `gate()` acts, sends to a person, or abstains by confidence; `calibrate()`
28
+ measures how answers' probabilities track how often they are right, and chooses the threshold that
29
+ holds a target accuracy on the application's own labelled answers.
30
+ - **Decisions where the package decides.** `decisionRoute()` for a graph's branch, `decisionPermissions()`
31
+ for an agent's tool calls, `decisionGuard()` and `decisionModel()` as agent middleware,
32
+ `decisionEvaluator()` and `reviewUnsure()` for evaluation, and `classifyRoute()`'s `decisions`:
33
+ an uncertain answer goes to a person.
34
+ - **Log probabilities.** `CompletionRequest.logprobs` asks for each answer token's log probability and
35
+ its alternatives, on `NexusResponse.logprobs` as `TokenLogprob` values, from OpenAI's Chat
36
+ Completions, the OpenAI-compatible adapters that report them, Gemini, and Ollama;
37
+ `ModelCapabilities.logprobs` declares a model that cannot.
38
+ - `MockDecisionProvider`, on `nexus-ai-pro/decisions/mock`, and `runDecisionConformance()`.
39
+
40
+ ### Changed
41
+
42
+ - `OperationFamily` and `RunKind` have `decision`; a `switch` over either that names every one needs
43
+ the new one.
44
+
45
+ ### Fixed
46
+
47
+ - A test that waits forever fails at five minutes, and every child process the tests start
48
+ synchronously has a timeout of its own: the 2.18.0 publish's first attempt waited two hours on one.
49
+
7
50
  ## [2.18.0] - 2026-10-22
8
51
 
9
52
  Span events and links, media that knows its length, a tenant's threads and runs listed and paged
@@ -3179,7 +3222,8 @@ shared policy vocabulary is in place for a future release that revisits this.
3179
3222
  - URL fetching rejects unsafe private-network targets and limits response reads.
3180
3223
  - CLI and audit findings no longer disclose detected secret values.
3181
3224
 
3182
- [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.18.0...HEAD
3225
+ [Unreleased]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.19.0...HEAD
3226
+ [2.19.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.18.0...v2.19.0
3183
3227
  [2.18.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.17.0...v2.18.0
3184
3228
  [2.17.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.16.0...v2.17.0
3185
3229
  [2.16.0]: https://github.com/mkhitar-abrahamyan/nexus-ai/compare/v2.15.1...v2.16.0
package/README.md CHANGED
@@ -156,6 +156,7 @@ from the doc comments. The guides live in the repository, so these links go to G
156
156
  | [Retrieval](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/retrieval.md) | Hybrid search, reranking, MMR, parent documents, and Redis, Pinecone, Weaviate, and Chroma stores |
157
157
  | [Embeddings and retrieval](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/embeddings.md) | Embeddings as a routed operation family, and RAG helpers |
158
158
  | [Media understanding](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/media.md) | Reading documents and images page by page, describing images, and answering questions about them, through any vision model or OCR |
159
+ | [Typed decisions](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/decisions.md) | A yes or no, a choice, or a score, answered with probabilities and a calibrated confidence, from TypeSafe AI's Jev or any model, so a graph, an approval, or a guardrail acts when it is sure and asks a person when not |
159
160
  | [Video generation](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/video.md) | Videos from a prompt or a first frame, on Google Veo and Runway, as durable jobs streamed into an asset store and priced per second |
160
161
  | [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 |
161
162
  | [Voice](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/docs/voice.md) | Transcription, speech, voice turns, and voice sessions |
@@ -330,11 +331,15 @@ Shipped so far:
330
331
  video is extended from its own job or edited on Gemini Omni Flash, Veo, and Runway;
331
332
  - 2.18 span events and links, as OpenTelemetry has them, media that knows its length from its own
332
333
  headers, a tenant's threads and runs listed and paged across all of its regions, MCP servers that
333
- reconnect and change their tools within a session, and videos made after reference videos.
334
+ reconnect and change their tools within a session, and videos made after reference videos;
335
+ - 2.19 typed decisions: a yes or no, a choice, or a score, answered with probabilities and a
336
+ confidence, from TypeSafe AI's Jev or from any model's log probabilities, gated by thresholds a
337
+ calibration chooses, so a graph's branch, an approval, or a guardrail acts when it is sure and asks
338
+ a person when it is not.
334
339
 
335
340
  Upgrading from 1.x: `npx nexus migrate src --write` moves your imports, and
336
341
  [MIGRATING.md](https://github.com/mkhitar-abrahamyan/nexus-ai/blob/main/MIGRATING.md) covers the rest.
337
- Next, 2.19 reads a service's own API description into emulated tools, and lets an emulator hold
342
+ Next, 2.20 reads a service's own API description into emulated tools, and lets an emulator hold
338
343
  what a whole evaluation did, so examples that build on each other are evaluated with nothing sent.
339
344
  Pinecone, images, the Gemini
340
345
  embedding batch tier, media understanding, video, Gemini Live, SIP, and both phone providers
@@ -95,6 +95,11 @@ export function negotiateCompletionRequest(request, capabilities, options = {})
95
95
  target.seed = undefined;
96
96
  });
97
97
  }
98
+ if (request.logprobs !== undefined && capabilities.logprobs === false) {
99
+ refuse('logprobs', request.logprobs, 'the model does not report token log probabilities', (target) => {
100
+ target.logprobs = undefined;
101
+ });
102
+ }
98
103
  if (request.topK !== undefined && capabilities.topK === false) {
99
104
  refuse('topK', request.topK, 'the model does not support top-k sampling', (target) => {
100
105
  target.topK = undefined;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Decisions from any completion model: each question asked on its own, answered with a one-token code,
3
+ * and its probabilities read from the code token's log probabilities, or counted over samples where
4
+ * the model reports none.
5
+ */
6
+ import type { CompletionRequest } from '../types/messages.js';
7
+ import type { NexusResponse } from '../types/response.js';
8
+ import { type DecisionProvider } from './core.js';
9
+ /** Anything that completes a request, such as a `NexusAI` client. */
10
+ export interface DecisionCompletionClient {
11
+ /** Runs one completion. */
12
+ complete(request: CompletionRequest): Promise<NexusResponse>;
13
+ }
14
+ /** How a completion model's probabilities are read. */
15
+ export type CompletionDecisionMethod = 'auto' | 'logprobs' | 'sampling';
16
+ /** Options for `completionDecisions()`. */
17
+ export interface CompletionDecisionsOptions {
18
+ /** The model to ask, as the client names it. */
19
+ model: string;
20
+ /** The provider's name in results. Defaults to `model`. */
21
+ name?: string;
22
+ /**
23
+ * How probabilities are read: `logprobs`, from one call's token log probabilities; `sampling`, by
24
+ * counting the answers of several calls; or `auto`, the default: log probabilities, and sampling for
25
+ * a model that reports none or refuses to, which it then asks by sampling from the start.
26
+ */
27
+ method?: CompletionDecisionMethod;
28
+ /** Calls per question, when sampling. Defaults to 8. */
29
+ samples?: number;
30
+ /** The temperature samples are drawn at. Defaults to 1. */
31
+ temperature?: number;
32
+ /** Questions asked at once. Defaults to 8. */
33
+ concurrency?: number;
34
+ /** Instructions added to every question, such as what the application's terms mean. */
35
+ instructions?: string;
36
+ }
37
+ /**
38
+ * Any completion model as a decision provider, through a client's own `complete()`, so every call is
39
+ * routed, authorized, budgeted, traced, and priced as every completion is. Each question is asked on
40
+ * its own, at temperature 0, to answer with one code: a letter per option, Y or N, or a level's digit.
41
+ * Its probabilities are read from the log probabilities of that token's alternatives where the model
42
+ * reports them, the way TypeSafe AI's Jev reads its own; elsewhere the question is asked `samples`
43
+ * times, and each answer counted. Sampled probabilities come in steps of one sample, and cost as many
44
+ * calls.
45
+ */
46
+ export declare function completionDecisions(client: DecisionCompletionClient, options: CompletionDecisionsOptions): DecisionProvider & {
47
+ readonly sampledModels: ReadonlySet<string>;
48
+ };
@@ -0,0 +1,195 @@
1
+ import { DecisionError, } from './core.js';
2
+ const LOGPROB_OPTIONS = 20;
3
+ export function completionDecisions(client, options) {
4
+ const method = options.method ?? 'auto';
5
+ const sampled = new Set();
6
+ const info = {
7
+ name: options.name ?? options.model,
8
+ capabilities: {
9
+ types: ['yes-no', 'choice', 'score'],
10
+ maxOptions: 255,
11
+ maxLevels: 10,
12
+ defaultModel: options.model,
13
+ },
14
+ chargedByItsCalls: true,
15
+ };
16
+ const ask = async (model, state, question, context, usage) => {
17
+ const codes = codesOf(question);
18
+ const prompt = promptOf(state, question, codes, options.instructions);
19
+ const request = (extra) => ({
20
+ model,
21
+ messages: [
22
+ {
23
+ role: 'system',
24
+ content: 'You answer one question about a state with exactly one of the codes given, and nothing else.',
25
+ },
26
+ { role: 'user', content: prompt },
27
+ ],
28
+ signal: context.signal,
29
+ ...(context.tenantId !== undefined ? { tenantId: context.tenantId } : {}),
30
+ ...extra,
31
+ });
32
+ const readable = codes.length <= LOGPROB_OPTIONS && codes.every((code) => code.length === 1);
33
+ if (method !== 'sampling' && readable && !sampled.has(model)) {
34
+ let response;
35
+ try {
36
+ response = await client.complete(request({ temperature: 0, maxTokens: 2, logprobs: { top: 20 } }));
37
+ }
38
+ catch (error) {
39
+ if (method === 'logprobs' || !refusedOption(error))
40
+ throw error;
41
+ }
42
+ if (response) {
43
+ usage.add(response);
44
+ const weights = weightsFrom(response.logprobs, codes);
45
+ if (weights)
46
+ return distributionOf(question, codes, weights);
47
+ if (method === 'logprobs') {
48
+ throw new DecisionError(`"${model}" reported no log probabilities for the codes it was given`, 'DECISION_FAILED');
49
+ }
50
+ }
51
+ sampled.add(model);
52
+ }
53
+ if (!readable && method === 'logprobs') {
54
+ throw new DecisionError(`A question of ${codes.length} options cannot be read from one token's ${LOGPROB_OPTIONS} alternatives`, 'DECISION_UNSUPPORTED');
55
+ }
56
+ const count = Math.max(1, Math.floor(options.samples ?? 8));
57
+ const answers = await Promise.all(Array.from({ length: count }, () => client.complete(request({ temperature: options.temperature ?? 1, maxTokens: 4 })).then((response) => {
58
+ usage.add(response);
59
+ return codeIn(response.content, codes);
60
+ })));
61
+ const weights = codes.map((code) => answers.filter((answer) => answer === code).length);
62
+ if (weights.every((weight) => weight === 0)) {
63
+ throw new DecisionError(`"${model}" answered none of ${count} samples with a code it was given`, 'DECISION_FAILED');
64
+ }
65
+ return distributionOf(question, codes, weights);
66
+ };
67
+ return {
68
+ info,
69
+ sampledModels: sampled,
70
+ async decide(request, context) {
71
+ const totals = new Totals();
72
+ const entries = Object.entries(request.questions);
73
+ const distributions = {};
74
+ const limit = Math.max(1, Math.floor(options.concurrency ?? 8));
75
+ let next = 0;
76
+ const worker = async () => {
77
+ while (next < entries.length) {
78
+ const [name, question] = entries[next++];
79
+ distributions[name] = await ask(request.model, request.state, question, context, totals);
80
+ }
81
+ };
82
+ await Promise.all(Array.from({ length: Math.min(limit, entries.length) }, worker));
83
+ return {
84
+ model: request.model,
85
+ distributions,
86
+ usage: totals.usage(),
87
+ ...(totals.cost() ? { cost: totals.cost() } : {}),
88
+ };
89
+ },
90
+ };
91
+ }
92
+ class Totals {
93
+ inputTokens = 0;
94
+ outputTokens = 0;
95
+ calls = 0;
96
+ amount = 0;
97
+ priced = false;
98
+ add(response) {
99
+ this.calls += 1;
100
+ this.inputTokens += response.meta.usage?.inputTokens ?? 0;
101
+ this.outputTokens += response.meta.usage?.outputTokens ?? 0;
102
+ if (response.meta.cost) {
103
+ this.amount += response.meta.cost.amount;
104
+ this.priced = true;
105
+ }
106
+ }
107
+ usage() {
108
+ return { inputTokens: this.inputTokens, outputTokens: this.outputTokens, calls: this.calls };
109
+ }
110
+ cost() {
111
+ return this.priced ? { amount: this.amount, currency: 'USD', basis: 'estimated' } : undefined;
112
+ }
113
+ }
114
+ function codesOf(question) {
115
+ if (question.type === 'yes-no')
116
+ return ['Y', 'N'];
117
+ if (question.type === 'score')
118
+ return question.levels.map((_, level) => String(level));
119
+ return question.options.length <= 26
120
+ ? question.options.map((_, index) => String.fromCharCode(65 + index))
121
+ : question.options.map((_, index) => String(index + 1));
122
+ }
123
+ function promptOf(state, question, codes, instructions) {
124
+ const text = typeof state === 'string' ? state : JSON.stringify(state, null, 2);
125
+ const lines = [`State:\n${text}`, ''];
126
+ if (instructions)
127
+ lines.push(instructions, '');
128
+ if (question.type === 'yes-no') {
129
+ lines.push(`Question: ${question.question ?? 'Is the statement true of the state?'}`);
130
+ lines.push(`Y: yes${question.criteria?.yes ? `, ${question.criteria.yes}` : ''}`);
131
+ lines.push(`N: no${question.criteria?.no ? `, ${question.criteria.no}` : ''}`);
132
+ }
133
+ else if (question.type === 'choice') {
134
+ lines.push(`Question: ${question.question ?? 'Which option fits the state?'}`);
135
+ question.options.forEach((option, index) => {
136
+ lines.push(`${codes[index]}: ${option.name}${option.description ? `, ${option.description}` : ''}`);
137
+ });
138
+ }
139
+ else {
140
+ lines.push(`Question: ${question.question ?? 'Which level fits the state?'}`);
141
+ question.levels.forEach((level, index) => {
142
+ lines.push(`${codes[index]}: ${level}`);
143
+ });
144
+ }
145
+ lines.push('', `Answer with one code: ${codes.join(', ')}.`);
146
+ return lines.join('\n');
147
+ }
148
+ function codeOf(token) {
149
+ return token.replace(/^[\s"'`*(]+|[\s"'`*).:,]+$/g, '').toUpperCase();
150
+ }
151
+ function weightsFrom(logprobs, codes) {
152
+ const first = logprobs?.find((item) => item.token.trim() !== '');
153
+ if (!first)
154
+ return undefined;
155
+ const candidates = first.top?.length ? first.top : [{ token: first.token, logprob: first.logprob }];
156
+ const weights = codes.map(() => 0);
157
+ let found = false;
158
+ for (const candidate of candidates) {
159
+ const index = matchCode(candidate.token, codes);
160
+ if (index < 0 || !Number.isFinite(candidate.logprob))
161
+ continue;
162
+ weights[index] = weights[index] + Math.exp(candidate.logprob);
163
+ found = true;
164
+ }
165
+ return found ? weights : undefined;
166
+ }
167
+ function codeIn(content, codes) {
168
+ const head = content.trim().split(/[\s:,.)]+/)[0] ?? '';
169
+ return codes[matchCode(head, codes)];
170
+ }
171
+ function matchCode(token, codes) {
172
+ const code = codeOf(token);
173
+ const index = codes.indexOf(code);
174
+ if (index >= 0 || codes.length !== 2 || codes[0] !== 'Y' || codes[1] !== 'N')
175
+ return index;
176
+ if (code === 'YES' || code === 'TRUE')
177
+ return 0;
178
+ return code === 'NO' || code === 'FALSE' ? 1 : -1;
179
+ }
180
+ function distributionOf(question, codes, weights) {
181
+ const total = weights.reduce((sum, weight) => sum + weight, 0);
182
+ const share = (index) => (total > 0 ? (weights[index] ?? 0) / total : 1 / codes.length);
183
+ if (question.type === 'yes-no')
184
+ return { type: 'yes-no', yes: share(0) };
185
+ if (question.type === 'score')
186
+ return { type: 'score', probabilities: codes.map((_, index) => share(index)) };
187
+ return {
188
+ type: 'choice',
189
+ probabilities: Object.fromEntries(question.options.map((option, index) => [option.name, share(index)])),
190
+ };
191
+ }
192
+ function refusedOption(error) {
193
+ const status = error?.status;
194
+ return status === 400 || status === 422;
195
+ }