nestjs-langchain 2.0.0 → 2.2.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 (112) hide show
  1. package/README.md +71 -6
  2. package/dist/agent.d.ts +21 -4
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +73 -33
  5. package/dist/agent.js.map +1 -1
  6. package/dist/constants.d.ts +16 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +21 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/errors/agent-aborted.error.d.ts +21 -0
  11. package/dist/errors/agent-aborted.error.d.ts.map +1 -0
  12. package/dist/errors/agent-aborted.error.js +22 -0
  13. package/dist/errors/agent-aborted.error.js.map +1 -0
  14. package/dist/errors/agent-recursion-limit.error.d.ts +23 -0
  15. package/dist/errors/agent-recursion-limit.error.d.ts.map +1 -0
  16. package/dist/errors/agent-recursion-limit.error.js +24 -0
  17. package/dist/errors/agent-recursion-limit.error.js.map +1 -0
  18. package/dist/errors/index.d.ts +5 -0
  19. package/dist/errors/index.d.ts.map +1 -1
  20. package/dist/errors/index.js +5 -0
  21. package/dist/errors/index.js.map +1 -1
  22. package/dist/errors/messages.d.ts +10 -0
  23. package/dist/errors/messages.d.ts.map +1 -1
  24. package/dist/errors/messages.js +24 -3
  25. package/dist/errors/messages.js.map +1 -1
  26. package/dist/errors/run-error.util.d.ts +21 -0
  27. package/dist/errors/run-error.util.d.ts.map +1 -0
  28. package/dist/errors/run-error.util.js +33 -0
  29. package/dist/errors/run-error.util.js.map +1 -0
  30. package/dist/errors/structured-output.error.d.ts +22 -0
  31. package/dist/errors/structured-output.error.d.ts.map +1 -0
  32. package/dist/errors/structured-output.error.js +23 -0
  33. package/dist/errors/structured-output.error.js.map +1 -0
  34. package/dist/errors/unwrap-middleware-error.util.d.ts +10 -0
  35. package/dist/errors/unwrap-middleware-error.util.d.ts.map +1 -0
  36. package/dist/errors/unwrap-middleware-error.util.js +17 -0
  37. package/dist/errors/unwrap-middleware-error.util.js.map +1 -0
  38. package/dist/index.d.ts +3 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/interfaces/completed-run.interface.d.ts +1 -1
  43. package/dist/interfaces/completed-run.interface.d.ts.map +1 -1
  44. package/dist/interfaces/langchain-module-options.interface.d.ts +6 -0
  45. package/dist/interfaces/langchain-module-options.interface.d.ts.map +1 -1
  46. package/dist/interfaces/run-options.interface.d.ts +34 -0
  47. package/dist/interfaces/run-options.interface.d.ts.map +1 -0
  48. package/dist/interfaces/run-options.interface.js +2 -0
  49. package/dist/interfaces/run-options.interface.js.map +1 -0
  50. package/dist/output/index.d.ts +3 -0
  51. package/dist/output/index.d.ts.map +1 -0
  52. package/dist/output/index.js +3 -0
  53. package/dist/output/index.js.map +1 -0
  54. package/dist/output/run-output.d.ts +43 -0
  55. package/dist/output/run-output.d.ts.map +1 -0
  56. package/dist/output/run-output.js +107 -0
  57. package/dist/output/run-output.js.map +1 -0
  58. package/dist/output/structured-output.middleware.d.ts +22 -0
  59. package/dist/output/structured-output.middleware.d.ts.map +1 -0
  60. package/dist/output/structured-output.middleware.js +40 -0
  61. package/dist/output/structured-output.middleware.js.map +1 -0
  62. package/dist/run/answered-calls.util.d.ts +10 -0
  63. package/dist/run/answered-calls.util.d.ts.map +1 -0
  64. package/dist/run/answered-calls.util.js +12 -0
  65. package/dist/run/answered-calls.util.js.map +1 -0
  66. package/dist/run/index.d.ts +3 -0
  67. package/dist/run/index.d.ts.map +1 -1
  68. package/dist/run/index.js +3 -0
  69. package/dist/run/index.js.map +1 -1
  70. package/dist/run/recursion-limit.util.d.ts +10 -0
  71. package/dist/run/recursion-limit.util.d.ts.map +1 -0
  72. package/dist/run/recursion-limit.util.js +11 -0
  73. package/dist/run/recursion-limit.util.js.map +1 -0
  74. package/dist/run/run-summary.util.d.ts +10 -3
  75. package/dist/run/run-summary.util.d.ts.map +1 -1
  76. package/dist/run/run-summary.util.js +18 -6
  77. package/dist/run/run-summary.util.js.map +1 -1
  78. package/dist/run/usage-aside.d.ts +23 -0
  79. package/dist/run/usage-aside.d.ts.map +1 -0
  80. package/dist/run/usage-aside.js +33 -0
  81. package/dist/run/usage-aside.js.map +1 -0
  82. package/dist/tools/tool-errors.middleware.d.ts +7 -1
  83. package/dist/tools/tool-errors.middleware.d.ts.map +1 -1
  84. package/dist/tools/tool-errors.middleware.js +81 -16
  85. package/dist/tools/tool-errors.middleware.js.map +1 -1
  86. package/dist/tools/tool-name.util.d.ts.map +1 -1
  87. package/dist/tools/tool-name.util.js +1 -3
  88. package/dist/tools/tool-name.util.js.map +1 -1
  89. package/lib/agent.ts +119 -32
  90. package/lib/constants.ts +24 -0
  91. package/lib/errors/agent-aborted.error.ts +22 -0
  92. package/lib/errors/agent-recursion-limit.error.ts +24 -0
  93. package/lib/errors/index.ts +5 -0
  94. package/lib/errors/messages.ts +55 -3
  95. package/lib/errors/run-error.util.ts +57 -0
  96. package/lib/errors/structured-output.error.ts +23 -0
  97. package/lib/errors/unwrap-middleware-error.util.ts +19 -0
  98. package/lib/index.ts +9 -1
  99. package/lib/interfaces/completed-run.interface.ts +1 -1
  100. package/lib/interfaces/langchain-module-options.interface.ts +6 -0
  101. package/lib/interfaces/run-options.interface.ts +34 -0
  102. package/lib/output/index.ts +5 -0
  103. package/lib/output/run-output.ts +154 -0
  104. package/lib/output/structured-output.middleware.ts +44 -0
  105. package/lib/run/answered-calls.util.ts +16 -0
  106. package/lib/run/index.ts +3 -0
  107. package/lib/run/recursion-limit.util.ts +11 -0
  108. package/lib/run/run-summary.util.ts +33 -11
  109. package/lib/run/usage-aside.ts +41 -0
  110. package/lib/tools/tool-errors.middleware.ts +104 -10
  111. package/lib/tools/tool-name.util.ts +1 -4
  112. package/package.json +31 -12
