@librechat/agents 3.2.58 → 3.2.60

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 (67) hide show
  1. package/dist/cjs/graphs/Graph.cjs +31 -7
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +7 -0
  4. package/dist/cjs/run.cjs +4 -0
  5. package/dist/cjs/run.cjs.map +1 -1
  6. package/dist/cjs/stream.cjs +2 -1
  7. package/dist/cjs/stream.cjs.map +1 -1
  8. package/dist/cjs/tools/BashExecutor.cjs +58 -9
  9. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  10. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +4 -2
  11. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  12. package/dist/cjs/tools/CodeExecutor.cjs +57 -7
  13. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  14. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -3
  15. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  16. package/dist/cjs/tools/ToolNode.cjs +114 -11
  17. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  18. package/dist/cjs/tools/eagerEventExecution.cjs +18 -1
  19. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
  20. package/dist/esm/graphs/Graph.mjs +31 -7
  21. package/dist/esm/graphs/Graph.mjs.map +1 -1
  22. package/dist/esm/main.mjs +3 -3
  23. package/dist/esm/run.mjs +4 -0
  24. package/dist/esm/run.mjs.map +1 -1
  25. package/dist/esm/stream.mjs +2 -1
  26. package/dist/esm/stream.mjs.map +1 -1
  27. package/dist/esm/tools/BashExecutor.mjs +56 -10
  28. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  29. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +4 -2
  30. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  31. package/dist/esm/tools/CodeExecutor.mjs +54 -8
  32. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  33. package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -3
  34. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  35. package/dist/esm/tools/ToolNode.mjs +115 -12
  36. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  37. package/dist/esm/tools/eagerEventExecution.mjs +18 -2
  38. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
  39. package/dist/types/graphs/Graph.d.ts +21 -3
  40. package/dist/types/run.d.ts +1 -0
  41. package/dist/types/tools/BashExecutor.d.ts +13 -0
  42. package/dist/types/tools/CodeExecutor.d.ts +14 -0
  43. package/dist/types/tools/ToolNode.d.ts +60 -1
  44. package/dist/types/tools/eagerEventExecution.d.ts +8 -0
  45. package/dist/types/types/hitl.d.ts +49 -3
  46. package/dist/types/types/run.d.ts +21 -0
  47. package/dist/types/types/tools.d.ts +95 -1
  48. package/package.json +1 -1
  49. package/src/graphs/Graph.ts +74 -28
  50. package/src/run.ts +4 -0
  51. package/src/specs/ask-user-question-batch.test.ts +289 -0
  52. package/src/specs/tool-error-resume.test.ts +194 -0
  53. package/src/stream.ts +17 -1
  54. package/src/tools/BashExecutor.ts +107 -14
  55. package/src/tools/BashProgrammaticToolCalling.ts +20 -1
  56. package/src/tools/CodeExecutor.ts +113 -9
  57. package/src/tools/ProgrammaticToolCalling.ts +27 -1
  58. package/src/tools/ToolNode.ts +208 -31
  59. package/src/tools/__tests__/BashExecutor.test.ts +39 -0
  60. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +113 -0
  61. package/src/tools/__tests__/ToolNode.session.test.ts +86 -0
  62. package/src/tools/__tests__/eagerEventExecution.session.test.ts +92 -0
  63. package/src/tools/__tests__/hitl.test.ts +48 -0
  64. package/src/tools/eagerEventExecution.ts +32 -5
  65. package/src/types/hitl.ts +49 -3
  66. package/src/types/run.ts +21 -0
  67. package/src/types/tools.ts +102 -1
@@ -4,7 +4,7 @@ import "../common/index.mjs";
4
4
  import { toLangChainContent } from "../messages/langchain.mjs";
5
5
  import { calculateMaxToolResultChars, truncateToolResultContent } from "../utils/truncation.mjs";
6
6
  import { safeDispatchCustomEvent } from "../utils/events.mjs";
7
- import { buildToolExecutionRequestPlan, recordArgsEqual } from "./eagerEventExecution.mjs";
7
+ import { buildToolExecutionRequestPlan, recordArgsEqual, resolveRuntimeSessionHint } from "./eagerEventExecution.mjs";
8
8
  import { ToolOutputReferenceRegistry, buildReferenceKey } from "./toolOutputReferences.mjs";
9
9
  import { RunnableCallable } from "../utils/run.mjs";
10
10
  import "../utils/index.mjs";
