@librechat/agents 3.3.8 → 3.3.9

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 (148) hide show
  1. package/dist/cjs/graphs/Graph.cjs +47 -13
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +35 -2
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
  6. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  7. package/dist/cjs/instrumentation.cjs +18 -48
  8. package/dist/cjs/instrumentation.cjs.map +1 -1
  9. package/dist/cjs/langfuse.cjs +174 -29
  10. package/dist/cjs/langfuse.cjs.map +1 -1
  11. package/dist/cjs/langfuseConfig.cjs +12 -0
  12. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  13. package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
  14. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  15. package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
  16. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  17. package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
  18. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
  19. package/dist/cjs/langfuseTraceShaping.cjs +121 -4
  20. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  21. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
  22. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  23. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
  24. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  25. package/dist/cjs/llm/init.cjs +3 -3
  26. package/dist/cjs/llm/invoke.cjs +5 -5
  27. package/dist/cjs/llm/openai/index.cjs +1 -1
  28. package/dist/cjs/main.cjs +10 -10
  29. package/dist/cjs/messages/prune.cjs +13 -1
  30. package/dist/cjs/messages/prune.cjs.map +1 -1
  31. package/dist/cjs/prompts/activityLabel.cjs +24 -12
  32. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  33. package/dist/cjs/run.cjs +47 -22
  34. package/dist/cjs/run.cjs.map +1 -1
  35. package/dist/cjs/session/messageSerialization.cjs +6 -0
  36. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  37. package/dist/cjs/stream.cjs +21 -10
  38. package/dist/cjs/stream.cjs.map +1 -1
  39. package/dist/cjs/summarization/node.cjs +5 -0
  40. package/dist/cjs/summarization/node.cjs.map +1 -1
  41. package/dist/cjs/tools/ToolNode.cjs +253 -24
  42. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  43. package/dist/cjs/tools/handlers.cjs +1 -1
  44. package/dist/cjs/tools/search/tool.cjs +1 -1
  45. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  46. package/dist/cjs/utils/index.cjs +2 -2
  47. package/dist/esm/graphs/Graph.mjs +48 -14
  48. package/dist/esm/graphs/Graph.mjs.map +1 -1
  49. package/dist/esm/graphs/MultiAgentGraph.mjs +35 -2
  50. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  51. package/dist/esm/hitl/askUserQuestion.mjs +3 -2
  52. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  53. package/dist/esm/instrumentation.mjs +18 -48
  54. package/dist/esm/instrumentation.mjs.map +1 -1
  55. package/dist/esm/langfuse.mjs +176 -28
  56. package/dist/esm/langfuse.mjs.map +1 -1
  57. package/dist/esm/langfuseConfig.mjs +10 -1
  58. package/dist/esm/langfuseConfig.mjs.map +1 -1
  59. package/dist/esm/langfuseRuntimeContext.mjs +21 -3
  60. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  61. package/dist/esm/langfuseRuntimeScope.mjs +39 -10
  62. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  63. package/dist/esm/langfuseSpanRegistry.mjs +91 -0
  64. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
  65. package/dist/esm/langfuseTraceShaping.mjs +121 -4
  66. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  67. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
  68. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  69. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
  70. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  71. package/dist/esm/llm/init.mjs +2 -2
  72. package/dist/esm/llm/invoke.mjs +5 -5
  73. package/dist/esm/llm/openai/index.mjs +1 -1
  74. package/dist/esm/main.mjs +8 -8
  75. package/dist/esm/messages/prune.mjs +13 -1
  76. package/dist/esm/messages/prune.mjs.map +1 -1
  77. package/dist/esm/prompts/activityLabel.mjs +24 -12
  78. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  79. package/dist/esm/run.mjs +47 -22
  80. package/dist/esm/run.mjs.map +1 -1
  81. package/dist/esm/session/messageSerialization.mjs +6 -0
  82. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  83. package/dist/esm/stream.mjs +21 -10
  84. package/dist/esm/stream.mjs.map +1 -1
  85. package/dist/esm/summarization/node.mjs +5 -0
  86. package/dist/esm/summarization/node.mjs.map +1 -1
  87. package/dist/esm/tools/ToolNode.mjs +254 -25
  88. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  89. package/dist/esm/tools/handlers.mjs +1 -1
  90. package/dist/esm/tools/search/tool.mjs +1 -1
  91. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  92. package/dist/esm/utils/index.mjs +2 -2
  93. package/dist/types/graphs/Graph.d.ts +19 -0
  94. package/dist/types/hitl/askUserQuestion.d.ts +11 -1
  95. package/dist/types/langfuse.d.ts +16 -8
  96. package/dist/types/langfuseConfig.d.ts +6 -0
  97. package/dist/types/langfuseRuntimeContext.d.ts +27 -1
  98. package/dist/types/langfuseRuntimeScope.d.ts +17 -2
  99. package/dist/types/langfuseSpanRegistry.d.ts +17 -0
  100. package/dist/types/langfuseTraceShaping.d.ts +2 -1
  101. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
  102. package/dist/types/session/types.d.ts +1 -0
  103. package/dist/types/tools/ToolNode.d.ts +7 -1
  104. package/dist/types/types/hitl.d.ts +8 -0
  105. package/dist/types/types/tools.d.ts +30 -0
  106. package/package.json +7 -4
  107. package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
  108. package/src/graphs/Graph.ts +69 -20
  109. package/src/graphs/MultiAgentGraph.ts +56 -2
  110. package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
  111. package/src/hitl/askUserQuestion.ts +14 -1
  112. package/src/instrumentation.ts +35 -77
  113. package/src/langfuse.ts +320 -43
  114. package/src/langfuseConfig.ts +24 -0
  115. package/src/langfuseRuntimeContext.ts +43 -1
  116. package/src/langfuseRuntimeScope.ts +94 -21
  117. package/src/langfuseSpanRegistry.ts +131 -0
  118. package/src/langfuseTraceShaping.ts +194 -7
  119. package/src/llm/anthropic/utils/message_inputs.ts +70 -19
  120. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
  121. package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
  122. package/src/llm/bedrock/utils/message_inputs.ts +32 -7
  123. package/src/messages/prune.ts +12 -1
  124. package/src/prompts/activityLabel.ts +23 -6
  125. package/src/run.ts +76 -45
  126. package/src/scripts/activity-labels/captured.json +56 -0
  127. package/src/scripts/activity-labels/checks.cjs +205 -0
  128. package/src/scripts/activity-labels/corpus.cjs +473 -0
  129. package/src/scripts/activity-labels/report.cjs +203 -0
  130. package/src/scripts/activity-labels/rescore.cjs +102 -0
  131. package/src/scripts/activity-labels/run.ts +705 -0
  132. package/src/scripts/activity-labels/variants.ts +71 -0
  133. package/src/session/messageSerialization.ts +12 -1
  134. package/src/session/types.ts +1 -0
  135. package/src/specs/activity-label-prompt.test.ts +26 -10
  136. package/src/specs/agent-handoffs.test.ts +306 -0
  137. package/src/specs/langfuse-callbacks.test.ts +456 -0
  138. package/src/specs/langfuse-routing.integration.test.ts +138 -1
  139. package/src/specs/langfuse-span-registry.test.ts +70 -0
  140. package/src/specs/langfuse-trace-shaping.test.ts +294 -0
  141. package/src/specs/prune.test.ts +38 -1
  142. package/src/stream.ts +70 -6
  143. package/src/summarization/node.ts +5 -0
  144. package/src/tools/ToolNode.ts +400 -9
  145. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
  146. package/src/tools/__tests__/hitl.test.ts +58 -0
  147. package/src/types/hitl.ts +8 -0
  148. package/src/types/tools.ts +35 -1
