nestjs-langchain 2.1.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 (60) hide show
  1. package/README.md +6 -3
  2. package/dist/agent.d.ts +6 -3
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +51 -19
  5. package/dist/agent.js.map +1 -1
  6. package/dist/constants.d.ts +15 -1
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +15 -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 +3 -0
  19. package/dist/errors/index.d.ts.map +1 -1
  20. package/dist/errors/index.js +3 -0
  21. package/dist/errors/index.js.map +1 -1
  22. package/dist/errors/messages.d.ts +3 -0
  23. package/dist/errors/messages.d.ts.map +1 -1
  24. package/dist/errors/messages.js +6 -0
  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/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/interfaces/langchain-module-options.interface.d.ts +6 -0
  35. package/dist/interfaces/langchain-module-options.interface.d.ts.map +1 -1
  36. package/dist/interfaces/run-options.interface.d.ts +13 -0
  37. package/dist/interfaces/run-options.interface.d.ts.map +1 -1
  38. package/dist/output/run-output.d.ts +1 -1
  39. package/dist/output/run-output.d.ts.map +1 -1
  40. package/dist/run/index.d.ts +1 -0
  41. package/dist/run/index.d.ts.map +1 -1
  42. package/dist/run/index.js +1 -0
  43. package/dist/run/index.js.map +1 -1
  44. package/dist/run/recursion-limit.util.d.ts +10 -0
  45. package/dist/run/recursion-limit.util.d.ts.map +1 -0
  46. package/dist/run/recursion-limit.util.js +11 -0
  47. package/dist/run/recursion-limit.util.js.map +1 -0
  48. package/lib/agent.ts +68 -27
  49. package/lib/constants.ts +17 -1
  50. package/lib/errors/agent-aborted.error.ts +22 -0
  51. package/lib/errors/agent-recursion-limit.error.ts +24 -0
  52. package/lib/errors/index.ts +3 -0
  53. package/lib/errors/messages.ts +12 -0
  54. package/lib/errors/run-error.util.ts +57 -0
  55. package/lib/index.ts +3 -0
  56. package/lib/interfaces/langchain-module-options.interface.ts +6 -0
  57. package/lib/interfaces/run-options.interface.ts +13 -0
  58. package/lib/run/index.ts +1 -0
  59. package/lib/run/recursion-limit.util.ts +11 -0
  60. package/package.json +4 -4
@@ -64,6 +64,12 @@ export interface LangChainModuleOptions {
64
64
  observer?: AgentObserver;
65
65
  /** LangChain-native run callbacks, for handlers written against its API. */
66
66
  callbacks?: AgentCallback[];
67
+ /**
68
+ * The most steps a run may take, each model call and each round of tool
69
+ * calls counting as one: LangGraph's 25 when omitted. A run's own
70
+ * `recursionLimit` overrides it.
71
+ */
72
+ recursionLimit?: number;
67
73
  }
68
74
  /**
69
75
  * A class building an agent's options, for `registerAsync({ useClass })` or
@@ -1 +1 @@
1
- {"version":3,"file":"langchain-module-options.interface.d.ts","sourceRoot":"","sources":["../../lib/interfaces/langchain-module-options.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,mBAAmB,EACnB,sBAAsB,EACvB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,UAAU,aAAa;IACrB,uBAAuB;IACvB,MAAM,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,8DAA8D;IAC9D,SAAS,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IACtC,kCAAkC;IAClC,qBAAqB,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;CAClD;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,aAAa,CAAC;AAEtD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,mBAAmB,GAAG,sBAAsB,CAAC;AAEzE,mEAAmE;AACnE,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,KAAK,EAAE,WAAW,CAAC;IACnB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,sBAAsB,IACpB,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC5D"}
1
+ {"version":3,"file":"langchain-module-options.interface.d.ts","sourceRoot":"","sources":["../../lib/interfaces/langchain-module-options.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EACV,mBAAmB,EACnB,sBAAsB,EACvB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,UAAU,aAAa;IACrB,uBAAuB;IACvB,MAAM,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,8DAA8D;IAC9D,SAAS,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IACtC,kCAAkC;IAClC,qBAAqB,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;CAClD;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,aAAa,CAAC;AAEtD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAExE;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,mBAAmB,GAAG,sBAAsB,CAAC;AAEzE,mEAAmE;AACnE,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,KAAK,EAAE,WAAW,CAAC;IACnB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,sBAAsB,IACpB,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC5D"}
@@ -6,6 +6,7 @@ import type { ZodType } from 'zod';
6
6
  * @example
7
7
  * ```ts
8
8
  * const { output } = await agent.run(email, { output: Ticket }); // a Ticket
9
+ * await agent.run(question, { signal: AbortSignal.timeout(30_000) });
9
10
  *
10
11
  * const ask = (question: string, options?: RunOptions) =>
11
12
  * agent.run(question, options); // `output` is unknown
@@ -17,5 +18,17 @@ export interface RunOptions {
17
18
  * typed from it, instead of the model's text.
18
19
  */
