@nestjs/ai 0.0.0-stage → 0.0.2

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 (101) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +108 -2
  3. package/dist/ai.constants.d.ts +30 -0
  4. package/dist/ai.constants.js +34 -0
  5. package/dist/ai.module-definition.d.ts +23 -0
  6. package/dist/ai.module-definition.js +39 -0
  7. package/dist/ai.module.d.ts +11 -0
  8. package/dist/ai.module.js +45 -0
  9. package/dist/context/ai-execution-context.d.ts +32 -0
  10. package/dist/context/ai-execution-context.js +41 -0
  11. package/dist/context/ai-params.d.ts +20 -0
  12. package/dist/context/ai-params.js +44 -0
  13. package/dist/decorators/agent-function.decorator.d.ts +22 -0
  14. package/dist/decorators/agent-function.decorator.js +36 -0
  15. package/dist/decorators/index.d.ts +3 -0
  16. package/dist/decorators/index.js +3 -0
  17. package/dist/decorators/inject-embedding-model.decorator.d.ts +10 -0
  18. package/dist/decorators/inject-embedding-model.decorator.js +16 -0
  19. package/dist/decorators/inject-model.decorator.d.ts +10 -0
  20. package/dist/decorators/inject-model.decorator.js +16 -0
  21. package/dist/errors/ai.errors.d.ts +54 -0
  22. package/dist/errors/ai.errors.js +62 -0
  23. package/dist/errors/index.d.ts +1 -0
  24. package/dist/errors/index.js +1 -0
  25. package/dist/http/keep-alive.util.d.ts +6 -0
  26. package/dist/http/keep-alive.util.js +38 -0
  27. package/dist/http/node-response.util.d.ts +14 -0
  28. package/dist/http/node-response.util.js +104 -0
  29. package/dist/index.d.ts +14 -0
  30. package/dist/index.js +16 -0
  31. package/dist/interfaces/agent-function.interface.d.ts +53 -0
  32. package/dist/interfaces/agent-function.interface.js +1 -0
  33. package/dist/interfaces/ai-call-options.interface.d.ts +65 -0
  34. package/dist/interfaces/ai-call-options.interface.js +1 -0
  35. package/dist/interfaces/ai-embedding.interface.d.ts +79 -0
  36. package/dist/interfaces/ai-embedding.interface.js +1 -0
  37. package/dist/interfaces/ai-module-options.interface.d.ts +110 -0
  38. package/dist/interfaces/ai-module-options.interface.js +1 -0
  39. package/dist/interfaces/ai-result.interface.d.ts +57 -0
  40. package/dist/interfaces/ai-result.interface.js +1 -0
  41. package/dist/interfaces/ai-schema.interface.d.ts +44 -0
  42. package/dist/interfaces/ai-schema.interface.js +1 -0
  43. package/dist/interfaces/ai-tool.interface.d.ts +22 -0
  44. package/dist/interfaces/ai-tool.interface.js +1 -0
  45. package/dist/interfaces/ai-usage.interface.d.ts +30 -0
  46. package/dist/interfaces/ai-usage.interface.js +1 -0
  47. package/dist/interfaces/index.d.ts +8 -0
  48. package/dist/interfaces/index.js +8 -0
  49. package/dist/models/ai-embedding-models.service.d.ts +31 -0
  50. package/dist/models/ai-embedding-models.service.js +138 -0
  51. package/dist/models/ai-models.service.d.ts +29 -0
  52. package/dist/models/ai-models.service.js +112 -0
  53. package/dist/models/model-registry.util.d.ts +6 -0
  54. package/dist/models/model-registry.util.js +21 -0
  55. package/dist/models/model-requests.d.ts +5 -0
  56. package/dist/models/model-requests.js +20 -0
  57. package/dist/runtime/agent-loop.d.ts +30 -0
  58. package/dist/runtime/agent-loop.js +81 -0
  59. package/dist/runtime/call-plan.d.ts +54 -0
  60. package/dist/runtime/call-plan.js +105 -0
  61. package/dist/runtime/embedding-call.d.ts +38 -0
  62. package/dist/runtime/embedding-call.js +120 -0
  63. package/dist/runtime/model-policy.d.ts +53 -0
  64. package/dist/runtime/model-policy.js +202 -0
  65. package/dist/runtime/steps.d.ts +32 -0
  66. package/dist/runtime/steps.js +190 -0
  67. package/dist/runtime/text-stream.d.ts +64 -0
  68. package/dist/runtime/text-stream.js +211 -0
  69. package/dist/services/ai.service.d.ts +70 -0
  70. package/dist/services/ai.service.js +183 -0
  71. package/dist/testing/fake-embedding-model.d.ts +57 -0
  72. package/dist/testing/fake-embedding-model.js +82 -0
  73. package/dist/testing/fake-model.d.ts +96 -0
  74. package/dist/testing/fake-model.js +201 -0
  75. package/dist/testing/index.d.ts +32 -0
  76. package/dist/testing/index.js +27 -0
  77. package/dist/tools/ai-tools.service.d.ts +77 -0
  78. package/dist/tools/ai-tools.service.js +234 -0
  79. package/dist/tools/tool-output.util.d.ts +6 -0
  80. package/dist/tools/tool-output.util.js +38 -0
  81. package/dist/utils/duration.util.d.ts +3 -0
  82. package/dist/utils/duration.util.js +17 -0
  83. package/dist/utils/errors.util.d.ts +16 -0
  84. package/dist/utils/errors.util.js +35 -0
  85. package/dist/utils/names.util.d.ts +2 -0
  86. package/dist/utils/names.util.js +10 -0
  87. package/dist/utils/schema.util.d.ts +16 -0
  88. package/dist/utils/schema.util.js +70 -0
  89. package/dist/utils/usage.util.d.ts +10 -0
  90. package/dist/utils/usage.util.js +42 -0
  91. package/dist/workflows/ai-agent.interfaces.d.ts +70 -0
  92. package/dist/workflows/ai-agent.interfaces.js +1 -0
  93. package/dist/workflows/ai-agent.workflow.d.ts +15 -0
  94. package/dist/workflows/ai-agent.workflow.js +25 -0
  95. package/dist/workflows/ai-agents.service.d.ts +83 -0
  96. package/dist/workflows/ai-agents.service.js +277 -0
  97. package/dist/workflows/ai-workflows.module.d.ts +6 -0
  98. package/dist/workflows/ai-workflows.module.js +22 -0
  99. package/dist/workflows/index.d.ts +6 -0
  100. package/dist/workflows/index.js +5 -0
  101. package/package.json +115 -4