@@ -5,9 +5,9 @@ const require_truncation = require("../utils/truncation.cjs");
5
5
  const require_toolContent = require("../utils/toolContent.cjs");
6
6
  const require_events = require("../utils/events.cjs");
7
7
  const require_injected = require("../messages/injected.cjs");
8
- const require_eagerEventExecution = require("./eagerEventExecution.cjs");
9
8
  const require_intentArg = require("./intentArg.cjs");
10
9
  const require_toolOutputReferences = require("./toolOutputReferences.cjs");
10
+ const require_eagerEventExecution = require("./eagerEventExecution.cjs");
11
11
  const require_run = require("../utils/run.cjs");
12
12
  require("../utils/index.cjs");
13
13
  const require_CodeSessionFileSummary = require("./CodeSessionFileSummary.cjs");
@@ -15,9 +15,9 @@ const require_executeHooks = require("../hooks/executeHooks.cjs");
15
15
  require("../hooks/index.cjs");
16
16
  const require_resolveLocalExecutionTools = require("./local/resolveLocalExecutionTools.cjs");
17
17
  require("./local/index.cjs");
18
+ let nanoid = require("nanoid");
18
19
  let _langchain_core_messages = require("@langchain/core/messages");
19
20
  let _langchain_langgraph = require("@langchain/langgraph");
