@librechat/agents 3.2.67 → 3.3.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 (68) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +1 -1
  2. package/dist/cjs/common/enum.cjs +2 -0
  3. package/dist/cjs/common/enum.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +14 -1
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  8. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  9. package/dist/cjs/llm/google/index.cjs +2 -0
  10. package/dist/cjs/llm/google/index.cjs.map +1 -1
  11. package/dist/cjs/llm/google/utils/common.cjs +28 -0
  12. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  13. package/dist/cjs/llm/openai/index.cjs +1 -1
  14. package/dist/cjs/main.cjs +1 -0
  15. package/dist/cjs/messages/format.cjs +136 -4
  16. package/dist/cjs/messages/format.cjs.map +1 -1
  17. package/dist/cjs/prompts/activityLabel.cjs +101 -0
  18. package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
  19. package/dist/cjs/run.cjs +162 -1
  20. package/dist/cjs/run.cjs.map +1 -1
  21. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  22. package/dist/esm/agents/AgentContext.mjs +1 -1
  23. package/dist/esm/common/enum.mjs +2 -0
  24. package/dist/esm/common/enum.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +15 -2
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  28. package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
  29. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  30. package/dist/esm/llm/google/index.mjs +3 -1
  31. package/dist/esm/llm/google/index.mjs.map +1 -1
  32. package/dist/esm/llm/google/utils/common.mjs +28 -1
  33. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  34. package/dist/esm/llm/openai/index.mjs +1 -1
  35. package/dist/esm/main.mjs +2 -2
  36. package/dist/esm/messages/format.mjs +136 -5
  37. package/dist/esm/messages/format.mjs.map +1 -1
  38. package/dist/esm/prompts/activityLabel.mjs +100 -0
  39. package/dist/esm/prompts/activityLabel.mjs.map +1 -0
  40. package/dist/esm/run.mjs +163 -2
  41. package/dist/esm/run.mjs.map +1 -1
  42. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  43. package/dist/types/common/enum.d.ts +3 -1
  44. package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
  45. package/dist/types/llm/google/utils/common.d.ts +9 -0
  46. package/dist/types/messages/format.d.ts +22 -0
  47. package/dist/types/prompts/activityLabel.d.ts +31 -0
  48. package/dist/types/run.d.ts +14 -0
  49. package/dist/types/types/activityLabel.d.ts +53 -0
  50. package/dist/types/types/index.d.ts +1 -0
  51. package/dist/types/types/stream.d.ts +2 -0
  52. package/package.json +1 -1
  53. package/src/common/enum.ts +2 -0
  54. package/src/graphs/Graph.ts +20 -0
  55. package/src/langfuseToolOutputTracing.ts +4 -1
  56. package/src/llm/google/index.ts +3 -0
  57. package/src/llm/google/utils/common.test.ts +57 -2
  58. package/src/llm/google/utils/common.ts +44 -0
  59. package/src/messages/foldToollessToolBlocks.test.ts +438 -0
  60. package/src/messages/format.ts +233 -5
  61. package/src/prompts/activityLabel.ts +177 -0
  62. package/src/run.ts +298 -2
  63. package/src/specs/activity-label-prompt.test.ts +128 -0
  64. package/src/specs/activity-label-trace-seed.test.ts +47 -0
  65. package/src/specs/bedrock-toolless.live.test.ts +123 -0
  66. package/src/types/activityLabel.ts +55 -0
  67. package/src/types/index.ts +1 -0
  68. package/src/types/stream.ts +2 -0
@@ -152,7 +152,7 @@ function getTextContent(part) {
152
152
  }
153
153
  function hasMeaningfulAssistantContent(part) {
154
154
  if (part.type === "text") return getTextContent(part).trim().length > 0;
155
- if (part.type === "tool_call" || part.type === "error" || part.type === "agent_update" || part.type === "summary") return false;
155
+ if (part.type === "tool_call" || part.type === "error" || part.type === "agent_update" || part.type === "summary" || part.type === "activity_label") return false;
156
156
  if (part.type === "think" || part.type === "thinking" || part.type === "reasoning" || part.type === "reasoning_content" || part.type === "redacted_thinking") return extractReasoningContent(part).trim().length > 0;
157
157
  return part.type != null && part.type !== "";
158
158
  }