@@ -0,0 +1,77 @@
1
+ import { type OnModuleInit } from '@nestjs/common';
2
+ import { DiscoveryService, ExternalContextCreator, MetadataScanner } from '@nestjs/core';
3
+ import type { Schema, ToolSet } from 'ai';
4
+ import type { AgentFunctionContext } from '../interfaces/agent-function.interface.js';
5
+ import type { AiModuleOptions } from '../interfaces/ai-module-options.interface.js';
6
+ import type { AiCaller, AiToolSelection } from '../interfaces/ai-tool.interface.js';
7
+ /** A method behind Nest's enhancers: called as `(input, context)`. */
8
+ export type AiToolHandler = (input: unknown, context: AgentFunctionContext) => Promise<unknown>;
9
+ /** An agent function of the application: a provider method with `@AgentFunction()`, offered to models as a tool. */
10
+ export interface AiToolEntry {
11
+ name: string;
12
+ description?: string;
13
+ scopes?: string[];
14
+ /** The `ai` schema of its arguments. */
15
+ inputSchema: Schema<unknown>;
16
+ handler: AiToolHandler;
17
+ /** The provider class it is a method of. */
18
+ host: Function;
19
+ /** `Class.method`, for messages. */
20
+ where: string;
21
+ }
22
+ /** An agent function's progress (`ctx.reportProgress()`), which a streamed answer sends as a transient data chunk. */
23
+ export interface AiToolProgress {
24
+ toolCallId: string;
25
+ toolName: string;
26
+ progress: number;
27
+ total?: number;
28
+ message?: string;
29
+ }
30
+ /**
31
+ * The application's agent functions: the `@AgentFunction()` methods of its providers (or of the `include` modules),
32
+ * found once at startup, each wrapped by Nest's enhancers with the context type `'ai'`, and called with an
33
+ * `AgentFunctionContext`. `select()` turns some of them into `ai` tools for one caller.
34
+ */
35
+ export declare class AiTools implements OnModuleInit {
36
+ private readonly discovery;
37
+ private readonly scanner;
38
+ private readonly options;
39
+ private readonly logger;
40
+ private readonly entries;
41
+ private readonly paramsFactory;
42
+ private readonly contextCreator;
43
+ private scanned;
44
+ constructor(discovery: DiscoveryService, scanner: MetadataScanner, externalContextCreator: ExternalContextCreator, options: AiModuleOptions);
45
+ onModuleInit(): void;
46
+ /** Every agent function, by name. */
47
+ get tools(): ReadonlyMap<string, AiToolEntry>;
48
+ /**
49
+ * `ai` tools for `names` (agent function names, or provider classes for all their agent functions), whose calls run
50
+ * as `caller`: what `AiService` calls pass the model, and what code that calls `ai` itself can pass `generateText()`.
51
+ */
52
+ select(selection: AiToolSelection, { caller, onExecute, onProgress, execute, }?: {
53
+ caller?: AiCaller;
54
+ /** @internal */
55
+ onExecute?: () => void;
56
+ /** @internal Where `ctx.reportProgress()` goes: a streamed answer. */
57
+ onProgress?: (progress: AiToolProgress) => void;
58
+ /** @internal `false`: the tools are offered to the model, and their calls returned, not run (durable runs). */
59
+ execute?: boolean;
60
+ }): ToolSet;
61
+ /**
62
+ * Runs the agent function `name` with `input` (validated by its schema) as `caller`, through its enhancers, as an
63
+ * agent's call does. Resolves with what the model would read; rejects with the `AiToolError` it would read.
64
+ */
65
+ call(entryOrName: AiToolEntry | string, input: unknown, caller?: AiCaller, options?: {
66
+ toolCallId?: string;
67
+ messages?: unknown[];
68
+ abortSignal?: AbortSignal;
69
+ idempotencyKey?: string;
70
+ onProgress?: (progress: AiToolProgress) => void;
71
+ }): Promise<unknown>;
72
+ /** The agent functions a selection names, each once, in order. Unknown names throw. */
73
+ resolve(selection: AiToolSelection): AiToolEntry[];
74
+ /** Finds the agent functions, once. */
75
+ scan(): void;
76
+ private known;
77
+ }
@@ -0,0 +1,234 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable, Logger } from '@nestjs/common';
14
+ import { DiscoveryService, ExternalContextCreator, MetadataScanner } from '@nestjs/core';
15
+ import { AGENT_FUNCTION_METADATA, AI_CONTEXT_TYPE, AI_MODULE_OPTIONS, AI_PARAMS_METADATA } from '../ai.constants.js';
16
+ import { AiParamsFactory, recordHandlerParams } from '../context/ai-params.js';
17
+ import { AiConfigurationError, AiToolError } from '../errors/ai.errors.js';
18
+ import { toToolError } from '../utils/errors.util.js';
19
+ import { assertProviderToolName } from '../utils/names.util.js';
20
+ import { EMPTY_OBJECT_SCHEMA, toModelSchema } from '../utils/schema.util.js';
21
+ import { toToolOutput } from './tool-output.util.js';
22
+ const NEVER_ABORTED = new AbortController().signal;
23
+ /**
24
+ * The application's agent functions: the `@AgentFunction()` methods of its providers (or of the `include` modules),
25
+ * found once at startup, each wrapped by Nest's enhancers with the context type `'ai'`, and called with an
26
+ * `AgentFunctionContext`. `select()` turns some of them into `ai` tools for one caller.
27
+ */
28
+ let AiTools = class AiTools {
29
+ constructor(discovery, scanner, externalContextCreator, options) {
30
+ this.discovery = discovery;
31
+ this.scanner = scanner;
32
+ this.options = options;
33
+ this.logger = new Logger('AiModule');
34
+ this.entries = new Map();
35
+ this.paramsFactory = new AiParamsFactory();
36
+ this.scanned = false;
37
+ this.contextCreator = withOwnHandlerMetadata(externalContextCreator);
38
+ }
39
+ onModuleInit() {
40
+ this.scan();
41
+ }
42
+ /** Every agent function, by name. */
43
+ get tools() {
44
+ this.scan();
45
+ return this.entries;
46
+ }
47
+ /**
48
+ * `ai` tools for `names` (agent function names, or provider classes for all their agent functions), whose calls run
49
+ * as `caller`: what `AiService` calls pass the model, and what code that calls `ai` itself can pass `generateText()`.
50
+ */
51
+ select(selection, { caller = {}, onExecute, onProgress, execute = true, } = {}) {
52
+ this.scan();
53
+ const tools = {};
54
+ for (const entry of this.resolve(selection)) {
55
+ // An `ai` tool is a plain object; `ai` validates the input against the schema before `execute`.
56
+ tools[entry.name] = {
57
+ ...(entry.description !== undefined && { description: entry.description }),
58
+ inputSchema: entry.inputSchema,
59
+ ...(execute && {
60
+ execute: (input, options) => {
61
+ onExecute?.();
62
+ return this.call(entry, input, caller, { ...options, ...(onProgress && { onProgress }) });
63
+ },
64
+ }),
65
+ };
66
+ }
67
+ return tools;
68
+ }
69
+ /**
70
+ * Runs the agent function `name` with `input` (validated by its schema) as `caller`, through its enhancers, as an
71
+ * agent's call does. Resolves with what the model would read; rejects with the `AiToolError` it would read.
72
+ */
73
+ async call(entryOrName, input, caller = {}, options = {}) {
74
+ const entry = typeof entryOrName === 'string' ? this.resolve([entryOrName])[0] : entryOrName;
75
+ if (typeof entryOrName === 'string') {
76
+ const validated = await entry.inputSchema.validate?.(input);
77
+ if (validated && !validated.success) {
78
+ throw new AiToolError(`Invalid input: ${validated.error.message}`, entry.name);
79
+ }
80
+ input = validated ? validated.value : input;
81
+ }
82
+ if (entry.scopes?.length) {
83
+ const held = new Set(caller.scopes ?? []);
84
+ const missing = entry.scopes.filter((scope) => !held.has(scope));
85
+ if (missing.length > 0) {
86
+ throw new AiToolError(`Forbidden: this tool needs the scope(s) ${missing.join(', ')}`, entry.name);
87
+ }
88
+ }
89
+ const toolCallId = options.toolCallId ?? '';
90
+ // Where `@nestjs/authentication` and `@nestjs/authorization` look on an `'ai'` call, as on an `'mcp'` one: the
91
+ // request it runs in, else the context itself, with its `user`.
92
+ const context = {
93
+ name: entry.name,
94
+ ...(caller.request !== undefined && { request: caller.request }),
95
+ ...(caller.user !== undefined && { user: caller.user }),
96
+ ...(caller.scopes !== undefined && { scopes: caller.scopes }),
97
+ signal: options.abortSignal ?? NEVER_ABORTED,
98
+ reportProgress: async (progress, total, message) => options.onProgress?.({
99
+ toolCallId,
100
+ toolName: entry.name,
101
+ progress,
102
+ ...(total !== undefined && { total }),
103
+ ...(message !== undefined && { message }),
104
+ }),
105
+ toolCallId,
106
+ ...(options.idempotencyKey !== undefined && { idempotencyKey: options.idempotencyKey }),
107
+ messages: (options.messages ?? []),
108
+ };
109
+ let value;
110
+ try {
111
+ value = await entry.handler(input ?? {}, context);
112
+ }
113
+ catch (error) {
114
+ // Nest's exception handler has run the filters, and logged the error.
115
+ throw toToolError(error, entry.name);
116
+ }
117
+ try {
118
+ return toToolOutput(value, entry.name, entry.where);
119
+ }
120
+ catch (error) {
121
+ if (!(error instanceof AiToolError)) {
122
+ this.logger.error(`${entry.where} returned a value that is not JSON`, error?.stack);
123
+ }
124
+ throw toToolError(error, entry.name);
125
+ }
126
+ }
127
+ /** The agent functions a selection names, each once, in order. Unknown names throw. */
128
+ resolve(selection) {
129
+ this.scan();
130
+ if (!Array.isArray(selection)) {
131
+ throw new AiConfigurationError("`tools` lists agent function names and provider classes, e.g. ['find-order', OrdersTools].");
132
+ }
133
+ const picked = new Map();
134
+ for (const item of selection) {
135
+ if (typeof item === 'string') {
136
+ const entry = this.entries.get(item);
137
+ if (!entry) {
138
+ throw new AiConfigurationError(`There is no agent function named "${item}". ${this.known()}`);
139
+ }
140
+ picked.set(entry.name, entry);
141
+ }
142
+ else if (typeof item === 'function') {
143
+ const matches = [...this.entries.values()].filter((entry) => entry.host === item || entry.host.prototype instanceof item);
144
+ if (matches.length === 0) {
145
+ throw new AiConfigurationError(`${item.name} has no agent functions, or is not a provider of the application. ${this.known()}`);
146
+ }
147
+ for (const entry of matches) {
148
+ picked.set(entry.name, entry);
149
+ }
150
+ }
151
+ else {
152
+ throw new AiConfigurationError(`\`tools\` lists agent function names and provider classes; got ${String(item)}.`);
153
+ }
154
+ }
155
+ return [...picked.values()];
156
+ }
157
+ /** Finds the agent functions, once. */
158
+ scan() {
159
+ if (this.scanned) {
160
+ return;
161
+ }
162
+ this.scanned = true;
163
+ const seen = new Set();
164
+ const wrappers = this.discovery.getProviders(this.options.include ? { include: this.options.include } : undefined);
165
+ for (const wrapper of wrappers) {
166
+ const { instance } = wrapper;
167
+ if (!instance || typeof instance !== 'object' || wrapper.isAlias || seen.has(instance)) {
168
+ continue;
169
+ }
170
+ seen.add(instance);
171
+ const prototype = Object.getPrototypeOf(instance);
172
+ for (const methodName of this.scanner.getAllMethodNames(prototype)) {
173
+ const method = prototype[methodName];
174
+ const definition = Reflect.getMetadata(AGENT_FUNCTION_METADATA, method);
175
+ if (!definition) {
176
+ continue;
177
+ }
178
+ const where = `${wrapper.metatype?.name ?? instance.constructor.name}.${methodName}`;
179
+ if (!wrapper.isDependencyTreeStatic()) {
180
+ throw new AiConfigurationError(`${where}: agent functions must be on singleton providers, and this one is request-scoped or transient (or ` +
181
+ 'depends on a request-scoped provider). Pass what a call needs through `caller` instead.');
182
+ }
183
+ const name = definition.name ?? methodName;
184
+ assertProviderToolName(name, where);
185
+ const existing = this.entries.get(name);
186
+ if (existing) {
187
+ throw new AiConfigurationError(`${where}: there is already an agent function named "${name}" (${existing.where}). Give one of them ` +
188
+ 'another { name }.');
189
+ }
190
+ recordHandlerParams(instance.constructor, methodName);
191
+ const handler = this.contextCreator.create(instance, method, methodName, AI_PARAMS_METADATA, this.paramsFactory, undefined, undefined, undefined, AI_CONTEXT_TYPE);
192
+ this.entries.set(name, {
193
+ name,
194
+ ...(definition.description !== undefined && { description: definition.description }),
195
+ ...(definition.scopes?.length && { scopes: [...definition.scopes] }),
196
+ inputSchema: definition.inputSchema
197
+ ? toModelSchema(definition.inputSchema, `${where} inputSchema`, { object: true })
198
+ : EMPTY_OBJECT_SCHEMA,
199
+ handler,
200
+ host: instance.constructor,
201
+ where,
202
+ });
203
+ }
204
+ }
205
+ }
206
+ known() {
207
+ const names = [...this.entries.keys()];
208
+ return names.length
209
+ ? `Agent functions: ${names.join(', ')}.`
210
+ : 'The application has no agent functions (@AgentFunction() methods).';
211
+ }
212
+ };
213
+ AiTools = __decorate([
214
+ Injectable(),
215
+ __param(3, Inject(AI_MODULE_OPTIONS)),
216
+ __metadata("design:paramtypes", [DiscoveryService,
217
+ MetadataScanner,
218
+ ExternalContextCreator, Object])
219
+ ], AiTools);
220
+ export { AiTools };
221
+ /**
222
+ * Nest's `ExternalContextCreator`, with a cache of handler metadata of its own. Nest's keeps what it reads of a
223
+ * method's parameters by class and method name, and builds the context type of the first `create()` for the method into
224
+ * it: the `ExecutionContext` that `createParamDecorator()` decorators receive. A method that is an `@nestjs/mcp` tool
225
+ * too is wrapped by both packages, and its parameter decorators would see the type of whichever wrapped it first,
226
+ * `'ai'` or `'mcp'`, on every call. Guards, interceptors and filters get the type of each `create()` either way. Each
227
+ * agent function is wrapped once, at startup, so nothing needs caching here.
228
+ */
229
+ function withOwnHandlerMetadata(creator) {
230
+ // `handlerMetadataStorage` is the private field `getMetadata()` reads, in every 12.x release.
231
+ return Object.create(creator, {
232
+ handlerMetadataStorage: { value: { get: () => undefined, set: () => { } } },
233
+ });
234
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * What the model reads of an agent function's return value: JSON (a `Date` becomes its ISO string; `undefined` is
3
+ * `null`). An MCP `CallToolResult` becomes its `structuredContent`, else its text blocks joined; `isError: true` is a
4
+ * tool error. Blocks other than text (images, audio, resources) are dropped.
5
+ */
6
+ export declare function toToolOutput(value: unknown, toolName: string, where: string): unknown;
@@ -0,0 +1,38 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import { AiToolError } from '../errors/ai.errors.js';
3
+ const logger = new Logger('AiModule');
4
+ function isCallToolResult(value) {
5
+ if (typeof value !== 'object' || value === null || !Array.isArray(value.content)) {
6
+ return false;
7
+ }
8
+ const keys = Object.keys(value);
9
+ return (keys.every((key) => ['content', 'structuredContent', 'isError', '_meta'].includes(key)) &&
10
+ value.content.every((block) => typeof block?.type === 'string'));
11
+ }
12
+ /**
13
+ * What the model reads of an agent function's return value: JSON (a `Date` becomes its ISO string; `undefined` is
14
+ * `null`). An MCP `CallToolResult` becomes its `structuredContent`, else its text blocks joined; `isError: true` is a
15
+ * tool error. Blocks other than text (images, audio, resources) are dropped.
16
+ */
17
+ export function toToolOutput(value, toolName, where) {
18
+ if (isCallToolResult(value)) {
19
+ const text = value.content
20
+ .filter((block) => block.type === 'text' && typeof block.text === 'string')
21
+ .map((block) => block.text)
22
+ .join('\n');
23
+ if (value.isError) {
24
+ throw new AiToolError(text || 'The tool failed', toolName);
25
+ }
26
+ if (value.content.some((block) => block.type !== 'text')) {
27
+ logger.warn(`${where} returned content other than text, which agents are not sent; only its text is.`);
28
+ }
29
+ return value.structuredContent !== undefined ? toJson(value.structuredContent) : text;
30
+ }
31
+ return value === undefined ? null : toJson(value);
32
+ }
33
+ function toJson(value) {
34
+ if (typeof value !== 'object' || value === null) {
35
+ return value;
36
+ }
37
+ return JSON.parse(JSON.stringify(value));
38
+ }
@@ -0,0 +1,3 @@
1
+ import type { Duration } from '@nestjs/resilience';
2
+ /** A duration (`@nestjs/resilience`'s: milliseconds, or `'30s'`, `'2m'`, ...) in milliseconds. */
3
+ export declare function toMilliseconds(value: Duration | string, option: string): number;
@@ -0,0 +1,17 @@
1
+ import { AiConfigurationError } from '../errors/ai.errors.js';
2
+ const UNITS = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
3
+ /** A duration (`@nestjs/resilience`'s: milliseconds, or `'30s'`, `'2m'`, ...) in milliseconds. */
4
+ export function toMilliseconds(value, option) {
5
+ if (typeof value === 'number') {
6
+ if (Number.isFinite(value) && value >= 0) {
7
+ return value;
8
+ }
9
+ }
10
+ else if (typeof value === 'string') {
11
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(value.trim());
12
+ if (match) {
13
+ return Number(match[1]) * UNITS[match[2]];
14
+ }
15
+ }
16
+ throw new AiConfigurationError(`\`${option}\` must be a number of milliseconds or a duration such as "30s" or "2m". Got ${JSON.stringify(value)}.`);
17
+ }
@@ -0,0 +1,16 @@
1
+ import { HttpException } from '@nestjs/common';
2
+ import { AiToolError } from '../errors/ai.errors.js';
3
+ /** What the model is told about an error that is not meant for it. */
4
+ export declare const INTERNAL_ERROR_MESSAGE = "Internal server error";
5
+ /**
6
+ * The message of an `HttpException`, as its response body carries it: a string response, the body's `message`
7
+ * (`ValidationPipe`'s list joined), else the exception's own.
8
+ */
9
+ export declare function httpExceptionMessage(exception: HttpException): string;
10
+ /**
11
+ * An agent function's failure, as the model reads it: an `HttpException`'s message, `Internal server error` for
12
+ * anything else (Nest's exception handler has logged it). A raw error message never reaches the model.
13
+ */
14
+ export declare function toToolError(error: unknown, toolName: string): AiToolError;
15
+ /** Whether `error` is an abort: the caller's signal, or a request cancelled with it. */
16
+ export declare function isAbortError(error: unknown): boolean;
@@ -0,0 +1,35 @@
1
+ import { HttpException } from '@nestjs/common';
2
+ import { AiToolError } from '../errors/ai.errors.js';
3
+ /** What the model is told about an error that is not meant for it. */
4
+ export const INTERNAL_ERROR_MESSAGE = 'Internal server error';
5
+ /**
6
+ * The message of an `HttpException`, as its response body carries it: a string response, the body's `message`
7
+ * (`ValidationPipe`'s list joined), else the exception's own.
8
+ */
9
+ export function httpExceptionMessage(exception) {
10
+ const response = exception.getResponse();
11
+ if (typeof response === 'string') {
12
+ return response;
13
+ }
14
+ const message = response?.message;
15
+ if (Array.isArray(message)) {
16
+ return message.map(String).join('; ');
17
+ }
18
+ return typeof message === 'string' ? message : exception.message;
19
+ }
20
+ /**
21
+ * An agent function's failure, as the model reads it: an `HttpException`'s message, `Internal server error` for
22
+ * anything else (Nest's exception handler has logged it). A raw error message never reaches the model.
23
+ */
24
+ export function toToolError(error, toolName) {
25
+ if (error instanceof AiToolError) {
26
+ return error;
27
+ }
28
+ const message = error instanceof HttpException ? httpExceptionMessage(error) : INTERNAL_ERROR_MESSAGE;
29
+ return new AiToolError(message, toolName, { cause: error });
30
+ }
31
+ /** Whether `error` is an abort: the caller's signal, or a request cancelled with it. */
32
+ export function isAbortError(error) {
33
+ return (error instanceof Error &&
34
+ (error.name === 'AbortError' || error.code === 'ABORT_ERR'));
35
+ }
@@ -0,0 +1,2 @@
1
+ /** Throws for an agent function name model providers refuse, naming the method. */
2
+ export declare function assertProviderToolName(name: string, where: string): void;
@@ -0,0 +1,10 @@
1
+ import { AiConfigurationError } from '../errors/ai.errors.js';
2
+ /** What OpenAI, Anthropic and Google accept as a tool name, which is what an agent function's name is to the model. */
3
+ const PROVIDER_TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
4
+ /** Throws for an agent function name model providers refuse, naming the method. */
5
+ export function assertProviderToolName(name, where) {
6
+ if (typeof name !== 'string' || !PROVIDER_TOOL_NAME.test(name)) {
7
+ throw new AiConfigurationError(`${where}: the name ${JSON.stringify(name)} is not one model providers accept as a tool name: 1 to 64 ` +
8
+ 'characters out of A-Z a-z 0-9 _ -. Give @AgentFunction() a { name } that fits.');
9
+ }
10
+ }
@@ -0,0 +1,16 @@
1
+ import { type Schema } from 'ai';
2
+ import type { AiSchema, AiSchemaResult } from '../interfaces/ai-schema.interface.js';
3
+ /** The `ai` schema of a tool without arguments. */
4
+ export declare const EMPTY_OBJECT_SCHEMA: Schema<Record<string, never>>;
5
+ /**
6
+ * The `ai` schema for a Standard Schema with JSON Schema: its JSON Schema (the input side: what the model writes),
7
+ * computed once, and its validation, whose issues become the error the model reads. `object: true` requires an
8
+ * object at the root (tool arguments).
9
+ */
10
+ export declare function toModelSchema<T = unknown>(schema: AiSchema, where: string, { object }?: {
11
+ object?: boolean | undefined;
12
+ }): Schema<T>;
13
+ /** Validation issues as one line each, with their path: what the model reads to correct itself. */
14
+ export declare function formatIssues(issues: NonNullable<Extract<AiSchemaResult<unknown>, {
15
+ issues: unknown;
16
+ }>['issues']>): string;
@@ -0,0 +1,70 @@
1
+ import { jsonSchema } from 'ai';
2
+ import { AiConfigurationError } from '../errors/ai.errors.js';
3
+ /** JSON Schema drafts, in order of preference: what `ai` sends providers, then what MCP uses. */
4
+ const TARGETS = ['draft-07', 'draft-2020-12'];
5
+ const converted = new WeakMap();
6
+ /** The `ai` schema of a tool without arguments. */
7
+ export const EMPTY_OBJECT_SCHEMA = jsonSchema({
8
+ type: 'object',
9
+ properties: {},
10
+ additionalProperties: false,
11
+ });
12
+ /**
13
+ * The `ai` schema for a Standard Schema with JSON Schema: its JSON Schema (the input side: what the model writes),
14
+ * computed once, and its validation, whose issues become the error the model reads. `object: true` requires an
15
+ * object at the root (tool arguments).
16
+ */
17
+ export function toModelSchema(schema, where, { object = false } = {}) {
18
+ const cached = converted.get(schema);
19
+ if (cached) {
20
+ return cached;
21
+ }
22
+ const standard = schema?.['~standard'];
23
+ if (!standard || typeof standard.validate !== 'function') {
24
+ throw new AiConfigurationError(`${where}: not a Standard Schema. Pass a Zod 4.2+ schema (z.object({ ... })), an ArkType type, a Valibot schema ` +
25
+ 'wrapped with toStandardJsonSchema(), or a JSON Schema wrapped with fromJsonSchema() from @modelcontextprotocol/server.');
26
+ }
27
+ if (typeof standard.jsonSchema?.input !== 'function') {
28
+ throw new AiConfigurationError(`${where}: the "${standard.vendor}" schema cannot produce JSON Schema (no ~standard.jsonSchema), which the ` +
29
+ 'model needs. Zod needs 4.2 or later (Zod 3 is not supported); Valibot needs toStandardJsonSchema() from ' +
30
+ '@valibot/to-json-schema.');
31
+ }
32
+ let root;
33
+ let failure;
34
+ for (const target of TARGETS) {
35
+ try {
36
+ root = standard.jsonSchema.input({ target });
37
+ break;
38
+ }
39
+ catch (error) {
40
+ failure = error;
41
+ }
42
+ }
43
+ if (!root) {
44
+ throw new AiConfigurationError(`${where}: the schema cannot be converted to JSON Schema: ${failure?.message ?? failure}`);
45
+ }
46
+ if (object && root.type !== undefined && root.type !== 'object') {
47
+ throw new AiConfigurationError(`${where}: arguments are an object, and this schema describes ${JSON.stringify(root.type)}. Wrap it in z.object({ ... }).`);
48
+ }
49
+ const result = jsonSchema(root, {
50
+ validate: async (value) => {
51
+ const outcome = (await standard.validate(value));
52
+ return outcome.issues
53
+ ? { success: false, error: new TypeError(formatIssues(outcome.issues)) }
54
+ : { success: true, value: outcome.value };
55
+ },
56
+ });
57
+ converted.set(schema, result);
58
+ return result;
59
+ }
60
+ /** Validation issues as one line each, with their path: what the model reads to correct itself. */
61
+ export function formatIssues(issues) {
62
+ return issues
63
+ .map((issue) => {
64
+ const path = (issue.path ?? [])
65
+ .map((segment) => String(typeof segment === 'object' && segment !== null ? segment.key : segment))
66
+ .join('.');
67
+ return path ? `${path}: ${issue.message}` : issue.message;
68
+ })
69
+ .join('\n');
70
+ }
@@ -0,0 +1,10 @@
1
+ import type { LanguageModelUsage } from 'ai';
2
+ import type { AiModelPricing } from '../interfaces/ai-module-options.interface.js';
3
+ import type { AiUsage } from '../interfaces/ai-usage.interface.js';
4
+ export declare const ZERO_USAGE: AiUsage;
5
+ /** `ai`'s usage of one model call, with what the provider did not report as 0. */
6
+ export declare function toUsage(usage: LanguageModelUsage | undefined): AiUsage;
7
+ export declare function addUsage(a: AiUsage, b: AiUsage): AiUsage;
8
+ /** The cost of `usage` at `pricing` (per million tokens); `undefined` without pricing. */
9
+ export declare function costOf(usage: AiUsage, pricing: AiModelPricing | undefined): number | undefined;
10
+ export declare function addCost(a: number | undefined, b: number | undefined): number | undefined;
@@ -0,0 +1,42 @@
1
+ export const ZERO_USAGE = Object.freeze({
2
+ inputTokens: 0,
3
+ outputTokens: 0,
4
+ totalTokens: 0,
5
+ cachedInputTokens: 0,
6
+ reasoningTokens: 0,
7
+ });
8
+ /** `ai`'s usage of one model call, with what the provider did not report as 0. */
9
+ export function toUsage(usage) {
10
+ const inputTokens = usage?.inputTokens ?? 0;
11
+ const outputTokens = usage?.outputTokens ?? 0;
12
+ return {
13
+ inputTokens,
14
+ outputTokens,
15
+ totalTokens: usage?.totalTokens ?? inputTokens + outputTokens,
16
+ cachedInputTokens: usage?.inputTokenDetails?.cacheReadTokens ?? 0,
17
+ reasoningTokens: usage?.outputTokenDetails?.reasoningTokens ?? 0,
18
+ };
19
+ }
20
+ export function addUsage(a, b) {
21
+ return {
22
+ inputTokens: a.inputTokens + b.inputTokens,
23
+ outputTokens: a.outputTokens + b.outputTokens,
24
+ totalTokens: a.totalTokens + b.totalTokens,
25
+ cachedInputTokens: a.cachedInputTokens + b.cachedInputTokens,
26
+ reasoningTokens: a.reasoningTokens + b.reasoningTokens,
27
+ };
28
+ }
29
+ /** The cost of `usage` at `pricing` (per million tokens); `undefined` without pricing. */
30
+ export function costOf(usage, pricing) {
31
+ if (!pricing) {
32
+ return undefined;
33
+ }
34
+ const cached = Math.min(usage.cachedInputTokens, usage.inputTokens);
35
+ const cost = (usage.inputTokens - cached) * pricing.input +
36
+ cached * (pricing.cachedInput ?? pricing.input) +
37
+ usage.outputTokens * pricing.output;
38
+ return cost / 1_000_000;
39
+ }
40
+ export function addCost(a, b) {
41
+ return a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0);
42
+ }