20
- let nanoid = require("nanoid");
21
21
  let _langchain_core_singletons = require("@langchain/core/singletons");
22
22
  //#region src/tools/ToolNode.ts
23
23
  function createToolErrorOwnership() {
@@ -294,6 +294,12 @@ var ToolNode = class extends require_run.RunnableCallable {
294
294
  eagerEventToolExecutions;
295
295
  /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
296
296
  eagerEventToolUsageCount;
297
+ /**
298
+ * Shared per-run eager prestart circuit breaker. Tool names added here
299
+ * (when a prestarted execution's args mismatch the final request) are no
300
+ * longer prestarted by the stream handler for the rest of the run.
301
+ */
302
+ eagerEventToolSuppressions;
297
303
  /** Agent ID for event-driven mode */
298
304
  agentId;
299
305
  /**
@@ -359,7 +365,7 @@ var ToolNode = class extends require_run.RunnableCallable {
359
365
  * other's in-flight state.
360
366
  */
361
367
  anonBatchCounter = 0;
362
- 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 }) {
368
+ constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, eagerEventToolSuppressions, agentId, executingAgentId, directToolNames, interruptingToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer }) {
363
369
  super({
364
370
  name: name ?? TOOL_NODE_RUN_NAME,
365
371
  tags,
@@ -383,6 +389,7 @@ var ToolNode = class extends require_run.RunnableCallable {
383
389
  this.eagerEventToolExecution = eagerEventToolExecution;
384
390
  this.eagerEventToolExecutions = eagerEventToolExecutions;
385
391
  this.eagerEventToolUsageCount = eagerEventToolUsageCount;
392
+ this.eagerEventToolSuppressions = eagerEventToolSuppressions;
386
393
  this.agentId = agentId;
387
394
  this.executingAgentId = executingAgentId ?? agentId;
388
395
  this.directToolNames = directToolNames;
@@ -412,10 +419,18 @@ var ToolNode = class extends require_run.RunnableCallable {
412
419
  });
413
420
  }
414
421
  async invoke(input, options) {
422
+ const scopedOptions = this.executingAgentId == null ? options : {
423
+ ...options,
424
+ metadata: {
425
+ ...options?.metadata,
426
+ agentId: this.executingAgentId
427
+ }
428
+ };
415
429
  return require_langfuseRuntimeScope.withLangfuseRuntimeScope(require_langfuseRuntimeScope.resolveLangfuseRuntimeScope({
416
430
  runLangfuse: this.runLangfuse,
417
- langfuseOverlay: this.agentLangfuse
418
- }), () => super.invoke(input, options));
431
+ langfuseOverlay: this.agentLangfuse,
432
+ agentId: this.executingAgentId
433
+ }), () => super.invoke(input, scopedOptions));
419
434
  }