@@ -207,6 +207,13 @@ var ToolNode = class extends RunnableCallable {
207
207
  agentLangfuse;
208
208
  toolCallStepIds;
209
209
  errorHandler;
210
+ /**
211
+ * Tool call ids whose `errorHandler` did NOT dispatch the error completion
212
+ * event (it returned `false` or threw). The output loop must dispatch the
213
+ * completion for these itself — skipping them there would strand the
214
+ * client's tool-call part without a terminal event.
215
+ */
216
+ undispatchedToolErrors = /* @__PURE__ */ new Set();
210
217
  toolUsageCount;
211
218
  /** Maps toolCallId → turn captured in runTool, used by handleRunToolCompletions */
212
219
  toolCallTurns = /* @__PURE__ */ new Map();
@@ -249,6 +256,21 @@ var ToolNode = class extends RunnableCallable {
249
256
  /** Tool names that bypass event dispatch and execute directly (e.g., graph-managed handoff tools) */
250
257
  directToolNames;
251
258
  /**
259
+ * Tool names whose in-process body may raise a LangGraph `interrupt()`
260
+ * mid-execution (e.g. `ask_user_question`). Used only to REORDER within
261
+ * the direct group: a tool named here that is *already* direct (a real
262
+ * in-process graphTool — the only kind whose body can reach
263
+ * `interrupt()`) is scheduled ahead of its non-interrupting direct
264
+ * siblings, so a mid-body interrupt unwinds the ToolNode before a
265
+ * non-idempotent sibling executes and LangGraph's resume-time batch
266
+ * re-execution can't double it. This set is deliberately NOT folded
267
+ * into direct classification: a name that resolves to a schema-only
268
+ * event stub (an inherited `toolDefinition` with no executable
269
+ * instance) stays event-dispatched — forcing it direct would invoke
270
+ * the stub, which throws. See {@link t.ToolNodeOptions.interruptingToolNames}.
271
+ */
272
+ interruptingToolNames;
273
+ /**
252
274
  * File checkpointer extracted from the local coding tool bundle when
253
275
  * `toolExecution.local.fileCheckpointing === true`. Exposed via
254
276
  * {@link getFileCheckpointer}. Undefined when checkpointing is off
@@ -287,7 +309,7 @@ var ToolNode = class extends RunnableCallable {
287
309
  * other's in-flight state.
288
310
  */
289
311
  anonBatchCounter = 0;
290
- constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer }) {
312
+ constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, interruptingToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer }) {
291
313
  super({
292
314
  name: name ?? TOOL_NODE_RUN_NAME,
293
315
  tags,
@@ -314,6 +336,7 @@ var ToolNode = class extends RunnableCallable {
314
336
  this.agentId = agentId;
315
337
  this.executingAgentId = executingAgentId ?? agentId;
316
338
  this.directToolNames = directToolNames;
339
+ this.interruptingToolNames = interruptingToolNames != null && interruptingToolNames.size > 0 ? interruptingToolNames : void 0;
317
340
  this.codeSessionToolNames = codeSessionToolNames != null && codeSessionToolNames.length > 0 ? new Set(codeSessionToolNames) : void 0;
318
341
  this.maxToolResultChars = maxToolResultChars ?? calculateMaxToolResultChars(maxContextTokens);
319
342
  this.hookRegistry = hookRegistry;
@@ -580,6 +603,17 @@ var ToolNode = class extends RunnableCallable {
580
603
  };
581
604
  if (codeSession?.files != null && codeSession.files.length > 0) invokeParams._injected_files = codeSession.files.map((file) => toInjectedFileRef(file, execSessionId));
582
605
  }
606
+ /**
607
+ * Stateful runtime session hint — orthogonal to the transient
608
+ * exec-session above, and injected independently (a first call has a
609
+ * hint but no exec session yet). Explicit host hint wins; otherwise
610
+ * fall back to the conversation's thread_id.
611
+ */
612
+ const runtimeSessionHint = this.resolveRuntimeSessionHint(config);
613
+ if (runtimeSessionHint != null) invokeParams = {
614
+ ...invokeParams,
615
+ _runtime_session_hint: runtimeSessionHint
616
+ };
583
617
  }
584
618
  /**
585
619
  * Forward the graph state as langgraph 1.4's `runtime.state` so
@@ -659,12 +693,20 @@ var ToolNode = class extends RunnableCallable {
659
693
  if (!this.handleToolErrors) throw e;
660
694
  if (isGraphInterrupt(e)) throw e;
661
695
  if (this.errorHandler) try {
662
- await this.errorHandler({
696
+ if (await this.errorHandler({
663
697
  error: e,
664
698
  id: call.id,
665
699
  name: call.name,
666
700
  input: call.args
667
- }, config.metadata);
701
+ }, config.metadata) === false && call.id != null && call.id !== "")
702
+ /**
703
+ * The handler could not dispatch the error completion (typically
704
+ * a resume pass, where a fast-failing tool errors before the
705
+ * step replay registers its run step). Remember the call so the
706
+ * output loop dispatches the completion itself instead of
707
+ * assuming the handler covered it.
708
+ */
709
+ this.undispatchedToolErrors.add(call.id);
668
710
  } catch (handlerError) {
669
711
  console.error("Error in errorHandler:", {
670
712
  toolName: call.name,
@@ -1060,6 +1102,11 @@ var ToolNode = class extends RunnableCallable {
1060
1102
  if (name === "") return false;
1061
1103
  return CODE_EXECUTION_TOOLS.has(name) || this.codeSessionToolNames?.has(name) === true;
1062
1104
  }
1105
+ /** Delegates to the shared resolver so the direct and event-driven planning
1106
+ * paths derive the runtime session hint identically. */
1107
+ resolveRuntimeSessionHint(config) {
1108
+ return resolveRuntimeSessionHint(this.toolExecution, config.configurable?.thread_id);
1109
+ }
1063
1110
  storeCodeSessionFromResults(results, requestMap) {
1064
1111
  if (!this.sessions) return;
1065
1112
  for (let i = 0; i < results.length; i++) {
@@ -1094,7 +1141,8 @@ var ToolNode = class extends RunnableCallable {
1094
1141
  if (isCommand(output)) continue;
1095
1142
  const toolMessage = output;
1096
1143
  const toolCallId = call.id ?? "";
1097
- if (toolMessage.status === "error" && this.errorHandler != null) continue;
1144
+ if (toolMessage.status === "error" && this.errorHandler != null) if (this.undispatchedToolErrors.has(toolCallId)) this.undispatchedToolErrors.delete(toolCallId);
1145
+ else continue;
1098
1146
  if (this.sessions && this.participatesInCodeSession(call.name)) {
1099
1147
  const artifact = toolMessage.artifact;
1100
1148
  const execSessionId = artifact?.session_id;
@@ -1546,12 +1594,14 @@ var ToolNode = class extends RunnableCallable {
1546
1594
  const plan = buildToolExecutionRequestPlan({
1547
1595
  toolCalls: approvedEntries.map((entry) => {
1548
1596
  const codeSessionContext = this.participatesInCodeSession(entry.call.name) || entry.call.name === "skill" || entry.call.name === "read_file" ? this.getCodeSessionContext() : void 0;
1597
+ const runtimeSessionHint = this.participatesInCodeSession(entry.call.name) ? this.resolveRuntimeSessionHint(config) : void 0;
1549
1598
  return {
1550
1599
  id: entry.call.id,
1551
1600
  name: entry.call.name,
1552
1601
  args: entry.args,
1553
1602
  stepId: entry.stepId,
1554
- codeSessionContext
1603
+ codeSessionContext,
1604
+ runtimeSessionHint
1555
1605
  };
1556
1606
  }),
1557
1607
  usageCount: this.toolUsageCount,
@@ -1921,6 +1971,61 @@ var ToolNode = class extends RunnableCallable {
1921
1971
  return converted;
1922
1972
  }
1923
1973
  /**
1974
+ * Execute a group of direct (in-process) tool calls with interrupt-safe
1975
+ * ordering, returning outputs aligned 1:1 with `directCalls`.
1976
+ *
1977
+ * Fast path (the common case): when no call in the group is in
1978
+ * `interruptingToolNames`, this is a single `Promise.all` — byte-for-byte
1979
+ * the prior behavior, so ordinary batches are unaffected.
1980
+ *
1981
+ * Interrupt-safe path: when the group contains an interrupting tool (e.g.
1982
+ * `ask_user_question`, whose body raises a LangGraph `interrupt()` to
1983
+ * collect a human answer), the interrupting calls run as their own awaited
1984
+ * group **first**; only after they all settle without interrupting do the
1985
+ * remaining (potentially non-idempotent) siblings run. If an interrupting
1986
+ * call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
1987
+ * whole ToolNode *before* any non-interrupting sibling has started — so a
1988
+ * sibling with real side effects (send_email, billing) never executes on
1989
+ * the first pass. On the resume pass LangGraph re-runs the batch from the
1990
+ * top; the interrupting tool resolves with the host's answer instead of
1991
+ * throwing, and the siblings execute for the FIRST time, exactly once.
1992
+ *
1993
+ * Without this ordering, a flat `Promise.all` starts every sibling
1994
+ * concurrently, so a non-idempotent sibling can complete its side effect
1995
+ * before the interrupt unwinds and then run a SECOND time on resume — the
1996
+ * duplicate side effect this method exists to prevent. Interrupting tools
1997
+ * are expected to be side-effect-free (they only suspend), so running them
1998
+ * as a group and re-running them on resume is harmless.
1999
+ *
2000
+ * `batchIndices[i]` is `directCalls[i]`'s position within the parent
2001
+ * ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is
2002
+ * preserved regardless of execution order. `baseContext` carries the
2003
+ * batch-scoped fields every call shares; `batchIndex` is filled in
2004
+ * per-call here.
2005
+ */
2006
+ async runDirectBatchInterruptSafe(directCalls, batchIndices, config, baseContext) {
2007
+ const runOne = (call, position) => this.runDirectToolWithLifecycleHooks(call, config, {
2008
+ ...baseContext,
2009
+ batchIndex: batchIndices[position]
2010
+ });
2011
+ const interrupting = this.interruptingToolNames;
2012
+ if (!(interrupting != null && directCalls.some((call) => interrupting.has(call.name)))) return Promise.all(directCalls.map((call, i) => runOne(call, i)));
2013
+ const outputs = new Array(directCalls.length);
2014
+ const interruptingPositions = [];
2015
+ const regularPositions = [];
2016
+ for (let i = 0; i < directCalls.length; i++) if (interrupting.has(directCalls[i].name)) interruptingPositions.push(i);
2017
+ else regularPositions.push(i);
2018
+ const interruptingOutputs = await Promise.all(interruptingPositions.map((i) => runOne(directCalls[i], i)));
2019
+ interruptingPositions.forEach((i, k) => {
2020
+ outputs[i] = interruptingOutputs[k];
2021
+ });
2022
+ const regularOutputs = await Promise.all(regularPositions.map((i) => runOne(directCalls[i], i)));
2023
+ regularPositions.forEach((i, k) => {
2024
+ outputs[i] = regularOutputs[k];
2025
+ });
2026
+ return outputs;
2027
+ }
2028
+ /**
1924
2029
  * Execute all tool calls via ON_TOOL_EXECUTE event dispatch.
1925
2030
  * Injected messages are placed AFTER ToolMessages to respect provider
1926
2031
  * message ordering (AIMessage tool_calls must be immediately followed
@@ -2057,15 +2162,14 @@ var ToolNode = class extends RunnableCallable {
2057
2162
  }
2058
2163
  }
2059
2164
  const directAdditionalContexts = [];
2060
- const directOutputs = directCalls.length > 0 ? await Promise.all(directCalls.map((call, i) => this.runDirectToolWithLifecycleHooks(call, config, {
2061
- batchIndex: directIndices[i],
2165
+ const directOutputs = directCalls.length > 0 ? await this.runDirectBatchInterruptSafe(directCalls, directIndices, config, {
2062
2166
  turn,
2063
2167
  batchScopeId,
2064
2168
  resolvedArgsByCallId,
2065
2169
  preBatchSnapshot,
2066
2170
  additionalContextsSink: directAdditionalContexts,
2067
2171
  runInput: input
2068
- }))) : [];
2172
+ }) : [];
2069
2173
  if (directCalls.length > 0 && directOutputs.length > 0) await this.handleRunToolCompletions(directCalls, directOutputs, config, resolvedArgsByCallId);
2070
2174
  const eventResult = eventCalls.length > 0 ? await this.dispatchToolEvents(eventCalls, config, {
2071
2175
  batchIndices: eventIndices,
@@ -2093,15 +2197,14 @@ var ToolNode = class extends RunnableCallable {
2093
2197
  } else {
2094
2198
  const preBatchSnapshot = this.toolOutputRegistry?.snapshot(batchScopeId);
2095
2199
  const directAdditionalContexts = [];
2096
- const toolOutputs = await Promise.all(filteredCalls.map((call, i) => this.runDirectToolWithLifecycleHooks(call, config, {
2097
- batchIndex: i,
2200
+ const toolOutputs = await this.runDirectBatchInterruptSafe(filteredCalls, filteredCalls.map((_call, i) => i), config, {
2098
2201
  turn,
2099
2202
  batchScopeId,
2100
2203
  resolvedArgsByCallId,
2101
2204
  preBatchSnapshot,
2102
2205
  additionalContextsSink: directAdditionalContexts,
2103
2206
  runInput: input
2104
- })));
2207
+ });
2105
2208
  await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId);
2106
2209
  outputs = directAdditionalContexts.length > 0 ? [...toolOutputs, new HumanMessage({
2107
2210
  content: directAdditionalContexts.join("\n\n"),