package/lib/agent.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Inject, Injectable, Logger, OnModuleInit } from '@nestjs/common';
2
2
  import { DiscoveryService } from '@nestjs/core';
3
3
  import { createAgent, initChatModel } from 'langchain';
4
+ import type { z, ZodType } from 'zod';
4
5
  import {
5
6
  AGENT_NAME_TOKEN,
6
7
  MODULE_OPTIONS_TOKEN,
@@ -11,6 +12,7 @@ import {
11
12
  ModelOption,
12
13
  } from './interfaces/langchain-module-options.interface.js';
13
14
  import { CompletedRun } from './interfaces/completed-run.interface.js';
15
+ import { RunOptions } from './interfaces/run-options.interface.js';
14
16
  import {
15
17
  ToolDiscoveryService,
16
18
  toolErrors,
@@ -18,13 +20,15 @@ import {
18
20
  } from './tools/index.js';
19
21
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
20
22
  import { LOG_CONTEXT, messageOf, RunReporter } from './logging/index.js';
21
- import { summariseRun } from './run/index.js';
22
- import { AgentRunError } from './errors/index.js';
23
+ import { DEFAULT_RECURSION_LIMIT } from './constants.js';
24
+ import { isRecursionLimit, summariseRun, UsageAside } from './run/index.js';
25
+ import { outputConfig, RunOutput, structuredOutput } from './output/index.js';
26
+ import { AgentRunError, toRunError } from './errors/index.js';
23
27
  import {
24
28
  agentNotBootstrapped,
25
29
  agentPrefix,
26
- agentRunFailed,
27
30
  duplicateAgentName,
31
+ invalidRecursionLimit,
28
32
  modelNeverReplied,
29
33
  modelNotBuilt,
30
34
  modelReplyEmpty,
@@ -59,9 +63,6 @@ export class Agent implements OnModuleInit {
59
63
  private readonly logger = new Logger();
60
64
  private readonly prefix: string;
61
65
  private readonly reporter: RunReporter;
62
- // Model failures, which only LangChain's callbacks see. Tool calls are
63
- // reported by their wrapper and the tool middlewares.
64
- private readonly modelErrors: BaseCallbackHandler;
65
66
 
66
67
  /** @internal Built by `LangChainModule`, never by hand. */
67
68
  constructor(
@@ -79,14 +80,6 @@ export class Agent implements OnModuleInit {
79
80
  prefix: this.prefix,
80
81
  observer: options.observer,
81
82
  });
82
- this.modelErrors = BaseCallbackHandler.fromMethods({
83
- handleLLMError: (error: unknown) => {
84
- this.reporter.logError(`the model failed: ${messageOf(error)}`);
85
- this.reporter.notify((observer) =>
86
- observer.onModelError?.({ agent: agentName, error }),
87
- );
88
- },
89
- });
90
83
  }
91
84
 
92
85
  /**
@@ -97,6 +90,12 @@ export class Agent implements OnModuleInit {
97
90
  async onModuleInit() {
98
91
  this.assertUniqueName();
99
92
 
93
+ const { recursionLimit } = this.options;
94
+
95
+ if (!isRecursionLimit(recursionLimit)) {
96
+ throw new Error(invalidRecursionLimit(this.prefix, recursionLimit));
97
+ }
98
+
100
99
  const tools = await this.toolDiscovery.getToolsFromModules(
101
100
  this.options.tools ?? [],
102
101
  this.reporter,
@@ -106,8 +105,21 @@ export class Agent implements OnModuleInit {
106
105
  model: await this.resolveModel(this.options.model),
107
106
  tools,
108
107
  systemPrompt: this.options.systemPrompt,
109
- // The first wraps the next: `toolErrors` sits right around the tool.
110
- middleware: [unknownTools(this.reporter), toolErrors(this.reporter)],
108
+ // The first wraps the next: `toolErrors` sits right around the tool,
109
+ // `structuredOutput` right around the model.
110
+ middleware: [
111
+ unknownTools(
112
+ this.reporter,
113
+ tools.map((tool) => tool.name),
114
+ ),
115
+ toolErrors(this.reporter),
116
+ structuredOutput(),
117
+ ],
118
+ }).withConfig({
119
+ // Traces and callbacks tell agents apart; the unnamed one keeps
120
+ // LangGraph's name. See #235.
121
+ runName: this.agentName === UNNAMED_AGENT ? undefined : this.agentName,
122
+ metadata: { agent: this.agentName },
111
123
  });
112
124
 
113
125
  this.logger.log(
@@ -179,21 +191,65 @@ export class Agent implements OnModuleInit {
179
191
  * as an `AgentRunError`, the original error in its `cause`.
180
192
  *
181
193
  * @param input The user's message.
182
- * @returns The model's answer, the tools it called and what the run cost.
194
+ * @param options With `output`, the schema the answer must match; with
195
+ * `signal`, what aborts the run; with `recursionLimit`, its most steps.
196
+ * @returns The model's answer, the object `output` parses when given, the
197
+ * tools it called and what the run cost.
183
198
  * @example
184
199
  * ```ts
185
- * const { output, tools } = await agent.run('What is 6 times 7?');
200
+ * const { output, tools } = await agent.run('What is 6 times 7?', {
201
+ * signal: AbortSignal.timeout(30_000),
202
+ * });
203
+ *
204
+ * const { output: ticket } = await agent.run(email, {
205
+ * output: z.object({ category: z.enum(['billing', 'bug', 'other']) }),
206
+ * });
186
207
  * ```
187
208
  */
188
- async run(input: string): Promise<CompletedRun> {
209
+ run<S extends ZodType>(
210
+ input: string,
211
+ options: RunOptions & { output: S },
212
+ ): Promise<CompletedRun<z.output<S>>>;
213
+ run(
214
+ input: string,
215
+ options?: RunOptions & { output?: undefined },
216
+ ): Promise<CompletedRun>;
217
+ // Options typed `RunOptions`, whose schema is not known statically. See #280.
218
+ run(input: string, options?: RunOptions): Promise<CompletedRun<unknown>>;
219
+ async run(
220
+ input: string,
221
+ options?: RunOptions,
222
+ ): Promise<CompletedRun<unknown>> {
189
223
  const startedAt = Date.now();
190
224
  const durationMs = () => Date.now() - startedAt;
225
+ // Always passed: left out, a run from another agent's tool would take
226
+ // that run's limit. See #315.
227
+ const recursionLimit =
228
+ options?.recursionLimit ??
229
+ this.options.recursionLimit ??
230
+ DEFAULT_RECURSION_LIMIT;
191
231
 
192
232
  try {
193
- const { answer, messages } = await this.complete(input);
233
+ // Read in the `try`, and `null` from untyped JavaScript taken as no
234
+ // options, as LangChain takes a `null` config. See #291.
235
+ const schema = options?.output;
236
+
237
+ if (!isRecursionLimit(recursionLimit)) {
238
+ throw new AgentRunError(
239
+ invalidRecursionLimit(this.prefix, recursionLimit),
240
+ this.agentName,
241
+ );
242
+ }
243
+
244
+ const output = schema && new RunOutput(schema, this.agentName);
245
+ const aside = new UsageAside();
246
+ const { answer, messages } = await this.complete(input, output, aside, {
247
+ signal: options?.signal,
248
+ recursionLimit,
249
+ });
194
250
  const finished = {
195
251
  durationMs: durationMs(),
196
- ...summariseRun(messages),
252
+ ...summariseRun(messages, output?.toolName, aside.usage),
197
253
  };
198
254
 
199
255
  this.reporter.notify((observer) =>
@@ -202,14 +258,12 @@ export class Agent implements OnModuleInit {
202
258
 
203
259
  return { status: 'completed', output: answer, ...finished };
204
260
  } catch (thrown) {
205
- const error =
206
- thrown instanceof AgentRunError
207
- ? thrown
208
- : new AgentRunError(
209
- agentRunFailed(this.prefix, thrown),
210
- this.agentName,
211
- { cause: thrown },
212
- );
261
+ const error = toRunError(thrown, {
262
+ agent: this.agentName,
263
+ prefix: this.prefix,
264
+ signal: options?.signal,
265
+ recursionLimit,
266
+ });
213
267
 
214
268
  this.reporter.notify((observer) =>
215
269
  observer.onRunError?.({
@@ -223,9 +277,30 @@ export class Agent implements OnModuleInit {
223
277
  }
224
278
  }
225
279
 
280
+ // Model failures, which only LangChain's callbacks see; tool calls are
281
+ // reported by their wrapper and the tool middlewares. One handler per run:
282
+ // a call the run's signal cuts short is no failure. See #318.
283
+ private modelErrors(signal: AbortSignal | undefined): BaseCallbackHandler {
284
+ return BaseCallbackHandler.fromMethods({
285
+ handleLLMError: (error: unknown) => {
286
+ if (signal?.aborted) {
287
+ return;
288
+ }
289
+
290
+ this.reporter.logError(`the model failed: ${messageOf(error)}`);
291
+ this.reporter.notify((observer) =>
292
+ observer.onModelError?.({ agent: this.agentName, error }),
293
+ );
294
+ },
295
+ });
296
+ }
297
+
226
298
  private async complete(
227
299
  input: string,
228
- ): Promise<{ answer: string; messages: AgentState['messages'] }> {
300
+ output: RunOutput | undefined,
301
+ aside: UsageAside,
302
+ run: Pick<RunOptions, 'signal' | 'recursionLimit'>,
303
+ ): Promise<{ answer: unknown; messages: AgentState['messages'] }> {
229
304
  if (!this.#graph) {
230
305
  throw new AgentRunError(
231
306
  agentNotBootstrapped(this.prefix),
@@ -233,10 +308,18 @@ export class Agent implements OnModuleInit {
233
308
  );
234
309
  }
235
310
 
236
- const { messages }: AgentState = await this.#graph.invoke(
311
+ const state: AgentState = await this.#graph.invoke(
237
312
  { messages: [{ role: 'user', content: input }] },
238
- { callbacks: [this.modelErrors, ...(this.options.callbacks ?? [])] },
313
+ {
314
+ callbacks: [
315
+ this.modelErrors(run.signal),
316
+ ...(this.options.callbacks ?? []),
317
+ ],
318
+ configurable: { ...outputConfig(output), ...aside.config },
319
+ ...run,
320
+ },
239
321
  );
322
+ const { messages } = state;
240
323
 
241
324
  const last = messages[messages.length - 1];
242
325
 
@@ -247,6 +330,10 @@ export class Agent implements OnModuleInit {
247
330
  );
248
331
  }
249
332
 
333
+ if (output) {
334
+ return { answer: output.read(state.structuredResponse), messages };
335
+ }
336
+
250
337
  // `text`, not `content`: block answers are an array.
251
338
  if (!last.text) {
252
339
  throw new AgentRunError(modelReplyEmpty(this.prefix), this.agentName);
package/lib/constants.ts CHANGED
@@ -5,6 +5,30 @@ export const TOOL_PARAMS_METADATA = 'nestjs-langchain:tool-params';
5
5
  // Emitted by `emitDecoratorMetadata` on every decorated method.
6
6
  export const PARAM_TYPES_METADATA = 'design:paramtypes';
7
7
 
8
+ // The agent routes a call with this prefix to structured output, never to the
9
+ // tool. Restated: LangChain does not export it. See #213.
10
+ export const STRUCTURED_OUTPUT_PREFIX = 'extract-';
11
+
12
+ // Ours rather than LangChain's counter or the schema's title: the same tool on
13
+ // every run, under the prefix no `@Tool` can take. See #281.
14
+ /**
15
+ * The name of the tool a run with an `output` schema answers through: a test
16
+ * faking the model queues a call to it.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const model = fakeModel().respondWithTools([
21
+ * { name: OUTPUT_TOOL, args: { category: 'bug' } },
22
+ * ]);
23
+ *
24
+ * const { output } = await agent.run(email, { output: Ticket });
25
+ * ```
26
+ */
27
+ export const OUTPUT_TOOL = `${STRUCTURED_OUTPUT_PREFIX}output` as const;
28
+
29
+ // LangChain's default, restated: it does not export it.
30
+ export const DEFAULT_RECURSION_LIMIT = 25;
31
+
8
32
  // The keys `@Module()` writes, restated: `@nestjs/common/constants` is
9
33
  // internal. See #97.
10
34
  export const MODULE_METADATA_KEYS = [
@@ -0,0 +1,22 @@
1
+ import { AgentRunError } from './agent-run.error.js';
2
+
3
+ /**
4
+ * The run's `signal` was aborted. Its `reason` is in `cause`.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * try {
9
+ * await agent.run(question, { signal: AbortSignal.timeout(30_000) });
10
+ * } catch (error) {
11
+ * if (error instanceof AgentAbortedError) {
12
+ * console.warn('gave up:', error.cause);
13
+ * }
14
+ * }
15
+ * ```
16
+ */
17
+ export class AgentAbortedError extends AgentRunError {
18
+ constructor(message: string, agent: string, options?: { cause?: unknown }) {
19
+ super(message, agent, options);
20
+ this.name = 'AgentAbortedError';
21
+ }
22
+ }
@@ -0,0 +1,24 @@
1
+ import { AgentRunError } from './agent-run.error.js';
2
+
3
+ /**
4
+ * The run took its `recursionLimit` of steps without an answer: the model kept
5
+ * calling tools. LangGraph's error is in `cause`.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * try {
10
+ * await agent.run(question, { recursionLimit: 10 });
11
+ * } catch (error) {
12
+ * if (error instanceof AgentRecursionLimitError) {
13
+ * return 'This question needs too many steps.';
14
+ * }
15
+ * throw error;
16
+ * }
17
+ * ```
18
+ */
19
+ export class AgentRecursionLimitError extends AgentRunError {
20
+ constructor(message: string, agent: string, options?: { cause?: unknown }) {
21
+ super(message, agent, options);
22
+ this.name = 'AgentRecursionLimitError';
23
+ }
24
+ }
@@ -1,2 +1,7 @@
1
+ export { AgentAbortedError } from './agent-aborted.error.js';
2
+ export { AgentRecursionLimitError } from './agent-recursion-limit.error.js';
1
3
  export { AgentRunError } from './agent-run.error.js';
4
+ export { toRunError } from './run-error.util.js';
5
+ export { StructuredOutputError } from './structured-output.error.js';
2
6
  export { ToolConfigurationError } from './tool-configuration.error.js';
7
+ export { unwrapMiddlewareError } from './unwrap-middleware-error.util.js';
@@ -96,14 +96,17 @@ export const schemaContradictsSignature = (
96
96
  `${where}: the schema for "${param}" describes ${declared.join(' | ')} ` +
97
97
  `but the signature declares ${expected}.`;
98
98
 
99
+ // The advice of both messages on a schema with no JSON Schema form.
100
+ const jsonSchemaAdvice =
101
+ 'Declare a schema JSON Schema can express and convert it with .transform(), ' +
102
+ 'such as z.iso.date() or z.iso.datetime() for a date.';
103
+
99
104
  export const noJsonSchemaForm = (
100
105
  where: string,
101
106
  param: string,
102
107
  reason: string,
103
108
  ): string =>
104
- `${where}: "${param}" has no JSON Schema form (${reason}). ` +
105
- 'Declare a schema JSON Schema can express and convert it with .transform(), ' +
106
- 'such as z.iso.datetime() for a date.';
109
+ `${where}: "${param}" has no JSON Schema form (${reason}). ${jsonSchemaAdvice}`;
107
110
 
108
111
  export const duplicateParamName = (
109
112
  where: string,
@@ -155,4 +158,53 @@ export const modelNeverReplied = (
155
158
  export const modelReplyEmpty = (prefix: string): string =>
156
159
  `The ${prefix}agent's model replied with no text content.`;
157
160
 
161
+ export const runAborted = (prefix: string): string =>
162
+ `The ${prefix}agent's run was aborted.`;
163
+
164
+ export const recursionLimitReached = (prefix: string, limit: number): string =>
165
+ `The ${prefix}agent's run reached its recursion limit of ${limit} steps ` +
166
+ 'without an answer. Raise `recursionLimit`, or check what makes the model ' +
167
+ 'keep calling tools.';
168
+
169
+ export const invalidRecursionLimit = (prefix: string, limit: unknown): string =>
170
+ `The ${prefix}agent's \`recursionLimit\` must be an integer of at least 1, ` +
171
+ `not ${typeof limit === 'string' ? `"${limit}"` : String(limit)}.`;
172
+
173
+ export const noOutputJsonSchemaForm = (
174
+ prefix: string,
175
+ reason: string,
176
+ ): string =>
177
+ `The ${prefix}agent's \`output\` schema has no JSON Schema form (${reason}). ` +
178
+ jsonSchemaAdvice;
179
+
180
+ export const outputNotAnObject = (prefix: string): string =>
181
+ `The ${prefix}agent's \`output\` schema is not an object with named fields: ` +
182
+ "the model fills it in as a tool's parameters. Wrap it, such as " +
183
+ '`z.object({ items: z.array(...) })`.';
184
+
185
+ export const structuredOutputMissing = (prefix: string): string =>
186
+ `The ${prefix}agent's model replied without the requested structured output.`;
187
+
188
+ export const structuredOutputInvalid = (
189
+ prefix: string,
190
+ reason: string,
191
+ ): string =>
192
+ `The ${prefix}agent's model replied with an output that does not match ` +
193
+ `the schema: ${reason}`;
194
+
195
+ export const structuredOutputMultiple = (
196
+ prefix: string,
197
+ count: number,
198
+ ): string =>
199
+ `The ${prefix}agent's model replied with ${count} outputs instead of one.`;
200
+
158
201
  export const unknownTool = (): string => 'not a tool of this agent';
202
+
203
+ // LangChain's answer to a tool the agent does not have, restated:
204
+ // `getInvalidToolError` is not exported. See #264.
205
+ export const invalidTool = (tool: string, tools: string[]): string =>
206
+ `Error: ${tool} is not a valid tool, try one of [${tools.join(', ')}].`;
207
+
208
+ export const toolNotRun = (tool: string): string =>
209
+ `Error: '${tool}' was not run: it came with the final answer. ` +
210
+ 'Call it on its own first if it is needed.';
@@ -0,0 +1,57 @@
1
+ import { AgentAbortedError } from './agent-aborted.error.js';
2
+ import { AgentRecursionLimitError } from './agent-recursion-limit.error.js';
3
+ import { AgentRunError } from './agent-run.error.js';
4
+ import {
5
+ agentRunFailed,
6
+ recursionLimitReached,
7
+ runAborted,
8
+ } from './messages.js';
9
+ import { unwrapMiddlewareError } from './unwrap-middleware-error.util.js';
10
+
11
+ interface FailedRun {
12
+ agent: string;
13
+ /** Empty for the unnamed agent, `'MATH '` otherwise. */
14
+ prefix: string;
15
+ signal?: AbortSignal;
16
+ /** The limit the run was given. */
17
+ recursionLimit: number;
18
+ }
19
+
20
+ /**
21
+ * The `AgentRunError` `run()` throws for what the agent's graph threw.
22
+ *
23
+ * @internal
24
+ * @param thrown What the graph threw.
25
+ * @param run The run that failed.
26
+ * @returns The error itself when it is one already, else the subclass of its
27
+ * kind, the original in `cause`.
28
+ */
29
+ export const toRunError = (
30
+ thrown: unknown,
31
+ { agent, prefix, signal, recursionLimit }: FailedRun,
32
+ ): AgentRunError => {
33
+ const cause = unwrapMiddlewareError(thrown);
34
+
35
+ if (cause instanceof AgentRunError) {
36
+ return cause;
37
+ }
38
+
39
+ // The reason itself, which LangChain may have wrapped on its way out.
40
+ if (signal?.aborted) {
41
+ return new AgentAbortedError(runAborted(prefix), agent, {
42
+ cause: signal.reason,
43
+ });
44
+ }
45
+
46
+ // By name, as LangGraph tells its errors apart; it is not a dependency of
47
+ // ours. See #194.
48
+ if (cause instanceof Error && cause.name === 'GraphRecursionError') {
49
+ return new AgentRecursionLimitError(
50
+ recursionLimitReached(prefix, recursionLimit),
51
+ agent,
52
+ { cause },
53
+ );
54
+ }
55
+
56
+ return new AgentRunError(agentRunFailed(prefix, cause), agent, { cause });
57
+ };
@@ -0,0 +1,23 @@
1
+ import { AgentRunError } from './agent-run.error.js';
2
+
3
+ /**
4
+ * The model's answer to a run with an `output` schema never came, or does not
5
+ * match the schema. LangChain's or Zod's error, if any, is in `cause`.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * try {
10
+ * const { output } = await agent.run(ticket, { output: Classification });
11
+ * } catch (error) {
12
+ * if (error instanceof StructuredOutputError) {
13
+ * console.error(error.message, error.cause);
14
+ * }
15
+ * }
16
+ * ```
17
+ */
18
+ export class StructuredOutputError extends AgentRunError {
19
+ constructor(message: string, agent: string, options?: { cause?: unknown }) {
20
+ super(message, agent, options);
21
+ this.name = 'StructuredOutputError';
22
+ }
23
+ }
@@ -0,0 +1,19 @@
1
+ import { MiddlewareError } from 'langchain';
2
+
3
+ /**
4
+ * The error as thrown, without the `MiddlewareError` LangChain wraps it in at
5
+ * each `wrapModelCall` middleware it crosses, a provider error included.
6
+ *
7
+ * @internal
8
+ * @param error What the agent's graph threw.
9
+ * @returns The innermost error.
10
+ */
11
+ export const unwrapMiddlewareError = (error: unknown): unknown => {
12
+ let unwrapped = error;
13
+
14
+ while (MiddlewareError.isInstance(unwrapped) && unwrapped.cause) {
15
+ unwrapped = unwrapped.cause;
16
+ }
17
+
18
+ return unwrapped;
19
+ };
package/lib/index.ts CHANGED
@@ -3,7 +3,14 @@
3
3
  export { LangChainModule } from './langchain.module.js';
4
4
  export { getAgentToken } from './langchain.module-definition.js';
5
5
  export { Agent } from './agent.js';
6
- export { AgentRunError, ToolConfigurationError } from './errors/index.js';
6
+ export { OUTPUT_TOOL } from './constants.js';
7
+ export {
8
+ AgentAbortedError,
9
+ AgentRecursionLimitError,
10
+ AgentRunError,
11
+ StructuredOutputError,
12
+ ToolConfigurationError,
13
+ } from './errors/index.js';
7
14
  export { InjectAgent, Tool, ToolParam } from './decorators/index.js';
8
15
  export type { ToolOptions, ToolParamOptions } from './decorators/index.js';
9
16
  export type {
@@ -16,6 +23,7 @@ export type {
16
23
  ToolStartEvent,
17
24
  } from './interfaces/agent-observer.interface.js';
18
25
  export type { CompletedRun } from './interfaces/completed-run.interface.js';
26
+ export type { RunOptions } from './interfaces/run-options.interface.js';
19
27
  export type {
20
28
  AgentCallback,
21
29
  LangChainModuleOptions,
@@ -7,7 +7,7 @@ import type { RunSummary } from '../run/index.js';
7
7
  export interface CompletedRun<TOutput = string> extends RunSummary {
8
8
  /** Always `'completed'`: a run that fails throws instead. */
9
9
  status: 'completed';
10
- /** The model's text answer. */
10
+ /** The model's text answer, or the object the run's `output` schema parsed. */
11
11
  output: TOutput;
12
12
  /** How long the run took. */
13
13
  durationMs: number;
@@ -73,6 +73,12 @@ export interface LangChainModuleOptions {
73
73
  observer?: AgentObserver;
74
74
  /** LangChain-native run callbacks, for handlers written against its API. */
75
75
  callbacks?: AgentCallback[];
76
+ /**
77
+ * The most steps a run may take, each model call and each round of tool
78
+ * calls counting as one: LangGraph's 25 when omitted. A run's own
79
+ * `recursionLimit` overrides it.
80
+ */
81
+ recursionLimit?: number;
76
82
  }
77
83
 
78
84
  /**
@@ -0,0 +1,34 @@
1
+ import type { ZodType } from 'zod';
2
+
3
+ /**
4
+ * The options of one run. Passed inline, they type `output` from the schema;
5
+ * typed `RunOptions`, `output` is `unknown`.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const { output } = await agent.run(email, { output: Ticket }); // a Ticket
10
+ * await agent.run(question, { signal: AbortSignal.timeout(30_000) });
11
+ *
12
+ * const ask = (question: string, options?: RunOptions) =>
13
+ * agent.run(question, options); // `output` is unknown
14
+ * ```
15
+ */
16
+ export interface RunOptions {
17
+ /**
18
+ * The schema the answer must match. `output` is then the object it parses,
19
+ * typed from it, instead of the model's text.
20
+ */
21
+ output?: ZodType;
22
+ /**
23
+ * Aborts the run: `run()` throws an `AgentAbortedError`, the signal's
24
+ * `reason` in `cause`. A tool already running ends in the background, its
25
+ * result dropped.
26
+ */
27
+ signal?: AbortSignal;
28
+ /**
29
+ * The most steps the run may take, each model call and each round of tool
30
+ * calls counting as one, instead of the agent's `recursionLimit`. Past it,
31
+ * `run()` throws an `AgentRecursionLimitError`.
32
+ */
33
+ recursionLimit?: number;
34
+ }
@@ -0,0 +1,5 @@
1
+ export { RunOutput } from './run-output.js';
2
+ export {
3
+ outputConfig,
4
+ structuredOutput,
5
+ } from './structured-output.middleware.js';