420
435
  /**
421
436
  * Returns the run-scoped tool output registry, or `undefined` when
@@ -1968,18 +1983,23 @@ var ToolNode = class extends require_run.RunnableCallable {
1968
1983
  const execution = this.eagerEventToolExecutions?.get(request.id);
1969
1984
  if (execution == null) return;
1970
1985
  this.eagerEventToolExecutions?.delete(request.id);
1971
- if (execution.toolName !== request.name || !require_eagerEventExecution.recordArgsEqual(execution.args, request.args)) return {
1972
- toolCallId: request.id,
1973
- toolName: request.name,
1974
- args: request.args,
1975
- request,
1976
- promise: Promise.resolve({ results: [{
1986
+ if (execution.toolName !== request.name || !require_eagerEventExecution.recordArgsEqual(execution.args, request.args)) {
1987
+ this.eagerEventToolSuppressions?.add(request.name);
1988
+ this.eagerEventToolSuppressions?.add(execution.toolName);
1989
+ console.warn(`[ToolNode] eager prestart args diverged from the final request for tool "${request.name}" (toolCallId=${request.id}); suppressing eager prestart for this tool for the rest of the run`);
1990
+ return {
1977
1991
  toolCallId: request.id,
1978
- status: "error",
1979
- content: "",
1980
- errorMessage: "Tool call changed after eager execution started; refusing to re-run the tool to avoid duplicate side effects."
1981
- }] })
1982
- };
1992
+ toolName: request.name,
1993
+ args: request.args,
1994
+ request,
1995
+ promise: Promise.resolve({ results: [{
1996
+ toolCallId: request.id,
1997
+ status: "error",
1998
+ content: "",
1999
+ errorMessage: "Tool call changed after eager execution started; refusing to re-run the tool to avoid duplicate side effects."
2000
+ }] })
2001
+ };
2002
+ }
1983
2003
  return execution;
1984
2004
  }
1985
2005
  async resolveEagerEventExecution(request, execution) {
@@ -2194,6 +2214,11 @@ var ToolNode = class extends require_run.RunnableCallable {
2194
2214
  const batchScopeId = config.configurable?.run_id ?? `\0anon-${this.anonBatchCounter++}`;
2195
2215
  const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
2196
2216
  let outputs;
2217
+ /** Hoisted from the messages-state branch so the Command tail can carry
2218
+ * the promotion into handoff updates (same-id state copies there would
2219
+ * otherwise overwrite the replacement message). */
2220
+ let promotedAiMessage;
2221
+ let invalidCallResults = [];
2197
2222
  if (this.isSendInput(input)) {
2198
2223
  const isLocalTool = this.directToolNames?.has(input.lg_tool_call.name) === true || this.shouldHandleUnknownHandoffLocally(input.lg_tool_call.name);
2199
2224
  if (this.eventDrivenMode && !isLocalTool) return this.executeViaEvent([input.lg_tool_call], config, input, {
@@ -2250,6 +2275,43 @@ var ToolNode = class extends require_run.RunnableCallable {
2250
2275
  */
2251
2276
  return (call.id == null || !toolMessageIds.has(call.id)) && !(call.id?.startsWith("srvtoolu_") ?? false);
2252
2277
  }) ?? [];
2278
+ const attributableInvalidCalls = !(!Array.isArray(input) && typeof aiMessage.id === "string" && aiMessage.id.length > 0) ? [] : (aiMessage.invalid_tool_calls ?? []).filter((call) => call.id != null && call.id !== "" && !toolMessageIds.has(call.id) && !call.id.startsWith("srvtoolu_"));
2279
+ invalidCallResults = attributableInvalidCalls.map((call) => new _langchain_core_messages.ToolMessage({
2280
+ status: "error",
2281
+ content: require_truncation.truncateToolResultContent(`Error: ${call.error ?? "Malformed tool call arguments."} The tool call input could not be parsed as a JSON object; the tool was not run.
2282
+ Please fix your mistakes.`, this.maxToolResultChars),
2283
+ name: normalizeInvalidCallName(call.name),
2284
+ tool_call_id: call.id
2285
+ }));
2286
+ /**
2287
+ * Promote the answered invalid calls into well-formed `tool_calls` on a
2288
+ * REPLACEMENT copy of the AI message (`messagesStateReducer` upserts by
2289
+ * id). Without this, provider converters that rebuild the call side of
2290
+ * the wire from `tool_calls` — OpenAI Completions `tool_calls`, OpenAI
2291
+ * Responses `function_call` items, Gemini/Bedrock function-call parts —
2292
+ * drop the invalid call while the synthesized result above still
2293
+ * references it, inverting the dangling-pair rejection (an output whose
2294
+ * call is missing). Promoting at this single seam keeps the call and
2295
+ * result sides agreeing for EVERY provider; args become `{}` (the raw
2296
+ * string never parsed — the paired error result tells the model why).
2297
+ * Skipped when the message has no id: the reducer would append a
2298
+ * duplicate instead of replacing, which is worse than the dangle.
2299
+ */
2300
+ promotedAiMessage = attributableInvalidCalls.length > 0 ? new _langchain_core_messages.AIMessage({
2301
+ id: aiMessage.id,
2302
+ content: sanitizeInvalidToolUseBlocks(aiMessage.content, attributableInvalidCalls),
2303
+ name: aiMessage.name,
2304
+ additional_kwargs: aiMessage.additional_kwargs,
2305
+ response_metadata: aiMessage.response_metadata,
2306
+ usage_metadata: aiMessage.usage_metadata,
2307
+ tool_calls: [...aiMessage.tool_calls ?? [], ...attributableInvalidCalls.map((call) => ({
2308
+ id: call.id,
2309
+ name: normalizeInvalidCallName(call.name),
2310
+ args: {},
2311
+ type: "tool_call"
2312
+ }))],
2313
+ invalid_tool_calls: (aiMessage.invalid_tool_calls ?? []).filter((call) => !attributableInvalidCalls.includes(call))
2314
+ }) : void 0;
2253
2315
  if (this.eventDrivenMode && filteredCalls.length > 0) {
2254
2316
  const directToolNames = this.directToolNames;
2255
2317
  const hasRegisteredHandoffTool = this.hasRegisteredHandoffTool();
@@ -2264,7 +2326,7 @@ var ToolNode = class extends require_run.RunnableCallable {
2264
2326
  if (directToolNames?.has(call.name) === true || this.shouldHandleUnknownHandoffLocally(call.name, hasRegisteredHandoffTool)) directEntries.push(entry);
2265
2327
  else eventEntries.push(entry);
2266
2328
  }
2267
- if (directEntries.length === 0) return this.executeViaEvent(filteredCalls, config, input, {
2329
+ if (directEntries.length === 0 && invalidCallResults.length === 0) return this.executeViaEvent(filteredCalls, config, input, {
2268
2330
  batchIndices: eventEntries.map((entry) => entry.batchIndex),
2269
2331
  turn,
2270
2332
  batchScopeId
@@ -2333,8 +2395,10 @@ var ToolNode = class extends require_run.RunnableCallable {
2333
2395
  }
2334
2396
  })] : [];
2335
2397
  outputs = [
2398
+ ...promotedAiMessage != null ? [promotedAiMessage] : [],
2336
2399
  ...directOutputs,
2337
2400
  ...eventResult.toolMessages,
2401
+ ...invalidCallResults,
2338
2402
  ...directInjected,
2339
2403
  ...eventResult.injected
2340
2404
  ];
@@ -2351,16 +2415,51 @@ var ToolNode = class extends require_run.RunnableCallable {
2351
2415
  runInput: input
2352
2416
  });
2353
2417
  await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId, errorOwnership);
2354
- outputs = directAdditionalContexts.length > 0 ? [...toolOutputs, new _langchain_core_messages.HumanMessage({
2355
- content: directAdditionalContexts.join("\n\n"),
2356
- additional_kwargs: {
2357
- role: "system",
2358
- source: "hook"
2359
- }
2360
- })] : toolOutputs;
2418
+ const promotedPrefix = promotedAiMessage != null ? [promotedAiMessage] : [];
2419
+ outputs = directAdditionalContexts.length > 0 ? [
2420
+ ...promotedPrefix,
2421
+ ...toolOutputs,
2422
+ ...invalidCallResults,
2423
+ new _langchain_core_messages.HumanMessage({
2424
+ content: directAdditionalContexts.join("\n\n"),
2425
+ additional_kwargs: {
2426
+ role: "system",
2427
+ source: "hook"
2428
+ }
2429
+ })
2430
+ ] : [
2431
+ ...promotedPrefix,
2432
+ ...toolOutputs,
2433
+ ...invalidCallResults
2434
+ ];
2435
+ }
2436
+ /**
2437
+ * Resolve the streamed tool-call cards for invalid calls, best-effort.
2438
+ * Runs AFTER the direct batch settled: on an interrupting first pass
2439
+ * this line is unreachable (the node unwound), so interrupt/resume
2440
+ * flows emit the completion exactly once — same reasoning as the
2441
+ * deferred blocked-call side effects. Skipped when the stream never
2442
+ * registered a step for the call (non-streaming providers), where a
2443
+ * completion could not be routed to a card anyway.
2444
+ */
2445
+ for (const result of invalidCallResults) {
2446
+ const invalidStepId = this.toolCallStepIds?.get(result.tool_call_id);
2447
+ if (invalidStepId == null || invalidStepId === "") continue;
2448
+ await this.dispatchStepCompleted(result.tool_call_id, result.name ?? "unknown", {}, typeof result.content === "string" ? result.content : "", config);
2361
2449
  }
2362
2450
  }