@@ -357,7 +357,7 @@ function formatAssistantMessage(message, options) {
357
357
  * flushed above or intentionally dropped when not preserving). */
358
358
  hasReasoning = false;
359
359
  pendingReasoningContent = "";
360
- } else if (part.type === "error" || part.type === "agent_update" || part.type === "summary") continue;
360
+ } else if (part.type === "error" || part.type === "agent_update" || part.type === "summary" || part.type === "activity_label") continue;
361
361
  else {
362
362
  if (part.type === "text" && !getTextContent(part).trim()) continue;
363
363
  currentContent.push(part);
@@ -422,6 +422,12 @@ function labelAllAgentContent(contentParts, agentIdMap, agentNames) {
422
422
  };
423
423
  for (let i = 0; i < contentParts.length; i++) {
424
424
  const part = contentParts[i];
425
+ /** UI-only progress headers are not agent content and must not disturb
426
+ * agent state: a label with no `agentIdMap` entry would otherwise read
427
+ * as an agent change and flush the buffer mid-agent, splitting one
428
+ * agent's contiguous content into two labeled blocks. Skipped before
429
+ * any state transition below (mirrors the transfer path). */
430
+ if (part.type === "activity_label") continue;
425
431
  const agentId = agentIdMap[i];
426
432
  if (agentId !== currentAgentId && currentAgentId !== void 0) flushAgentBuffer();
427
433
  currentAgentId = agentId;
@@ -487,6 +493,12 @@ const labelContentByAgent = (contentParts, agentIdMap, agentNames, options) => {
487
493
  };
488
494
  for (let i = 0; i < contentParts.length; i++) {
489
495
  const part = contentParts[i];
496
+ /** UI-only progress headers are not agent content and must not disturb
497
+ * agent state: a label with no `agentIdMap` entry would otherwise look
498
+ * like an agent change, flushing the buffer and resetting an open
499
+ * transfer capture so the transferred agent's following chunks lose
500
+ * their frame. Skipped before any state transition below. */
501
+ if (part.type === "activity_label") continue;
490
502
  const agentId = agentIdMap[i];
491
503
  const isTransferTool = (part.type === "tool_call" && part.tool_call?.name?.startsWith("lc_transfer_to_")) ?? false;
492
504
  if (agentId !== currentAgentId && currentAgentId !== void 0) flushAgentBuffer();
@@ -902,6 +914,34 @@ function appendMessageContent(msg, role, textChunks, parts) {
902
914
  textChunks.push(`${role}: [tool_use] ${String(block.name ?? "")} ${JSON.stringify(block.input ?? {})}`);
903
915
  continue;
904
916
  }
917
+ if (block.type === "tool_call") {
918
+ hasToolUseBlock = true;
919
+ const nested = block.tool_call;
920
+ const name = String(nested?.name ?? block.name ?? "");
921
+ const rawArgs = nested?.args ?? block.args ?? {};
922
+ const argsText = typeof rawArgs === "string" ? rawArgs : JSON.stringify(rawArgs);
923
+ textChunks.push(`${role}: [tool_use] ${name} ${argsText}`.trimEnd());
924
+ const output = nested?.output;
925
+ if (output != null && output !== "") textChunks.push(`Tool: ${String(output)}`);
926
+ continue;
927
+ }
928
+ if (block.type === "tool_result") {
929
+ hasToolUseBlock = true;
930
+ const inner = block.content;
931
+ if (typeof inner === "string") {
932
+ if (inner) textChunks.push(`${role}: [tool_result] ${inner}`);
933
+ } else if (Array.isArray(inner)) for (const innerBlock of inner) if (typeof innerBlock === "string") {
934
+ if (innerBlock) textChunks.push(`${role}: [tool_result] ${innerBlock}`);
935
+ } else if (IMAGE_BLOCK_TYPES.has(innerBlock.type ?? "")) {
936
+ flushTextChunks(textChunks, parts);
937
+ parts.push({ ...innerBlock });
938
+ } else {
939
+ const innerText = innerBlock.text ?? innerBlock.input;
940
+ textChunks.push(`${role}: [tool_result] ${typeof innerText === "string" && innerText ? innerText : JSON.stringify(innerBlock)}`);
941
+ }
942
+ else if (inner != null) textChunks.push(`${role}: [tool_result] ${JSON.stringify(inner)}`);
943
+ continue;
944
+ }
905
945
  const text = block.text ?? block.input;
906
946
  if (typeof text === "string" && text) {
907
947
  textChunks.push(`${role}: ${text}`);
@@ -914,8 +954,17 @@ function appendMessageContent(msg, role, textChunks, parts) {
914
954
  function appendToolCalls(msg, role, textChunks) {
915
955
  if (role !== "AI") return;
916
956
  const aiMsg = msg;
917
- if (!aiMsg.tool_calls || aiMsg.tool_calls.length === 0) return;
918
- for (const tc of aiMsg.tool_calls) textChunks.push(`AI: [tool_call] ${tc.name}(${JSON.stringify(tc.args)})`);
957
+ if (aiMsg.tool_calls && aiMsg.tool_calls.length > 0) {
958
+ for (const tc of aiMsg.tool_calls) textChunks.push(`AI: [tool_call] ${tc.name}(${JSON.stringify(tc.args)})`);
959
+ return;
960
+ }
961
+ const rawToolCalls = aiMsg.additional_kwargs.tool_calls;
962
+ if (!Array.isArray(rawToolCalls)) return;
963
+ for (const tc of rawToolCalls) {
964
+ const fn = tc.function;
965
+ if (fn == null) continue;
966
+ textChunks.push(`AI: [tool_call] ${String(fn.name ?? "")}(${String(fn.arguments ?? "")})`);
967
+ }
919
968
  }
920
969
  /**
921
970
  * Ensures compatibility when switching from a non-thinking agent to a thinking-enabled agent.
@@ -1005,6 +1054,88 @@ function ensureThinkingBlockInMessages(messages, _provider, config, runStartInde
1005
1054
  }
1006
1055
  return result;
1007
1056
  }
1057
+ /** Whether a message carries tool content a tool-less agent cannot legally
1058
+ * send. Covers every representation a provider converter will serialize back
1059
+ * into a request: a ToolMessage, parsed `AIMessage.tool_calls`, raw
1060
+ * `additional_kwargs.tool_calls` (OpenAI keeps calls here when the parsed
1061
+ * array is empty), and `tool_use` / `tool_call` / `tool_result` content
1062
+ * blocks (`@langchain/aws` and the Anthropic converter map these to Converse
1063
+ * `toolUse` / `toolResult`). Missing the parent AI message is not just a
1064
+ * passthrough: folding its ToolMessage alone would leave an orphan
1065
+ * `assistant(tool_calls) -> user(...)` sequence. */
1066
+ function messageHasToolContent(msg) {
1067
+ if (isToolMessage(msg)) return true;
1068
+ const aiMsg = msg;
1069
+ if (aiMsg.tool_calls != null && aiMsg.tool_calls.length > 0) return true;
1070
+ const rawToolCalls = aiMsg.additional_kwargs.tool_calls;
1071
+ if (Array.isArray(rawToolCalls) && rawToolCalls.length > 0) return true;
1072
+ if (Array.isArray(msg.content)) {
1073
+ for (const block of msg.content) if (typeof block === "object" && (block.type === "tool_use" || block.type === "tool_call" || block.type === "tool_result")) return true;
1074
+ }
1075
+ return false;
1076
+ }
1077
+ /** Whether a message carries a tool RESULT: a ToolMessage, or a message whose
1078
+ * content includes a `tool_result` block (the shape when a call/result pair is
1079
+ * split as `AIMessage(tool_call)` + `HumanMessage(tool_result)`). Such a result
1080
+ * belongs with the preceding tool call, so it is absorbed into the same fold
1081
+ * and labelled as tool output. */
1082
+ function isToolResultMessage(msg) {
1083
+ if (isToolMessage(msg)) return true;
1084
+ if (Array.isArray(msg.content)) return msg.content.some((block) => typeof block === "object" && block.type === "tool_result");
1085
+ return false;
1086
+ }
1087
+ /**
1088
+ * Folds tool_use / tool_result content into plain text for an agent that binds
1089
+ * no tools.
1090
+ *
1091
+ * In a multi-agent graph, a tool-less destination still inherits the prior
1092
+ * agent's conversation history, which can contain toolUse/toolResult blocks.
1093
+ * Because it binds no tools, the model is invoked with no tool schema — and
1094
+ * Bedrock's Converse API rejects any request that carries toolUse/toolResult
1095
+ * blocks without a top-level toolConfig ("The toolConfig field must be defined
1096
+ * when using toolUse and toolResult content blocks"). Adding a dummy toolConfig
1097
+ * is not an option: AWS requires at least one tool, and it would expose a
1098
+ * capability the destination was intentionally denied.
1099
+ *
1100
+ * Each tool-call turn plus its trailing tool results (ToolMessages or
1101
+ * `tool_result` content blocks) is collapsed into a single `[Previous tool
1102
+ * interaction]` HumanMessage that preserves the tool name, arguments and result
1103
+ * as text (image blocks are kept as-is). Runs in a single pass: non-tool
1104
+ * messages pass through, `result` is allocated lazily on the first fold, and the
1105
+ * original array is returned unchanged when it holds no tool content (the common
1106
+ * fresh-tool-less-agent case).
1107
+ */
1108
+ function foldToolBlocksForToollessAgent(messages, config) {
1109
+ let result = null;
1110
+ let foldedCount = 0;
1111
+ let i = 0;
1112
+ while (i < messages.length) {
1113
+ const msg = messages[i];
1114
+ if (!messageHasToolContent(msg)) {
1115
+ result?.push(msg);
1116
+ i++;
1117
+ continue;
1118
+ }
1119
+ /** First fold — copy the untouched prefix once, then append from here. */
1120
+ if (result === null) result = messages.slice(0, i);
1121
+ const parts = [];
1122
+ const textChunks = ["[Previous tool interaction]"];
1123
+ appendMessageContent(msg, isToolResultMessage(msg) ? "Tool" : "AI", textChunks, parts);
1124
+ foldedCount++;
1125
+ let j = i + 1;
1126
+ while (j < messages.length && isToolResultMessage(messages[j])) {
1127
+ appendMessageContent(messages[j], "Tool", textChunks, parts);
1128
+ foldedCount++;
1129
+ j++;
1130
+ }
1131
+ flushTextChunks(textChunks, parts);
1132
+ result.push(withMessageRole(new _langchain_core_messages.HumanMessage({ content: require_langchain.toLangChainContent(parts) }), "user"));
1133
+ i = j;
1134
+ }
1135
+ if (result === null) return messages;
1136
+ require_events.emitAgentLog(config, "warn", "format", `foldToolBlocksForToollessAgent: folded ${foldedCount} tool message(s) into text for a tool-less agent`);
1137
+ return result;
1138
+ }
1008
1139
  /**
1009
1140
  * Walks backwards from `currentIndex` through the message array to check
1010
1141
  * whether an earlier AI message in the same "chain" (no HumanMessage boundary)
@@ -1032,6 +1163,7 @@ function chainHasThinkingBlock(messages, currentIndex) {
1032
1163
  }
1033
1164
  //#endregion
1034
1165
  exports.ensureThinkingBlockInMessages = ensureThinkingBlockInMessages;
1166
+ exports.foldToolBlocksForToollessAgent = foldToolBlocksForToollessAgent;
1035
1167
  exports.formatAgentMessages = formatAgentMessages;
1036
1168
  exports.formatFromLangChain = formatFromLangChain;
1037
1169
  exports.formatLangChainMessages = formatLangChainMessages;