19
20
  output?: ZodType;
21
+ /**
22
+ * Aborts the run: `run()` throws an `AgentAbortedError`, the signal's
23
+ * `reason` in `cause`. A tool already running ends in the background, its
24
+ * result dropped.
25
+ */
26
+ signal?: AbortSignal;
27
+ /**
28
+ * The most steps the run may take, each model call and each round of tool
29
+ * calls counting as one, instead of the agent's `recursionLimit`. Past it,
30
+ * `run()` throws an `AgentRecursionLimitError`.
31
+ */
32
+ recursionLimit?: number;
20
33
  }
21
34
  //# sourceMappingURL=run-options.interface.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-options.interface.d.ts","sourceRoot":"","sources":["../../lib/interfaces/run-options.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
1
+ {"version":3,"file":"run-options.interface.d.ts","sourceRoot":"","sources":["../../lib/interfaces/run-options.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB"}
@@ -16,7 +16,7 @@ export declare class RunOutput {
16
16
  */
17
17
  readonly strategy: TypedToolStrategy;
18
18
  /** The tool the model answers through. */
19
- readonly toolName = "extract-output";
19
+ readonly toolName: "extract-output";
20
20
  private readonly prefix;
21
21
  /**
22
22
  * @param schema The run's `output` schema.
@@ -1 +1 @@
1
- {"version":3,"file":"run-output.d.ts","sourceRoot":"","sources":["../../lib/output/run-output.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,iBAAiB,EAClB,MAAM,WAAW,CAAC;AACnB,OAAU,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAqBjC;;;;;GAKG;AACH,qBAAa,SAAS;;IAkBlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAlBxB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,oBAAe;IAEhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;;;;OAKG;gBAEgB,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM;IAqChC,OAAO,CAAC,KAAK;IAUb;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAiBhC;;;;OAIG;IACH,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,OAAO;IAmB1C,OAAO,CAAC,OAAO;CAOhB"}
1
+ {"version":3,"file":"run-output.d.ts","sourceRoot":"","sources":["../../lib/output/run-output.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,iBAAiB,EAClB,MAAM,WAAW,CAAC;AACnB,OAAU,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAqBjC;;;;;GAKG;AACH,qBAAa,SAAS;;IAkBlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IAlBxB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,mBAAe;IAEhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;;;;OAKG;gBAEgB,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM;IAqChC,OAAO,CAAC,KAAK;IAUb;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAiBhC;;;;OAIG;IACH,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,OAAO;IAmB1C,OAAO,CAAC,OAAO;CAOhB"}
@@ -1,4 +1,5 @@
1
1
  export { answeredCalls } from './answered-calls.util.js';
2
+ export { isRecursionLimit } from './recursion-limit.util.js';
2
3
  export { summariseRun } from './run-summary.util.js';
3
4
  export { UsageAside } from './usage-aside.js';
4
5
  export type { RunSummary } from './run-summary.util.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/run/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { answeredCalls } from './answered-calls.util.js';
2
+ export { isRecursionLimit } from './recursion-limit.util.js';
2
3
  export { summariseRun } from './run-summary.util.js';
3
4
  export { UsageAside } from './usage-aside.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/run/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Whether a `recursionLimit` is one LangGraph applies: it rejects a limit
3
+ * below 1, but runs on with `NaN`. See #194.
4
+ *
5
+ * @internal
6
+ * @param limit The option as given, from untyped JavaScript too.
7
+ * @returns `true` for an integer of at least 1, or no limit.
8
+ */
9
+ export declare const isRecursionLimit: (limit: unknown) => boolean;
10
+ //# sourceMappingURL=recursion-limit.util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recursion-limit.util.d.ts","sourceRoot":"","sources":["../../lib/run/recursion-limit.util.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,OAAO,KAAG,OAEoB,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whether a `recursionLimit` is one LangGraph applies: it rejects a limit
3
+ * below 1, but runs on with `NaN`. See #194.
4
+ *
5
+ * @internal
6
+ * @param limit The option as given, from untyped JavaScript too.
7
+ * @returns `true` for an integer of at least 1, or no limit.
8
+ */
9
+ export const isRecursionLimit = (limit) => limit === undefined ||
10
+ (typeof limit === 'number' && Number.isInteger(limit) && limit >= 1);
11
+ //# sourceMappingURL=recursion-limit.util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recursion-limit.util.js","sourceRoot":"","sources":["../../lib/run/recursion-limit.util.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAc,EAAW,EAAE,CAC1D,KAAK,KAAK,SAAS;IACnB,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC"}
package/lib/agent.ts CHANGED
@@ -20,14 +20,15 @@ import {
20
20
  } from './tools/index.js';
21
21
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
22
22
  import { LOG_CONTEXT, messageOf, RunReporter } from './logging/index.js';
23
- import { summariseRun, UsageAside } from './run/index.js';
23
+ import { DEFAULT_RECURSION_LIMIT } from './constants.js';
24
+ import { isRecursionLimit, summariseRun, UsageAside } from './run/index.js';
24
25
  import { outputConfig, RunOutput, structuredOutput } from './output/index.js';
25
- import { AgentRunError, unwrapMiddlewareError } from './errors/index.js';
26
+ import { AgentRunError, toRunError } from './errors/index.js';
26
27
  import {
27
28
  agentNotBootstrapped,
28
29
  agentPrefix,
29
- agentRunFailed,
30
30
  duplicateAgentName,
31
+ invalidRecursionLimit,
31
32
  modelNeverReplied,
32
33
  modelNotBuilt,
33
34
  modelReplyEmpty,
@@ -62,9 +63,6 @@ export class Agent implements OnModuleInit {
62
63
  private readonly logger = new Logger();
63
64
  private readonly prefix: string;
64
65
  private readonly reporter: RunReporter;
65
- // Model failures, which only LangChain's callbacks see. Tool calls are
66
- // reported by their wrapper and the tool middlewares.
67
- private readonly modelErrors: BaseCallbackHandler;
68
66
 
69
67
  /** @internal Built by `LangChainModule`, never by hand. */
70
68
  constructor(
@@ -82,14 +80,6 @@ export class Agent implements OnModuleInit {
82
80
  prefix: this.prefix,
83
81
  observer: options.observer,
84
82
  });
85
- this.modelErrors = BaseCallbackHandler.fromMethods({
86
- handleLLMError: (error: unknown) => {
87
- this.reporter.logError(`the model failed: ${messageOf(error)}`);
88
- this.reporter.notify((observer) =>
89
- observer.onModelError?.({ agent: agentName, error }),
90
- );
91
- },
92
- });
93
83
  }
94
84
 
95
85
  /**
@@ -100,6 +90,12 @@ export class Agent implements OnModuleInit {
100
90
  async onModuleInit() {
101
91
  this.assertUniqueName();
102
92
 
93
+ const { recursionLimit } = this.options;
94
+
95
+ if (!isRecursionLimit(recursionLimit)) {
96
+ throw new Error(invalidRecursionLimit(this.prefix, recursionLimit));
97
+ }
98
+
103
99
  const tools = await this.toolDiscovery.getToolsFromModules(
104
100
  this.options.tools ?? [],
105
101
  this.reporter,
@@ -119,6 +115,11 @@ export class Agent implements OnModuleInit {
119
115
  toolErrors(this.reporter),
120
116
  structuredOutput(),
121
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 },
122
123
  });
123
124
 
124
125
  this.logger.log(
@@ -190,12 +191,15 @@ export class Agent implements OnModuleInit {
190
191
  * as an `AgentRunError`, the original error in its `cause`.
191
192
  *
192
193
  * @param input The user's message.
193
- * @param options With `output`, the schema the answer must match.
194
+ * @param options With `output`, the schema the answer must match; with
195
+ * `signal`, what aborts the run; with `recursionLimit`, its most steps.
194
196
  * @returns The model's answer, the object `output` parses when given, the
195
197
  * tools it called and what the run cost.
196
198
  * @example
197
199
  * ```ts
198
- * 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
+ * });
199
203
  *
200
204
  * const { output: ticket } = await agent.run(email, {
201
205
  * output: z.object({ category: z.enum(['billing', 'bug', 'other']) }),
@@ -218,14 +222,31 @@ export class Agent implements OnModuleInit {
218
222
  ): Promise<CompletedRun<unknown>> {
219
223
  const startedAt = Date.now();
220
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;
221
231
 
222
232
  try {
223
233
  // Read in the `try`, and `null` from untyped JavaScript taken as no
224
234
  // options, as LangChain takes a `null` config. See #291.
225
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
+
226
244
  const output = schema && new RunOutput(schema, this.agentName);
227
245
  const aside = new UsageAside();
228
- const { answer, messages } = await this.complete(input, output, aside);
246
+ const { answer, messages } = await this.complete(input, output, aside, {
247
+ signal: options?.signal,
248
+ recursionLimit,
249
+ });
229
250
  const finished = {
230
251
  durationMs: durationMs(),
231
252
  ...summariseRun(messages, output?.toolName, aside.usage),
@@ -237,15 +258,12 @@ export class Agent implements OnModuleInit {
237
258
 
238
259
  return { status: 'completed', output: answer, ...finished };
239
260
  } catch (thrown) {
240
- const cause = unwrapMiddlewareError(thrown);
241
- const error =
242
- cause instanceof AgentRunError
243
- ? cause
244
- : new AgentRunError(
245
- agentRunFailed(this.prefix, cause),
246
- this.agentName,
247
- { cause },
248
- );
261
+ const error = toRunError(thrown, {
262
+ agent: this.agentName,
263
+ prefix: this.prefix,
264
+ signal: options?.signal,
265
+ recursionLimit,
266
+ });
249
267
 
250
268
  this.reporter.notify((observer) =>
251
269
  observer.onRunError?.({
@@ -259,10 +277,29 @@ export class Agent implements OnModuleInit {
259
277
  }
260
278
  }
261
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
+
262
298
  private async complete(
263
299
  input: string,
264
300
  output: RunOutput | undefined,
265
301
  aside: UsageAside,
302
+ run: Pick<RunOptions, 'signal' | 'recursionLimit'>,
266
303
  ): Promise<{ answer: unknown; messages: AgentState['messages'] }> {
267
304
  if (!this.#graph) {
268
305
  throw new AgentRunError(
@@ -274,8 +311,12 @@ export class Agent implements OnModuleInit {
274
311
  const state: AgentState = await this.#graph.invoke(
275
312
  { messages: [{ role: 'user', content: input }] },
276
313
  {
277
- callbacks: [this.modelErrors, ...(this.options.callbacks ?? [])],
314
+ callbacks: [
315
+ this.modelErrors(run.signal),
316
+ ...(this.options.callbacks ?? []),
317
+ ],
278
318
  configurable: { ...outputConfig(output), ...aside.config },
319
+ ...run,
279
320
  },
280
321
  );
281
322
  const { messages } = state;
package/lib/constants.ts CHANGED
@@ -11,7 +11,23 @@ export const STRUCTURED_OUTPUT_PREFIX = 'extract-';
11
11
 
12
12
  // Ours rather than LangChain's counter or the schema's title: the same tool on
13
13
  // every run, under the prefix no `@Tool` can take. See #281.
14
- export const OUTPUT_TOOL = `${STRUCTURED_OUTPUT_PREFIX}output`;
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;
15
31
 
16
32
  // The keys `@Module()` writes, restated: `@nestjs/common/constants` is
17
33
  // internal. See #97.
@@ -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,4 +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';
2
5
  export { StructuredOutputError } from './structured-output.error.js';
3
6
  export { ToolConfigurationError } from './tool-configuration.error.js';
4
7
  export { unwrapMiddlewareError } from './unwrap-middleware-error.util.js';
@@ -158,6 +158,18 @@ export const modelNeverReplied = (
158
158
  export const modelReplyEmpty = (prefix: string): string =>
159
159
  `The ${prefix}agent's model replied with no text content.`;
160
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
+
161
173
  export const noOutputJsonSchemaForm = (
162
174
  prefix: string,
163
175
  reason: string,
@@ -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
+ };
package/lib/index.ts CHANGED
@@ -3,7 +3,10 @@
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 { OUTPUT_TOOL } from './constants.js';
6
7
  export {
8
+ AgentAbortedError,
9
+ AgentRecursionLimitError,
7
10
  AgentRunError,
8
11
  StructuredOutputError,
9
12
  ToolConfigurationError,
@@ -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
  /**
@@ -7,6 +7,7 @@ import type { ZodType } from 'zod';
7
7
  * @example
8
8
  * ```ts
9
9
  * const { output } = await agent.run(email, { output: Ticket }); // a Ticket
10
+ * await agent.run(question, { signal: AbortSignal.timeout(30_000) });
10
11
  *
11
12
  * const ask = (question: string, options?: RunOptions) =>
12
13
  * agent.run(question, options); // `output` is unknown
@@ -18,4 +19,16 @@ export interface RunOptions {
18
19
  * typed from it, instead of the model's text.
19
20
  */
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;
21
34
  }
package/lib/run/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { answeredCalls } from './answered-calls.util.js';
2
+ export { isRecursionLimit } from './recursion-limit.util.js';
2
3
  export { summariseRun } from './run-summary.util.js';
3
4
  export { UsageAside } from './usage-aside.js';
4
5
  export type { RunSummary } from './run-summary.util.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whether a `recursionLimit` is one LangGraph applies: it rejects a limit
3
+ * below 1, but runs on with `NaN`. See #194.
4
+ *
5
+ * @internal
6
+ * @param limit The option as given, from untyped JavaScript too.
7
+ * @returns `true` for an integer of at least 1, or no limit.
8
+ */
9
+ export const isRecursionLimit = (limit: unknown): boolean =>
10
+ limit === undefined ||
11
+ (typeof limit === 'number' && Number.isInteger(limit) && limit >= 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nestjs-langchain",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "description": "AI agents for NestJS, powered by LangChain: declare agents as modules, give them your services as tools.",
6
6
  "keywords": [
@@ -70,7 +70,7 @@
70
70
  "format": "prettier --write \"{lib,tests,scripts}/**/*.{ts,mjs}\" vitest.config.ts",
71
71
  "lint": "oxlint --type-aware lib/ tests/ scripts/ vitest.config.ts --fix",
72
72
  "lint:check": "oxlint --type-aware lib/ tests/ scripts/ vitest.config.ts",
73
- "test": "vitest run --typecheck",
73
+ "test": "vitest run && tsc --noEmit",
74
74
  "test:watch": "vitest",
75
75
  "release": "release-it",
76
76
  "release:rc": "release-it --preRelease=rc --no-plugins.@release-it/conventional-changelog.infile",
@@ -92,7 +92,7 @@
92
92
  "@nestjs/testing": "^12.0.1",
93
93
  "@release-it/conventional-changelog": "12.0.2",
94
94
  "@types/node": "^26.6.4",
95
- "@vitest/coverage-v8": "^4.1.11",
95
+ "@vitest/coverage-v8": "^5.0.5",
96
96
  "eslint-plugin-jsdoc": "^65.2.0",
97
97
  "husky": "9.1.7",
98
98
  "langchain": "^1.5.4",
@@ -107,7 +107,7 @@
107
107
  "rxjs": "^7.8.2",
108
108
  "semver": "^7.8.5",
109
109
  "typescript": "^6.0.3",
110
- "vitest": "^4.1.11",
110
+ "vitest": "^5.0.5",
111
111
  "zod": "^4.0.0"
112
112
  },
113
113
  "peerDependencies": {