2363
2451
  if (!outputs.some(_langchain_langgraph.isCommand)) return Array.isArray(input) ? outputs : { messages: outputs };
2452
+ /**
2453
+ * Carry the invalid-call promotion into handoff commands. A handoff
2454
+ * tool's Command snapshots `update.messages` from the PRE-promotion
2455
+ * state (MultiAgentGraph builds a filtered same-id copy of the original
2456
+ * AI message), and commands apply after the sibling reducer updates —
2457
+ * so the stale copy would overwrite the replacement message, and a
2458
+ * Send handoff's child state could omit the synthesized results
2459
+ * entirely. Patch each command's same-id AI message with the promotion
2460
+ * and append any missing synthesized results.
2461
+ */
2462
+ if (promotedAiMessage != null) outputs = outputs.map((output) => (0, _langchain_langgraph.isCommand)(output) ? patchCommandUpdateForPromotedInvalidCalls(output, promotedAiMessage, invalidCallResults) : output);
2364
2463
  const combinedOutputs = [];
2365
2464
  let parentCommand = null;
2366
2465
  /**
@@ -2449,10 +2548,140 @@ function areToolCallsInvoked(message, invokedToolIds) {
2449
2548
  if (!invokedToolIds || invokedToolIds.size === 0) return false;
2450
2549
  return message.tool_calls?.every((toolCall) => toolCall.id != null && invokedToolIds.has(toolCall.id)) ?? false;
2451
2550
  }
2551
+ /**
2552
+ * Normalize the `tool_use` content blocks of promoted invalid calls so the
2553
+ * replacement AI message is valid on EVERY provider surface, not just
2554
+ * `tool_calls`. Anthropic formats an array-content AI message from its blocks
2555
+ * verbatim, and a call whose streamed `input_json` never parsed leaves the
2556
+ * block's `input` as the raw accumulated STRING — replayed as-is, the API
2557
+ * rejects it with `tool_use.input: Input should be an object` before pairing
2558
+ * is even checked. Blocks matching a promoted call id get `input: {}`
2559
+ * (mirroring the promoted args); everything else passes through untouched.
2560
+ * String content (OpenAI-style) is returned as-is.
2561
+ */
2562
+ function sanitizeInvalidToolUseBlocks(content, promotedCalls) {
2563
+ if (!Array.isArray(content)) return content;
2564
+ const promotedNamesById = new Map(promotedCalls.filter((call) => call.id != null).map((call) => [call.id, normalizeInvalidCallName(call.name)]));
2565
+ return content.map((block) => {
2566
+ if (typeof block !== "object" || block?.type !== "tool_use") return block;
2567
+ const toolUse = block;
2568
+ if (toolUse.id == null || !promotedNamesById.has(toolUse.id)) return block;
2569
+ const inputIsObject = typeof toolUse.input === "object" && toolUse.input != null && !Array.isArray(toolUse.input);
2570
+ /** `name` normalizes with the SAME fallback the promoted `tool_calls`
2571
+ * entry uses — a nameless block would fail provider validation on its
2572
+ * own even with a valid input. */
2573
+ const nameIsValid = typeof toolUse.name === "string" && toolUse.name !== "";
2574
+ if (inputIsObject && nameIsValid) return block;
2575
+ return {
2576
+ ...block,
2577
+ ...inputIsObject ? {} : { input: {} },
2578
+ ...nameIsValid ? {} : { name: promotedNamesById.get(toolUse.id) }
2579
+ };
2580
+ });
2581
+ }
2582
+ /**
2583
+ * Name fallback for attributable invalid calls, shared by every surface that
2584
+ * materializes them (synthesized result, promoted tool_calls entry, sanitized
2585
+ * block, handoff patch): `''` is normalized like `undefined` — providers
2586
+ * reject nameless calls, so an empty string would defeat the promotion.
2587
+ *
2588
+ * INVARIANT MAP — a tool call lives in several parallel representations, and
2589
+ * any surface that materializes, copies, filters, routes on, or reports one
2590
+ * must keep ALL of them agreeing (`tool_calls`, `invalid_tool_calls`,
2591
+ * provider content blocks, paired results). The attribution predicate is:
2592
+ * id-bearing (non-empty), non-server (`srvtoolu_`), unanswered
2593
+ * (`toolMessageIds`), messages-state input, id-bearing AI message. Surfaces
2594
+ * that apply it today — extend this list when adding another:
2595
+ * - `run()`'s `canPromoteInvalidCalls` gate + attributable filter
2596
+ * - `toolsCondition`'s invalid-only / server-mix routing branch
2597
+ * - `sanitizeInvalidToolUseBlocks` (block input AND name)
2598
+ * - `patchCommandUpdateForPromotedInvalidCalls` (handoff snapshots)
2599
+ * - `processHandoffReception`'s transfer-block filtering (MultiAgentGraph)
2600
+ * - `findPendingToolCalls` in langfuseTraceShaping (span claims)
2601
+ * - `serializeMessage`/`deserializeMessage` (session round-trip keeps
2602
+ * `invalid_tool_calls` with the content blocks they repair)
2603
+ */
2604
+ function normalizeInvalidCallName(name) {
2605
+ return name != null && name !== "" ? name : "unknown";
2606
+ }
2607
+ /**
2608
+ * Rewrite a handoff Command's `update.messages` so the invalid-call promotion
2609
+ * survives into the child state: the same-id AI message copy (snapshotted
2610
+ * pre-promotion by the handoff tool) gets the sanitized content, the promoted
2611
+ * `tool_calls` entries for the answered invalid calls, and the leftover
2612
+ * `invalid_tool_calls`; synthesized results missing from the update are
2613
+ * appended so the child's history keeps every call/result pair. The update
2614
+ * copy's own `tool_calls` narrowing (parallel handoffs filter to a single
2615
+ * call) is preserved. Commands without a same-id AI message pass through.
2616
+ */
2617
+ function patchCommandUpdateForPromotedInvalidCalls(command, promoted, invalidResults) {
2618
+ const update = command.update;
2619
+ const messages = update?.messages;
2620
+ if (!Array.isArray(messages) || promoted.id == null || invalidResults.length === 0) return command;
2621
+ if (!messages.some((msg) => (0, _langchain_core_messages.isAIMessage)(msg) && msg.id === promoted.id)) return command;
2622
+ const next = messages.map((msg) => {
2623
+ if (!(0, _langchain_core_messages.isAIMessage)(msg) || msg.id !== promoted.id) return msg;
2624
+ const existingIds = new Set((msg.tool_calls ?? []).map((call) => call.id));
2625
+ const promotedEntries = invalidResults.filter((result) => !existingIds.has(result.tool_call_id)).map((result) => ({
2626
+ id: result.tool_call_id,
2627
+ name: normalizeInvalidCallName(result.name),
2628
+ args: {},
2629
+ type: "tool_call"
2630
+ }));
2631
+ return new _langchain_core_messages.AIMessage({
2632
+ id: msg.id,
2633
+ content: promoted.content,
2634
+ name: msg.name,
2635
+ additional_kwargs: msg.additional_kwargs,
2636
+ response_metadata: msg.response_metadata,
2637
+ usage_metadata: msg.usage_metadata,
2638
+ tool_calls: [...msg.tool_calls ?? [], ...promotedEntries],
2639
+ invalid_tool_calls: promoted.invalid_tool_calls
2640
+ });
2641
+ });
2642
+ const presentResultIds = new Set(next.filter((msg) => msg._getType() === "tool").map((msg) => msg.tool_call_id));
2643
+ const missingResults = invalidResults.filter((result) => !presentResultIds.has(result.tool_call_id));
2644
+ return new _langchain_langgraph.Command({
2645
+ graph: command.graph,
2646
+ goto: command.goto,
2647
+ resume: command.resume,
2648
+ update: {
2649
+ ...update,
2650
+ messages: [...next, ...missingResults]
2651
+ }
2652
+ });
2653
+ }
2654
+ /**
2655
+ * Whether the message carries an `invalid_tool_calls` entry ToolNode can pair a
2656
+ * synthesized error result with (id-bearing, non-server). Shared by the routing
2657
+ * condition below so an invalid-only turn still enters ToolNode — otherwise the
2658
+ * malformed `tool_use` block is committed with no `tool_result` and the next
2659
+ * model call is rejected by pairing-strict providers.
2660
+ */
2661
+ function hasAttributableInvalidToolCalls(message) {
2662
+ return message.invalid_tool_calls?.some((call) => call.id != null && call.id !== "" && !call.id.startsWith("srvtoolu_")) ?? false;
2663
+ }
2452
2664
  function toolsCondition(state, toolNode, invokedToolIds) {
2453
2665
  const messages = Array.isArray(state) ? state : state.messages;
2454
2666
  const message = messages[messages.length - 1];
2455
2667
  if (message && "tool_calls" in message && (message.tool_calls?.length ?? 0) > 0 && !areToolCallsInvoked(message, invokedToolIds)) return toolNode;
2668
+ /**
2669
+ * The valid calls (if any) did not route above, but ToolNode still owes any
2670
+ * malformed calls their synthesized error results. Route when EVERY valid
2671
+ * call is provider-server-executed (`srvtoolu_` — ToolNode's batch filter
2672
+ * excludes those before execution, so nothing re-runs): that covers both the
2673
+ * invalid-only turn and the Anthropic server-call + malformed-client-call
2674
+ * mix, where `handleAnthropicSearchResults` marks the server call invoked
2675
+ * and the first branch declines. A valid NON-server call that was invoked
2676
+ * externally stays conservative (no routing) — ToolNode does not filter on
2677
+ * `invokedToolIds`, so entering it would re-execute that call.
2678
+ *
2679
+ * Mirrors ToolNode's own gating exactly, or the routed turn would no-op and
2680
+ * bounce back to the model with the dangle intact: array-state graphs get a
2681
+ * plain output list (no reducer upsert — ToolNode skips invalid handling
2682
+ * there), and an id-less message cannot take the replacement upsert either.
2683
+ */
2684
+ if (!Array.isArray(state) && message && typeof message.id === "string" && message.id.length > 0 && hasAttributableInvalidToolCalls(message) && (message.tool_calls ?? []).every((call) => call.id?.startsWith("srvtoolu_") === true)) return toolNode;
2456
2685
  return _langchain_langgraph.END;
2457
2686
  }
2458
2687
  //#endregion