@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.
- package/dist/cjs/graphs/Graph.cjs +47 -13
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +35 -2
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
- package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +18 -48
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +174 -29
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseConfig.cjs +12 -0
- package/dist/cjs/langfuseConfig.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
- package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
- package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
- package/dist/cjs/langfuseTraceShaping.cjs +121 -4
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +5 -5
- package/dist/cjs/llm/openai/index.cjs +1 -1
- package/dist/cjs/main.cjs +10 -10
- package/dist/cjs/messages/prune.cjs +13 -1
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +24 -12
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +47 -22
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/messageSerialization.cjs +6 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -1
- package/dist/cjs/stream.cjs +21 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +5 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +253 -24
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/search/tool.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/graphs/Graph.mjs +48 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +35 -2
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestion.mjs +3 -2
- package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +18 -48
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +176 -28
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseConfig.mjs +10 -1
- package/dist/esm/langfuseConfig.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeContext.mjs +21 -3
- package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeScope.mjs +39 -10
- package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +91 -0
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
- package/dist/esm/langfuseTraceShaping.mjs +121 -4
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +2 -2
- package/dist/esm/llm/invoke.mjs +5 -5
- package/dist/esm/llm/openai/index.mjs +1 -1
- package/dist/esm/main.mjs +8 -8
- package/dist/esm/messages/prune.mjs +13 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +24 -12
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +47 -22
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/messageSerialization.mjs +6 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -1
- package/dist/esm/stream.mjs +21 -10
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +5 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +254 -25
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/search/tool.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/graphs/Graph.d.ts +19 -0
- package/dist/types/hitl/askUserQuestion.d.ts +11 -1
- package/dist/types/langfuse.d.ts +16 -8
- package/dist/types/langfuseConfig.d.ts +6 -0
- package/dist/types/langfuseRuntimeContext.d.ts +27 -1
- package/dist/types/langfuseRuntimeScope.d.ts +17 -2
- package/dist/types/langfuseSpanRegistry.d.ts +17 -0
- package/dist/types/langfuseTraceShaping.d.ts +2 -1
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
- package/dist/types/session/types.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +7 -1
- package/dist/types/types/hitl.d.ts +8 -0
- package/dist/types/types/tools.d.ts +30 -0
- package/package.json +7 -4
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
- package/src/graphs/Graph.ts +69 -20
- package/src/graphs/MultiAgentGraph.ts +56 -2
- package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
- package/src/hitl/askUserQuestion.ts +14 -1
- package/src/instrumentation.ts +35 -77
- package/src/langfuse.ts +320 -43
- package/src/langfuseConfig.ts +24 -0
- package/src/langfuseRuntimeContext.ts +43 -1
- package/src/langfuseRuntimeScope.ts +94 -21
- package/src/langfuseSpanRegistry.ts +131 -0
- package/src/langfuseTraceShaping.ts +194 -7
- package/src/llm/anthropic/utils/message_inputs.ts +70 -19
- package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
- package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
- package/src/llm/bedrock/utils/message_inputs.ts +32 -7
- package/src/messages/prune.ts +12 -1
- package/src/prompts/activityLabel.ts +23 -6
- package/src/run.ts +76 -45
- package/src/scripts/activity-labels/captured.json +56 -0
- package/src/scripts/activity-labels/checks.cjs +205 -0
- package/src/scripts/activity-labels/corpus.cjs +473 -0
- package/src/scripts/activity-labels/report.cjs +203 -0
- package/src/scripts/activity-labels/rescore.cjs +102 -0
- package/src/scripts/activity-labels/run.ts +705 -0
- package/src/scripts/activity-labels/variants.ts +71 -0
- package/src/session/messageSerialization.ts +12 -1
- package/src/session/types.ts +1 -0
- package/src/specs/activity-label-prompt.test.ts +26 -10
- package/src/specs/agent-handoffs.test.ts +306 -0
- package/src/specs/langfuse-callbacks.test.ts +456 -0
- package/src/specs/langfuse-routing.integration.test.ts +138 -1
- package/src/specs/langfuse-span-registry.test.ts +70 -0
- package/src/specs/langfuse-trace-shaping.test.ts +294 -0
- package/src/specs/prune.test.ts +38 -1
- package/src/stream.ts +70 -6
- package/src/summarization/node.ts +5 -0
- package/src/tools/ToolNode.ts +400 -9
- package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
- package/src/tools/__tests__/hitl.test.ts +58 -0
- package/src/types/hitl.ts +8 -0
- package/src/types/tools.ts +35 -1
|
@@ -5,9 +5,9 @@ import { calculateMaxToolResultChars, truncateToolResultContent } from "../utils
|
|
|
5
5
|
import { cloneToolMessageWithContent, compactToolContent, hasComputerCallOutputMarker, isComputerCallOutputContent, isComputerCallOutputMessage, serializeStructuredValueBounded, serializeToolContentBounded } from "../utils/toolContent.mjs";
|
|
6
6
|
import { safeDispatchCustomEvent } from "../utils/events.mjs";
|
|
7
7
|
import { convertInjectedMessages } from "../messages/injected.mjs";
|
|
8
|
-
import { buildToolExecutionRequestPlan, recordArgsEqual, resolveRuntimeSessionHint } from "./eagerEventExecution.mjs";
|
|
9
8
|
import { INTENT_ARG, isIntentLabelProperty, outcomeFieldsFromResult, readOutcomeFields, resolveToolOutcome } from "./intentArg.mjs";
|
|
10
9
|
import { ToolOutputReferenceRegistry, buildReferenceKey } from "./toolOutputReferences.mjs";
|
|
10
|
+
import { buildToolExecutionRequestPlan, recordArgsEqual, resolveRuntimeSessionHint } from "./eagerEventExecution.mjs";
|
|
11
11
|
import { RunnableCallable } from "../utils/run.mjs";
|
|
12
12
|
import "../utils/index.mjs";
|
|
13
13
|
import { stripCodeSessionFileSummary } from "./CodeSessionFileSummary.mjs";
|
|
@@ -15,9 +15,9 @@ import { executeHooks } from "../hooks/executeHooks.mjs";
|
|
|
15
15
|
import "../hooks/index.mjs";
|
|
16
16
|
import { resolveLocalExecutionTools, resolveLocalToolRegistry } from "./local/resolveLocalExecutionTools.mjs";
|
|
17
17
|
import "./local/index.mjs";
|
|
18
|
-
import { HumanMessage, ToolMessage, isAIMessage, isBaseMessage } from "@langchain/core/messages";
|
|
19
|
-
import { Command, END, Send, interrupt, isCommand, isGraphInterrupt } from "@langchain/langgraph";
|
|
20
18
|
import { nanoid } from "nanoid";
|
|
19
|
+
import { AIMessage, HumanMessage, ToolMessage, isAIMessage, isBaseMessage } from "@langchain/core/messages";
|
|
20
|
+
import { Command, END, Send, interrupt, isCommand, isGraphInterrupt } from "@langchain/langgraph";
|
|
21
21
|
import { AsyncLocalStorageProviderSingleton } from "@langchain/core/singletons";
|
|
22
22
|
//#region src/tools/ToolNode.ts
|
|
23
23
|
function createToolErrorOwnership() {
|
|
@@ -294,6 +294,12 @@ var ToolNode = class extends 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 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 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 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 withLangfuseRuntimeScope(resolveLangfuseRuntimeScope({
|
|
416
430
|
runLangfuse: this.runLangfuse,
|
|
417
|
-
langfuseOverlay: this.agentLangfuse
|
|
418
|
-
|
|
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
|
|
@@ -1966,18 +1981,23 @@ var ToolNode = class extends RunnableCallable {
|
|
|
1966
1981
|
const execution = this.eagerEventToolExecutions?.get(request.id);
|
|
1967
1982
|
if (execution == null) return;
|
|
1968
1983
|
this.eagerEventToolExecutions?.delete(request.id);
|
|
1969
|
-
if (execution.toolName !== request.name || !recordArgsEqual(execution.args, request.args))
|
|
1970
|
-
|
|
1971
|
-
toolName
|
|
1972
|
-
args
|
|
1973
|
-
|
|
1974
|
-
promise: Promise.resolve({ results: [{
|
|
1984
|
+
if (execution.toolName !== request.name || !recordArgsEqual(execution.args, request.args)) {
|
|
1985
|
+
this.eagerEventToolSuppressions?.add(request.name);
|
|
1986
|
+
this.eagerEventToolSuppressions?.add(execution.toolName);
|
|
1987
|
+
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`);
|
|
1988
|
+
return {
|
|
1975
1989
|
toolCallId: request.id,
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1990
|
+
toolName: request.name,
|
|
1991
|
+
args: request.args,
|
|
1992
|
+
request,
|
|
1993
|
+
promise: Promise.resolve({ results: [{
|
|
1994
|
+
toolCallId: request.id,
|
|
1995
|
+
status: "error",
|
|
1996
|
+
content: "",
|
|
1997
|
+
errorMessage: "Tool call changed after eager execution started; refusing to re-run the tool to avoid duplicate side effects."
|
|
1998
|
+
}] })
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
1981
2001
|
return execution;
|
|
1982
2002
|
}
|
|
1983
2003
|
async resolveEagerEventExecution(request, execution) {
|
|
@@ -2192,6 +2212,11 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2192
2212
|
const batchScopeId = config.configurable?.run_id ?? `\0anon-${this.anonBatchCounter++}`;
|
|
2193
2213
|
const turn = this.toolOutputRegistry?.nextTurn(batchScopeId) ?? 0;
|
|
2194
2214
|
let outputs;
|
|
2215
|
+
/** Hoisted from the messages-state branch so the Command tail can carry
|
|
2216
|
+
* the promotion into handoff updates (same-id state copies there would
|
|
2217
|
+
* otherwise overwrite the replacement message). */
|
|
2218
|
+
let promotedAiMessage;
|
|
2219
|
+
let invalidCallResults = [];
|
|
2195
2220
|
if (this.isSendInput(input)) {
|
|
2196
2221
|
const isLocalTool = this.directToolNames?.has(input.lg_tool_call.name) === true || this.shouldHandleUnknownHandoffLocally(input.lg_tool_call.name);
|
|
2197
2222
|
if (this.eventDrivenMode && !isLocalTool) return this.executeViaEvent([input.lg_tool_call], config, input, {
|
|
@@ -2248,6 +2273,43 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2248
2273
|
*/
|
|
2249
2274
|
return (call.id == null || !toolMessageIds.has(call.id)) && !(call.id?.startsWith("srvtoolu_") ?? false);
|
|
2250
2275
|
}) ?? [];
|
|
2276
|
+
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_"));
|
|
2277
|
+
invalidCallResults = attributableInvalidCalls.map((call) => new ToolMessage({
|
|
2278
|
+
status: "error",
|
|
2279
|
+
content: 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.
|
|
2280
|
+
Please fix your mistakes.`, this.maxToolResultChars),
|
|
2281
|
+
name: normalizeInvalidCallName(call.name),
|
|
2282
|
+
tool_call_id: call.id
|
|
2283
|
+
}));
|
|
2284
|
+
/**
|
|
2285
|
+
* Promote the answered invalid calls into well-formed `tool_calls` on a
|
|
2286
|
+
* REPLACEMENT copy of the AI message (`messagesStateReducer` upserts by
|
|
2287
|
+
* id). Without this, provider converters that rebuild the call side of
|
|
2288
|
+
* the wire from `tool_calls` — OpenAI Completions `tool_calls`, OpenAI
|
|
2289
|
+
* Responses `function_call` items, Gemini/Bedrock function-call parts —
|
|
2290
|
+
* drop the invalid call while the synthesized result above still
|
|
2291
|
+
* references it, inverting the dangling-pair rejection (an output whose
|
|
2292
|
+
* call is missing). Promoting at this single seam keeps the call and
|
|
2293
|
+
* result sides agreeing for EVERY provider; args become `{}` (the raw
|
|
2294
|
+
* string never parsed — the paired error result tells the model why).
|
|
2295
|
+
* Skipped when the message has no id: the reducer would append a
|
|
2296
|
+
* duplicate instead of replacing, which is worse than the dangle.
|
|
2297
|
+
*/
|
|
2298
|
+
promotedAiMessage = attributableInvalidCalls.length > 0 ? new AIMessage({
|
|
2299
|
+
id: aiMessage.id,
|
|
2300
|
+
content: sanitizeInvalidToolUseBlocks(aiMessage.content, attributableInvalidCalls),
|
|
2301
|
+
name: aiMessage.name,
|
|
2302
|
+
additional_kwargs: aiMessage.additional_kwargs,
|
|
2303
|
+
response_metadata: aiMessage.response_metadata,
|
|
2304
|
+
usage_metadata: aiMessage.usage_metadata,
|
|
2305
|
+
tool_calls: [...aiMessage.tool_calls ?? [], ...attributableInvalidCalls.map((call) => ({
|
|
2306
|
+
id: call.id,
|
|
2307
|
+
name: normalizeInvalidCallName(call.name),
|
|
2308
|
+
args: {},
|
|
2309
|
+
type: "tool_call"
|
|
2310
|
+
}))],
|
|
2311
|
+
invalid_tool_calls: (aiMessage.invalid_tool_calls ?? []).filter((call) => !attributableInvalidCalls.includes(call))
|
|
2312
|
+
}) : void 0;
|
|
2251
2313
|
if (this.eventDrivenMode && filteredCalls.length > 0) {
|
|
2252
2314
|
const directToolNames = this.directToolNames;
|
|
2253
2315
|
const hasRegisteredHandoffTool = this.hasRegisteredHandoffTool();
|
|
@@ -2262,7 +2324,7 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2262
2324
|
if (directToolNames?.has(call.name) === true || this.shouldHandleUnknownHandoffLocally(call.name, hasRegisteredHandoffTool)) directEntries.push(entry);
|
|
2263
2325
|
else eventEntries.push(entry);
|
|
2264
2326
|
}
|
|
2265
|
-
if (directEntries.length === 0) return this.executeViaEvent(filteredCalls, config, input, {
|
|
2327
|
+
if (directEntries.length === 0 && invalidCallResults.length === 0) return this.executeViaEvent(filteredCalls, config, input, {
|
|
2266
2328
|
batchIndices: eventEntries.map((entry) => entry.batchIndex),
|
|
2267
2329
|
turn,
|
|
2268
2330
|
batchScopeId
|
|
@@ -2331,8 +2393,10 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2331
2393
|
}
|
|
2332
2394
|
})] : [];
|
|
2333
2395
|
outputs = [
|
|
2396
|
+
...promotedAiMessage != null ? [promotedAiMessage] : [],
|
|
2334
2397
|
...directOutputs,
|
|
2335
2398
|
...eventResult.toolMessages,
|
|
2399
|
+
...invalidCallResults,
|
|
2336
2400
|
...directInjected,
|
|
2337
2401
|
...eventResult.injected
|
|
2338
2402
|
];
|
|
@@ -2349,16 +2413,51 @@ var ToolNode = class extends RunnableCallable {
|
|
|
2349
2413
|
runInput: input
|
|
2350
2414
|
});
|
|
2351
2415
|
await this.handleRunToolCompletions(filteredCalls, toolOutputs, config, resolvedArgsByCallId, errorOwnership);
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2416
|
+
const promotedPrefix = promotedAiMessage != null ? [promotedAiMessage] : [];
|
|
2417
|
+
outputs = directAdditionalContexts.length > 0 ? [
|
|
2418
|
+
...promotedPrefix,
|
|
2419
|
+
...toolOutputs,
|
|
2420
|
+
...invalidCallResults,
|
|
2421
|
+
new HumanMessage({
|
|
2422
|
+
content: directAdditionalContexts.join("\n\n"),
|
|
2423
|
+
additional_kwargs: {
|
|
2424
|
+
role: "system",
|
|
2425
|
+
source: "hook"
|
|
2426
|
+
}
|
|
2427
|
+
})
|
|
2428
|
+
] : [
|
|
2429
|
+
...promotedPrefix,
|
|
2430
|
+
...toolOutputs,
|
|
2431
|
+
...invalidCallResults
|
|
2432
|
+
];
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Resolve the streamed tool-call cards for invalid calls, best-effort.
|
|
2436
|
+
* Runs AFTER the direct batch settled: on an interrupting first pass
|
|
2437
|
+
* this line is unreachable (the node unwound), so interrupt/resume
|
|
2438
|
+
* flows emit the completion exactly once — same reasoning as the
|
|
2439
|
+
* deferred blocked-call side effects. Skipped when the stream never
|
|
2440
|
+
* registered a step for the call (non-streaming providers), where a
|
|
2441
|
+
* completion could not be routed to a card anyway.
|
|
2442
|
+
*/
|
|
2443
|
+
for (const result of invalidCallResults) {
|
|
2444
|
+
const invalidStepId = this.toolCallStepIds?.get(result.tool_call_id);
|
|
2445
|
+
if (invalidStepId == null || invalidStepId === "") continue;
|
|
2446
|
+
await this.dispatchStepCompleted(result.tool_call_id, result.name ?? "unknown", {}, typeof result.content === "string" ? result.content : "", config);
|
|
2359
2447
|
}
|
|
2360
2448
|
}
|
|
2361
2449
|
if (!outputs.some(isCommand)) return Array.isArray(input) ? outputs : { messages: outputs };
|
|
2450
|
+
/**
|
|
2451
|
+
* Carry the invalid-call promotion into handoff commands. A handoff
|
|
2452
|
+
* tool's Command snapshots `update.messages` from the PRE-promotion
|
|
2453
|
+
* state (MultiAgentGraph builds a filtered same-id copy of the original
|
|
2454
|
+
* AI message), and commands apply after the sibling reducer updates —
|
|
2455
|
+
* so the stale copy would overwrite the replacement message, and a
|
|
2456
|
+
* Send handoff's child state could omit the synthesized results
|
|
2457
|
+
* entirely. Patch each command's same-id AI message with the promotion
|
|
2458
|
+
* and append any missing synthesized results.
|
|
2459
|
+
*/
|
|
2460
|
+
if (promotedAiMessage != null) outputs = outputs.map((output) => isCommand(output) ? patchCommandUpdateForPromotedInvalidCalls(output, promotedAiMessage, invalidCallResults) : output);
|
|
2362
2461
|
const combinedOutputs = [];
|
|
2363
2462
|
let parentCommand = null;
|
|
2364
2463
|
/**
|
|
@@ -2447,10 +2546,140 @@ function areToolCallsInvoked(message, invokedToolIds) {
|
|
|
2447
2546
|
if (!invokedToolIds || invokedToolIds.size === 0) return false;
|
|
2448
2547
|
return message.tool_calls?.every((toolCall) => toolCall.id != null && invokedToolIds.has(toolCall.id)) ?? false;
|
|
2449
2548
|
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Normalize the `tool_use` content blocks of promoted invalid calls so the
|
|
2551
|
+
* replacement AI message is valid on EVERY provider surface, not just
|
|
2552
|
+
* `tool_calls`. Anthropic formats an array-content AI message from its blocks
|
|
2553
|
+
* verbatim, and a call whose streamed `input_json` never parsed leaves the
|
|
2554
|
+
* block's `input` as the raw accumulated STRING — replayed as-is, the API
|
|
2555
|
+
* rejects it with `tool_use.input: Input should be an object` before pairing
|
|
2556
|
+
* is even checked. Blocks matching a promoted call id get `input: {}`
|
|
2557
|
+
* (mirroring the promoted args); everything else passes through untouched.
|
|
2558
|
+
* String content (OpenAI-style) is returned as-is.
|
|
2559
|
+
*/
|
|
2560
|
+
function sanitizeInvalidToolUseBlocks(content, promotedCalls) {
|
|
2561
|
+
if (!Array.isArray(content)) return content;
|
|
2562
|
+
const promotedNamesById = new Map(promotedCalls.filter((call) => call.id != null).map((call) => [call.id, normalizeInvalidCallName(call.name)]));
|
|
2563
|
+
return content.map((block) => {
|
|
2564
|
+
if (typeof block !== "object" || block?.type !== "tool_use") return block;
|
|
2565
|
+
const toolUse = block;
|
|
2566
|
+
if (toolUse.id == null || !promotedNamesById.has(toolUse.id)) return block;
|
|
2567
|
+
const inputIsObject = typeof toolUse.input === "object" && toolUse.input != null && !Array.isArray(toolUse.input);
|
|
2568
|
+
/** `name` normalizes with the SAME fallback the promoted `tool_calls`
|
|
2569
|
+
* entry uses — a nameless block would fail provider validation on its
|
|
2570
|
+
* own even with a valid input. */
|
|
2571
|
+
const nameIsValid = typeof toolUse.name === "string" && toolUse.name !== "";
|
|
2572
|
+
if (inputIsObject && nameIsValid) return block;
|
|
2573
|
+
return {
|
|
2574
|
+
...block,
|
|
2575
|
+
...inputIsObject ? {} : { input: {} },
|
|
2576
|
+
...nameIsValid ? {} : { name: promotedNamesById.get(toolUse.id) }
|
|
2577
|
+
};
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* Name fallback for attributable invalid calls, shared by every surface that
|
|
2582
|
+
* materializes them (synthesized result, promoted tool_calls entry, sanitized
|
|
2583
|
+
* block, handoff patch): `''` is normalized like `undefined` — providers
|
|
2584
|
+
* reject nameless calls, so an empty string would defeat the promotion.
|
|
2585
|
+
*
|
|
2586
|
+
* INVARIANT MAP — a tool call lives in several parallel representations, and
|
|
2587
|
+
* any surface that materializes, copies, filters, routes on, or reports one
|
|
2588
|
+
* must keep ALL of them agreeing (`tool_calls`, `invalid_tool_calls`,
|
|
2589
|
+
* provider content blocks, paired results). The attribution predicate is:
|
|
2590
|
+
* id-bearing (non-empty), non-server (`srvtoolu_`), unanswered
|
|
2591
|
+
* (`toolMessageIds`), messages-state input, id-bearing AI message. Surfaces
|
|
2592
|
+
* that apply it today — extend this list when adding another:
|
|
2593
|
+
* - `run()`'s `canPromoteInvalidCalls` gate + attributable filter
|
|
2594
|
+
* - `toolsCondition`'s invalid-only / server-mix routing branch
|
|
2595
|
+
* - `sanitizeInvalidToolUseBlocks` (block input AND name)
|
|
2596
|
+
* - `patchCommandUpdateForPromotedInvalidCalls` (handoff snapshots)
|
|
2597
|
+
* - `processHandoffReception`'s transfer-block filtering (MultiAgentGraph)
|
|
2598
|
+
* - `findPendingToolCalls` in langfuseTraceShaping (span claims)
|
|
2599
|
+
* - `serializeMessage`/`deserializeMessage` (session round-trip keeps
|
|
2600
|
+
* `invalid_tool_calls` with the content blocks they repair)
|
|
2601
|
+
*/
|
|
2602
|
+
function normalizeInvalidCallName(name) {
|
|
2603
|
+
return name != null && name !== "" ? name : "unknown";
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* Rewrite a handoff Command's `update.messages` so the invalid-call promotion
|
|
2607
|
+
* survives into the child state: the same-id AI message copy (snapshotted
|
|
2608
|
+
* pre-promotion by the handoff tool) gets the sanitized content, the promoted
|
|
2609
|
+
* `tool_calls` entries for the answered invalid calls, and the leftover
|
|
2610
|
+
* `invalid_tool_calls`; synthesized results missing from the update are
|
|
2611
|
+
* appended so the child's history keeps every call/result pair. The update
|
|
2612
|
+
* copy's own `tool_calls` narrowing (parallel handoffs filter to a single
|
|
2613
|
+
* call) is preserved. Commands without a same-id AI message pass through.
|
|
2614
|
+
*/
|
|
2615
|
+
function patchCommandUpdateForPromotedInvalidCalls(command, promoted, invalidResults) {
|
|
2616
|
+
const update = command.update;
|
|
2617
|
+
const messages = update?.messages;
|
|
2618
|
+
if (!Array.isArray(messages) || promoted.id == null || invalidResults.length === 0) return command;
|
|
2619
|
+
if (!messages.some((msg) => isAIMessage(msg) && msg.id === promoted.id)) return command;
|
|
2620
|
+
const next = messages.map((msg) => {
|
|
2621
|
+
if (!isAIMessage(msg) || msg.id !== promoted.id) return msg;
|
|
2622
|
+
const existingIds = new Set((msg.tool_calls ?? []).map((call) => call.id));
|
|
2623
|
+
const promotedEntries = invalidResults.filter((result) => !existingIds.has(result.tool_call_id)).map((result) => ({
|
|
2624
|
+
id: result.tool_call_id,
|
|
2625
|
+
name: normalizeInvalidCallName(result.name),
|
|
2626
|
+
args: {},
|
|
2627
|
+
type: "tool_call"
|
|
2628
|
+
}));
|
|
2629
|
+
return new AIMessage({
|
|
2630
|
+
id: msg.id,
|
|
2631
|
+
content: promoted.content,
|
|
2632
|
+
name: msg.name,
|
|
2633
|
+
additional_kwargs: msg.additional_kwargs,
|
|
2634
|
+
response_metadata: msg.response_metadata,
|
|
2635
|
+
usage_metadata: msg.usage_metadata,
|
|
2636
|
+
tool_calls: [...msg.tool_calls ?? [], ...promotedEntries],
|
|
2637
|
+
invalid_tool_calls: promoted.invalid_tool_calls
|
|
2638
|
+
});
|
|
2639
|
+
});
|
|
2640
|
+
const presentResultIds = new Set(next.filter((msg) => msg._getType() === "tool").map((msg) => msg.tool_call_id));
|
|
2641
|
+
const missingResults = invalidResults.filter((result) => !presentResultIds.has(result.tool_call_id));
|
|
2642
|
+
return new Command({
|
|
2643
|
+
graph: command.graph,
|
|
2644
|
+
goto: command.goto,
|
|
2645
|
+
resume: command.resume,
|
|
2646
|
+
update: {
|
|
2647
|
+
...update,
|
|
2648
|
+
messages: [...next, ...missingResults]
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Whether the message carries an `invalid_tool_calls` entry ToolNode can pair a
|
|
2654
|
+
* synthesized error result with (id-bearing, non-server). Shared by the routing
|
|
2655
|
+
* condition below so an invalid-only turn still enters ToolNode — otherwise the
|
|
2656
|
+
* malformed `tool_use` block is committed with no `tool_result` and the next
|
|
2657
|
+
* model call is rejected by pairing-strict providers.
|
|
2658
|
+
*/
|
|
2659
|
+
function hasAttributableInvalidToolCalls(message) {
|
|
2660
|
+
return message.invalid_tool_calls?.some((call) => call.id != null && call.id !== "" && !call.id.startsWith("srvtoolu_")) ?? false;
|
|
2661
|
+
}
|
|
2450
2662
|
function toolsCondition(state, toolNode, invokedToolIds) {
|
|
2451
2663
|
const messages = Array.isArray(state) ? state : state.messages;
|
|
2452
2664
|
const message = messages[messages.length - 1];
|
|
2453
2665
|
if (message && "tool_calls" in message && (message.tool_calls?.length ?? 0) > 0 && !areToolCallsInvoked(message, invokedToolIds)) return toolNode;
|
|
2666
|
+
/**
|
|
2667
|
+
* The valid calls (if any) did not route above, but ToolNode still owes any
|
|
2668
|
+
* malformed calls their synthesized error results. Route when EVERY valid
|
|
2669
|
+
* call is provider-server-executed (`srvtoolu_` — ToolNode's batch filter
|
|
2670
|
+
* excludes those before execution, so nothing re-runs): that covers both the
|
|
2671
|
+
* invalid-only turn and the Anthropic server-call + malformed-client-call
|
|
2672
|
+
* mix, where `handleAnthropicSearchResults` marks the server call invoked
|
|
2673
|
+
* and the first branch declines. A valid NON-server call that was invoked
|
|
2674
|
+
* externally stays conservative (no routing) — ToolNode does not filter on
|
|
2675
|
+
* `invokedToolIds`, so entering it would re-execute that call.
|
|
2676
|
+
*
|
|
2677
|
+
* Mirrors ToolNode's own gating exactly, or the routed turn would no-op and
|
|
2678
|
+
* bounce back to the model with the dangle intact: array-state graphs get a
|
|
2679
|
+
* plain output list (no reducer upsert — ToolNode skips invalid handling
|
|
2680
|
+
* there), and an id-less message cannot take the replacement upsert either.
|
|
2681
|
+
*/
|
|
2682
|
+
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;
|
|
2454
2683
|
return END;
|
|
2455
2684
|
}
|
|
2456
2685
|
//#endregion
|