@librechat/agents 3.3.8 → 3.3.10

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 (156) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +4 -0
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +66 -13
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +35 -2
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
  8. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
  9. package/dist/cjs/instrumentation.cjs +18 -48
  10. package/dist/cjs/instrumentation.cjs.map +1 -1
  11. package/dist/cjs/langfuse.cjs +174 -29
  12. package/dist/cjs/langfuse.cjs.map +1 -1
  13. package/dist/cjs/langfuseConfig.cjs +12 -0
  14. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  15. package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
  16. package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
  17. package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
  18. package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
  19. package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
  20. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
  21. package/dist/cjs/langfuseTraceShaping.cjs +121 -4
  22. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  23. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
  24. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  25. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
  26. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  27. package/dist/cjs/llm/init.cjs +3 -3
  28. package/dist/cjs/llm/invoke.cjs +5 -5
  29. package/dist/cjs/llm/openai/index.cjs +1 -1
  30. package/dist/cjs/main.cjs +10 -10
  31. package/dist/cjs/messages/prune.cjs +13 -1
  32. package/dist/cjs/messages/prune.cjs.map +1 -1
  33. package/dist/cjs/prompts/activityLabel.cjs +24 -12
  34. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  35. package/dist/cjs/run.cjs +57 -22
  36. package/dist/cjs/run.cjs.map +1 -1
  37. package/dist/cjs/session/messageSerialization.cjs +6 -0
  38. package/dist/cjs/session/messageSerialization.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +21 -10
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/summarization/node.cjs +5 -0
  42. package/dist/cjs/summarization/node.cjs.map +1 -1
  43. package/dist/cjs/tools/ToolNode.cjs +253 -24
  44. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  45. package/dist/cjs/tools/handlers.cjs +1 -1
  46. package/dist/cjs/tools/search/tool.cjs +1 -1
  47. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  48. package/dist/cjs/utils/index.cjs +2 -2
  49. package/dist/esm/agents/AgentContext.mjs +4 -0
  50. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  51. package/dist/esm/graphs/Graph.mjs +67 -14
  52. package/dist/esm/graphs/Graph.mjs.map +1 -1
  53. package/dist/esm/graphs/MultiAgentGraph.mjs +35 -2
  54. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  55. package/dist/esm/hitl/askUserQuestion.mjs +3 -2
  56. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
  57. package/dist/esm/instrumentation.mjs +18 -48
  58. package/dist/esm/instrumentation.mjs.map +1 -1
  59. package/dist/esm/langfuse.mjs +176 -28
  60. package/dist/esm/langfuse.mjs.map +1 -1
  61. package/dist/esm/langfuseConfig.mjs +10 -1
  62. package/dist/esm/langfuseConfig.mjs.map +1 -1
  63. package/dist/esm/langfuseRuntimeContext.mjs +21 -3
  64. package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
  65. package/dist/esm/langfuseRuntimeScope.mjs +39 -10
  66. package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
  67. package/dist/esm/langfuseSpanRegistry.mjs +91 -0
  68. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
  69. package/dist/esm/langfuseTraceShaping.mjs +121 -4
  70. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  71. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
  72. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  73. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
  74. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  75. package/dist/esm/llm/init.mjs +2 -2
  76. package/dist/esm/llm/invoke.mjs +5 -5
  77. package/dist/esm/llm/openai/index.mjs +1 -1
  78. package/dist/esm/main.mjs +8 -8
  79. package/dist/esm/messages/prune.mjs +13 -1
  80. package/dist/esm/messages/prune.mjs.map +1 -1
  81. package/dist/esm/prompts/activityLabel.mjs +24 -12
  82. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  83. package/dist/esm/run.mjs +57 -22
  84. package/dist/esm/run.mjs.map +1 -1
  85. package/dist/esm/session/messageSerialization.mjs +6 -0
  86. package/dist/esm/session/messageSerialization.mjs.map +1 -1
  87. package/dist/esm/stream.mjs +21 -10
  88. package/dist/esm/stream.mjs.map +1 -1
  89. package/dist/esm/summarization/node.mjs +5 -0
  90. package/dist/esm/summarization/node.mjs.map +1 -1
  91. package/dist/esm/tools/ToolNode.mjs +254 -25
  92. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  93. package/dist/esm/tools/handlers.mjs +1 -1
  94. package/dist/esm/tools/search/tool.mjs +1 -1
  95. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  96. package/dist/esm/utils/index.mjs +2 -2
  97. package/dist/types/agents/AgentContext.d.ts +2 -0
  98. package/dist/types/graphs/Graph.d.ts +24 -0
  99. package/dist/types/hitl/askUserQuestion.d.ts +11 -1
  100. package/dist/types/langfuse.d.ts +16 -8
  101. package/dist/types/langfuseConfig.d.ts +6 -0
  102. package/dist/types/langfuseRuntimeContext.d.ts +27 -1
  103. package/dist/types/langfuseRuntimeScope.d.ts +17 -2
  104. package/dist/types/langfuseSpanRegistry.d.ts +17 -0
  105. package/dist/types/langfuseTraceShaping.d.ts +2 -1
  106. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
  107. package/dist/types/run.d.ts +7 -0
  108. package/dist/types/session/types.d.ts +1 -0
  109. package/dist/types/tools/ToolNode.d.ts +7 -1
  110. package/dist/types/types/hitl.d.ts +8 -0
  111. package/dist/types/types/tools.d.ts +30 -0
  112. package/package.json +7 -4
  113. package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
  114. package/src/agents/AgentContext.ts +5 -0
  115. package/src/graphs/Graph.ts +108 -20
  116. package/src/graphs/MultiAgentGraph.ts +56 -2
  117. package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
  118. package/src/hitl/askUserQuestion.ts +14 -1
  119. package/src/instrumentation.ts +35 -77
  120. package/src/langfuse.ts +320 -43
  121. package/src/langfuseConfig.ts +24 -0
  122. package/src/langfuseRuntimeContext.ts +43 -1
  123. package/src/langfuseRuntimeScope.ts +94 -21
  124. package/src/langfuseSpanRegistry.ts +131 -0
  125. package/src/langfuseTraceShaping.ts +194 -7
  126. package/src/llm/anthropic/utils/message_inputs.ts +70 -19
  127. package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
  128. package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
  129. package/src/llm/bedrock/utils/message_inputs.ts +32 -7
  130. package/src/messages/prune.ts +12 -1
  131. package/src/prompts/activityLabel.ts +23 -6
  132. package/src/run.ts +91 -45
  133. package/src/scripts/activity-labels/captured.json +56 -0
  134. package/src/scripts/activity-labels/checks.cjs +205 -0
  135. package/src/scripts/activity-labels/corpus.cjs +473 -0
  136. package/src/scripts/activity-labels/report.cjs +203 -0
  137. package/src/scripts/activity-labels/rescore.cjs +102 -0
  138. package/src/scripts/activity-labels/run.ts +705 -0
  139. package/src/scripts/activity-labels/variants.ts +71 -0
  140. package/src/session/messageSerialization.ts +12 -1
  141. package/src/session/types.ts +1 -0
  142. package/src/specs/activity-label-prompt.test.ts +26 -10
  143. package/src/specs/agent-handoffs.test.ts +306 -0
  144. package/src/specs/discovered-tools.test.ts +217 -0
  145. package/src/specs/langfuse-callbacks.test.ts +456 -0
  146. package/src/specs/langfuse-routing.integration.test.ts +138 -1
  147. package/src/specs/langfuse-span-registry.test.ts +70 -0
  148. package/src/specs/langfuse-trace-shaping.test.ts +294 -0
  149. package/src/specs/prune.test.ts +38 -1
  150. package/src/stream.ts +70 -6
  151. package/src/summarization/node.ts +5 -0
  152. package/src/tools/ToolNode.ts +400 -9
  153. package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
  154. package/src/tools/__tests__/hitl.test.ts +58 -0
  155. package/src/types/hitl.ts +8 -0
  156. package/src/types/tools.ts +35 -1
@@ -31,8 +31,8 @@ function truncateForLabel(value, maxLength) {
31
31
  * Reduces a committed label to bounded single-line data.
32
32
  *
33
33
  * Sections in this prompt are delimited by blank lines, so a label carrying
34
- * embedded newlines could otherwise forge an apparent `Tool calls:` or
35
- * `Label:` section. Unlike every other input here, previous labels re-enter
34
+ * embedded newlines could otherwise forge an apparent entries section or
35
+ * `Header:` cue. Unlike every other input here, previous labels re-enter
36
36
  * the prompt on EVERY later batch, so one malformed result — plain model
37
37
  * noncompliance, or injection surfacing through a tool result — would
38
38
  * persistently steer unrelated later labels rather than affecting one. The
@@ -109,17 +109,29 @@ function buildActivityLabelPrompt({ entries, charLimit, thinkingExcerpts, lastAs
109
109
  if (entries.length > 0) {
110
110
  const shown = entries.slice(0, MAX_PROMPT_ENTRIES);
111
111
  const omitted = entries.length - shown.length;
112
- sections.push("Tool calls:\n" + shown.map((entry) => {
113
- const input = clip(serializeForLabel(entry.toolInput, charLimit), charLimit);
114
- const redacted = redaction != null && shouldRedactTool(entry.toolName, redaction);
115
- let outcome;
116
- if (redacted) outcome = redaction.redactionText;
117
- else if (entry.status === "error") outcome = `ERROR: ${clip(entry.error ?? "unknown error", charLimit)}`;
118
- else outcome = clip(serializeForLabel(entry.toolOutput, charLimit), charLimit);
119
- return `- ${entry.toolName}(${input}) → ${outcome}`;
120
- }).join("\n") + (omitted > 0 ? `\n- …and ${omitted} more tool ${omitted === 1 ? "call" : "calls"}` : ""));
112
+ sections.push(
113
+ /** Frames the list as reference material, not the thing to
114
+ * transcribe. Ported from LibreChat's fallback builder (its
115
+ * runtime.ts documents that without this the model "hands back a
116
+ * transcription" of the list) after the eval harness measured it
117
+ * across three independent sweeps: fewer template-redundancy and
118
+ * length violations than a bare `Tool calls:` heading, with no
119
+ * per-case regressions (agents #360). */
120
+ "What it called, and what came back (do not restate these):\n" + shown.map((entry) => {
121
+ const input = clip(serializeForLabel(entry.toolInput, charLimit), charLimit);
122
+ const redacted = redaction != null && shouldRedactTool(entry.toolName, redaction);
123
+ let outcome;
124
+ if (redacted) outcome = redaction.redactionText;
125
+ else if (entry.status === "error") outcome = `ERROR: ${clip(entry.error ?? "unknown error", charLimit)}`;
126
+ else outcome = clip(serializeForLabel(entry.toolOutput, charLimit), charLimit);
127
+ return `- ${entry.toolName}(${input}) → ${outcome}`;
128
+ }).join("\n") + (omitted > 0 ? `\n- …and ${omitted} more tool ${omitted === 1 ? "call" : "calls"}` : "")
129
+ );
121
130
  }
122
- sections.push("Label:");
131
+ /** The fallback builder's terminal cue, measured alongside the heading
132
+ * (same sweeps). The default system prompt already describes the
133
+ * output as "the header of a collapsed activity group". */
134
+ sections.push("Header:");
123
135
  return sections.join("\n\n");
124
136
  }
125
137
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"activityLabel.mjs","names":[],"sources":["../../../src/prompts/activityLabel.ts"],"sourcesContent":["import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';\nimport type { ActivityLabelToolEntry } from '@/types/activityLabel';\nimport { shouldRedactTool } from '@/langfuseToolOutputTracing';\n\n/**\n * Default system prompt for fast-model activity labeling.\n *\n * Style synthesized from Claude Code's tool-use summary prompt (git-subject\n * register, past tense, distinctive nouns) and claude.ai's observed group\n * headers (5–9 words describing a mixed reasoning + tool block, e.g.\n * \"Synthesized version data and curated comparative framework\").\n */\nexport const ACTIVITY_LABEL_PROMPT = `Write a short label describing what this block of agent activity accomplished. It appears as the header of a collapsed activity group in a chat UI.\n\nRules:\n- 5 to 9 words, past-tense verb first\n- Name the most distinctive subject (file, API, topic); drop articles and filler\n- Describe outcomes, not mechanics; if something failed, say so plainly\n- Output only the label — no quotes, no punctuation at the end, no preamble\n\nExamples:\n- Searched Node.js release notes and changelogs\n- Compared runtime versions across official sources\n- Fixed failing auth middleware tests\n- Read project config and dependency manifests\n- Attempted database migration, hit permission errors`;\n\n/** Truncates a serialized value for the label prompt. */\nexport function truncateForLabel(value: string, maxLength: number): string {\n if (value.length <= maxLength) {\n return value;\n }\n return value.slice(0, Math.max(0, maxLength - 1)) + '…';\n}\n\n/**\n * Reduces a committed label to bounded single-line data.\n *\n * Sections in this prompt are delimited by blank lines, so a label carrying\n * embedded newlines could otherwise forge an apparent `Tool calls:` or\n * `Label:` section. Unlike every other input here, previous labels re-enter\n * the prompt on EVERY later batch, so one malformed result — plain model\n * noncompliance, or injection surfacing through a tool result — would\n * persistently steer unrelated later labels rather than affecting one. The\n * clip bounds the same way `lastAssistantText` and reasoning excerpts are\n * bounded: oversized headers must not inflate later requests past the fast\n * model's window and starve the run of labels entirely.\n */\nfunction sanitizePreviousLabel(label: string): string {\n return truncateForLabel(label.replace(/\\s+/g, ' ').trim(), PREVIOUS_LABEL_LIMIT);\n}\n\nconst ABORT_SERIALIZATION = Symbol('abort-label-serialization');\n\n/**\n * Serializes a tool value for the prompt WITHOUT materializing huge JSON:\n * the output is clipped to a few hundred characters anyway, so a multi-\n * megabyte tool result must not be stringified in full on the label path.\n * Strings clip immediately; structured values serialize under a character\n * budget and degrade to a shape summary once it is exhausted.\n */\nfunction serializeForLabel(value: unknown, limit: number): string {\n if (value == null) {\n return '';\n }\n if (typeof value === 'string') {\n return value.length > limit ? value.slice(0, limit + 1) : value;\n }\n let budget = limit * 4;\n try {\n return (\n JSON.stringify(value, (_key, nested: unknown) => {\n if (budget <= 0) {\n throw ABORT_SERIALIZATION;\n }\n if (typeof nested === 'string') {\n const clipped =\n nested.length > limit ? nested.slice(0, limit) : nested;\n budget -= clipped.length;\n return clipped;\n }\n budget -= 8;\n return nested;\n }) ?? ''\n );\n } catch (error) {\n if (error === ABORT_SERIALIZATION) {\n return Array.isArray(value) ? `[Array(${value.length})]` : '[Object]';\n }\n return String(value);\n }\n}\n\nconst INPUT_CONTEXT_LIMIT = 200;\nconst MAX_THINKING_EXCERPTS = 4;\nconst MAX_PREVIOUS_LABELS = 3;\n/** Per-label bound. A header is 5-9 words; anything past this is\n * noncompliance or payload, and previous labels are the one input that\n * RE-ENTERS the prompt on every later batch of the run. */\nconst PREVIOUS_LABEL_LIMIT = 200;\n/** A label is 5-9 words; no batch needs more than this many entries to\n * produce one, and the cap keeps a 200-call programmatic batch from\n * building an enormous prompt out of per-field-bounded pieces. */\nconst MAX_PROMPT_ENTRIES = 12;\n\nexport type BuildActivityLabelPromptParams = {\n entries: ActivityLabelToolEntry[];\n charLimit: number;\n thinkingExcerpts?: string[];\n lastAssistantText?: string;\n /**\n * Headers already committed for earlier batches in this run, in run order\n * with the most recent last. Rendered ahead of the block context so the\n * label continues the run's story instead of restating a line the user is\n * already reading. Capped at {@link MAX_PREVIOUS_LABELS}.\n */\n previousLabels?: string[];\n /**\n * Resolved tool-output tracing policy. The label prompt becomes Langfuse\n * generation input, so outputs/errors excluded from tracing (global\n * disable or `redactedToolNames`) must never appear in it — the same\n * redaction the span processor applies to structured tool observations.\n */\n redaction?: ResolvedLangfuseToolOutputTracingConfig;\n};\n\n/**\n * Builds the user prompt for a fast-model activity label. Pure — exported\n * for direct testing of redaction and truncation behavior.\n */\nexport function buildActivityLabelPrompt({\n entries,\n charLimit,\n thinkingExcerpts,\n lastAssistantText,\n previousLabels,\n redaction,\n}: BuildActivityLabelPromptParams): string {\n const clip = truncateForLabel;\n /** Reasoning and intent text can quote tool output verbatim — including\n * output from EARLIER calls to a redacted tool that this batch does not\n * contain — so any active policy (global disable or a configured\n * redacted-name list) drops both wholesale. There is no reliable way to\n * scrub a quoted fragment out of free-form model prose. */\n const excerptsRedacted =\n redaction != null &&\n (redaction.enabled === false || redaction.redactedToolNames.size > 0);\n const sections: string[] = [];\n /** Previous labels are free-form model prose too, and per-agent overlays\n * mean an earlier header may have been generated under ANOTHER agent's\n * weaker policy — so they share the excerpts' wholesale drop rather than\n * letting a handoff leak a looser agent's phrasing into this trace. */\n if (!excerptsRedacted && previousLabels != null && previousLabels.length > 0) {\n const recent = previousLabels\n .slice(-MAX_PREVIOUS_LABELS)\n .map(sanitizePreviousLabel)\n /** A label that sanitizes to nothing carries no story to continue;\n * rendering it would leave a bare bullet implying a missing header. */\n .filter((label) => label.length > 0);\n if (recent.length > 0) {\n sections.push(\n 'Previous headers in this run (most recent last):\\n' +\n recent.map((label) => `- ${label}`).join('\\n')\n );\n }\n }\n /** Intent text is free-form assistant prose that can quote a redacted\n * tool result just as reasoning can, so it shares the excerpts' fate. */\n if (\n !excerptsRedacted &&\n lastAssistantText != null &&\n lastAssistantText.length > 0\n ) {\n sections.push(\n `Intent (assistant's last message): ${clip(lastAssistantText, INPUT_CONTEXT_LIMIT)}`\n );\n }\n if (\n !excerptsRedacted &&\n thinkingExcerpts != null &&\n thinkingExcerpts.length > 0\n ) {\n sections.push(\n 'Reasoning excerpts:\\n' +\n thinkingExcerpts\n .slice(0, MAX_THINKING_EXCERPTS)\n .map((excerpt) => `- ${clip(excerpt, charLimit)}`)\n .join('\\n')\n );\n }\n if (entries.length > 0) {\n const shown = entries.slice(0, MAX_PROMPT_ENTRIES);\n const omitted = entries.length - shown.length;\n sections.push(\n 'Tool calls:\\n' +\n shown\n .map((entry) => {\n const input = clip(\n serializeForLabel(entry.toolInput, charLimit),\n charLimit\n );\n const redacted =\n redaction != null && shouldRedactTool(entry.toolName, redaction);\n let outcome: string;\n if (redacted) {\n outcome = redaction.redactionText;\n } else if (entry.status === 'error') {\n outcome = `ERROR: ${clip(entry.error ?? 'unknown error', charLimit)}`;\n } else {\n outcome = clip(\n serializeForLabel(entry.toolOutput, charLimit),\n charLimit\n );\n }\n return `- ${entry.toolName}(${input}) → ${outcome}`;\n })\n .join('\\n') +\n (omitted > 0\n ? `\\n- …and ${omitted} more tool ${omitted === 1 ? 'call' : 'calls'}`\n : '')\n );\n }\n sections.push('Label:');\n return sections.join('\\n\\n');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"activityLabel.mjs","names":[],"sources":["../../../src/prompts/activityLabel.ts"],"sourcesContent":["import type { ResolvedLangfuseToolOutputTracingConfig } from '@/langfuseRuntimeContext';\nimport type { ActivityLabelToolEntry } from '@/types/activityLabel';\nimport { shouldRedactTool } from '@/langfuseToolOutputTracing';\n\n/**\n * Default system prompt for fast-model activity labeling.\n *\n * Style synthesized from Claude Code's tool-use summary prompt (git-subject\n * register, past tense, distinctive nouns) and claude.ai's observed group\n * headers (5–9 words describing a mixed reasoning + tool block, e.g.\n * \"Synthesized version data and curated comparative framework\").\n */\nexport const ACTIVITY_LABEL_PROMPT = `Write a short label describing what this block of agent activity accomplished. It appears as the header of a collapsed activity group in a chat UI.\n\nRules:\n- 5 to 9 words, past-tense verb first\n- Name the most distinctive subject (file, API, topic); drop articles and filler\n- Describe outcomes, not mechanics; if something failed, say so plainly\n- Output only the label — no quotes, no punctuation at the end, no preamble\n\nExamples:\n- Searched Node.js release notes and changelogs\n- Compared runtime versions across official sources\n- Fixed failing auth middleware tests\n- Read project config and dependency manifests\n- Attempted database migration, hit permission errors`;\n\n/** Truncates a serialized value for the label prompt. */\nexport function truncateForLabel(value: string, maxLength: number): string {\n if (value.length <= maxLength) {\n return value;\n }\n return value.slice(0, Math.max(0, maxLength - 1)) + '…';\n}\n\n/**\n * Reduces a committed label to bounded single-line data.\n *\n * Sections in this prompt are delimited by blank lines, so a label carrying\n * embedded newlines could otherwise forge an apparent entries section or\n * `Header:` cue. Unlike every other input here, previous labels re-enter\n * the prompt on EVERY later batch, so one malformed result — plain model\n * noncompliance, or injection surfacing through a tool result — would\n * persistently steer unrelated later labels rather than affecting one. The\n * clip bounds the same way `lastAssistantText` and reasoning excerpts are\n * bounded: oversized headers must not inflate later requests past the fast\n * model's window and starve the run of labels entirely.\n */\nfunction sanitizePreviousLabel(label: string): string {\n return truncateForLabel(\n label.replace(/\\s+/g, ' ').trim(),\n PREVIOUS_LABEL_LIMIT\n );\n}\n\nconst ABORT_SERIALIZATION = Symbol('abort-label-serialization');\n\n/**\n * Serializes a tool value for the prompt WITHOUT materializing huge JSON:\n * the output is clipped to a few hundred characters anyway, so a multi-\n * megabyte tool result must not be stringified in full on the label path.\n * Strings clip immediately; structured values serialize under a character\n * budget and degrade to a shape summary once it is exhausted.\n */\nfunction serializeForLabel(value: unknown, limit: number): string {\n if (value == null) {\n return '';\n }\n if (typeof value === 'string') {\n return value.length > limit ? value.slice(0, limit + 1) : value;\n }\n let budget = limit * 4;\n try {\n return (\n JSON.stringify(value, (_key, nested: unknown) => {\n if (budget <= 0) {\n throw ABORT_SERIALIZATION;\n }\n if (typeof nested === 'string') {\n const clipped =\n nested.length > limit ? nested.slice(0, limit) : nested;\n budget -= clipped.length;\n return clipped;\n }\n budget -= 8;\n return nested;\n }) ?? ''\n );\n } catch (error) {\n if (error === ABORT_SERIALIZATION) {\n return Array.isArray(value) ? `[Array(${value.length})]` : '[Object]';\n }\n return String(value);\n }\n}\n\nconst INPUT_CONTEXT_LIMIT = 200;\nconst MAX_THINKING_EXCERPTS = 4;\nconst MAX_PREVIOUS_LABELS = 3;\n/** Per-label bound. A header is 5-9 words; anything past this is\n * noncompliance or payload, and previous labels are the one input that\n * RE-ENTERS the prompt on every later batch of the run. */\nconst PREVIOUS_LABEL_LIMIT = 200;\n/** A label is 5-9 words; no batch needs more than this many entries to\n * produce one, and the cap keeps a 200-call programmatic batch from\n * building an enormous prompt out of per-field-bounded pieces. */\nconst MAX_PROMPT_ENTRIES = 12;\n\nexport type BuildActivityLabelPromptParams = {\n entries: ActivityLabelToolEntry[];\n charLimit: number;\n thinkingExcerpts?: string[];\n lastAssistantText?: string;\n /**\n * Headers already committed for earlier batches in this run, in run order\n * with the most recent last. Rendered ahead of the block context so the\n * label continues the run's story instead of restating a line the user is\n * already reading. Capped at {@link MAX_PREVIOUS_LABELS}.\n */\n previousLabels?: string[];\n /**\n * Resolved tool-output tracing policy. The label prompt becomes Langfuse\n * generation input, so outputs/errors excluded from tracing (global\n * disable or `redactedToolNames`) must never appear in it — the same\n * redaction the span processor applies to structured tool observations.\n */\n redaction?: ResolvedLangfuseToolOutputTracingConfig;\n};\n\n/**\n * Builds the user prompt for a fast-model activity label. Pure — exported\n * for direct testing of redaction and truncation behavior.\n */\nexport function buildActivityLabelPrompt({\n entries,\n charLimit,\n thinkingExcerpts,\n lastAssistantText,\n previousLabels,\n redaction,\n}: BuildActivityLabelPromptParams): string {\n const clip = truncateForLabel;\n /** Reasoning and intent text can quote tool output verbatim — including\n * output from EARLIER calls to a redacted tool that this batch does not\n * contain — so any active policy (global disable or a configured\n * redacted-name list) drops both wholesale. There is no reliable way to\n * scrub a quoted fragment out of free-form model prose. */\n const excerptsRedacted =\n redaction != null &&\n (redaction.enabled === false || redaction.redactedToolNames.size > 0);\n const sections: string[] = [];\n /** Previous labels are free-form model prose too, and per-agent overlays\n * mean an earlier header may have been generated under ANOTHER agent's\n * weaker policy — so they share the excerpts' wholesale drop rather than\n * letting a handoff leak a looser agent's phrasing into this trace. */\n if (\n !excerptsRedacted &&\n previousLabels != null &&\n previousLabels.length > 0\n ) {\n const recent = previousLabels\n .slice(-MAX_PREVIOUS_LABELS)\n .map(sanitizePreviousLabel)\n /** A label that sanitizes to nothing carries no story to continue;\n * rendering it would leave a bare bullet implying a missing header. */\n .filter((label) => label.length > 0);\n if (recent.length > 0) {\n sections.push(\n 'Previous headers in this run (most recent last):\\n' +\n recent.map((label) => `- ${label}`).join('\\n')\n );\n }\n }\n /** Intent text is free-form assistant prose that can quote a redacted\n * tool result just as reasoning can, so it shares the excerpts' fate. */\n if (\n !excerptsRedacted &&\n lastAssistantText != null &&\n lastAssistantText.length > 0\n ) {\n sections.push(\n `Intent (assistant's last message): ${clip(lastAssistantText, INPUT_CONTEXT_LIMIT)}`\n );\n }\n if (\n !excerptsRedacted &&\n thinkingExcerpts != null &&\n thinkingExcerpts.length > 0\n ) {\n sections.push(\n 'Reasoning excerpts:\\n' +\n thinkingExcerpts\n .slice(0, MAX_THINKING_EXCERPTS)\n .map((excerpt) => `- ${clip(excerpt, charLimit)}`)\n .join('\\n')\n );\n }\n if (entries.length > 0) {\n const shown = entries.slice(0, MAX_PROMPT_ENTRIES);\n const omitted = entries.length - shown.length;\n sections.push(\n /** Frames the list as reference material, not the thing to\n * transcribe. Ported from LibreChat's fallback builder (its\n * runtime.ts documents that without this the model \"hands back a\n * transcription\" of the list) after the eval harness measured it\n * across three independent sweeps: fewer template-redundancy and\n * length violations than a bare `Tool calls:` heading, with no\n * per-case regressions (agents #360). */\n 'What it called, and what came back (do not restate these):\\n' +\n shown\n .map((entry) => {\n const input = clip(\n serializeForLabel(entry.toolInput, charLimit),\n charLimit\n );\n const redacted =\n redaction != null && shouldRedactTool(entry.toolName, redaction);\n let outcome: string;\n if (redacted) {\n outcome = redaction.redactionText;\n } else if (entry.status === 'error') {\n outcome = `ERROR: ${clip(entry.error ?? 'unknown error', charLimit)}`;\n } else {\n outcome = clip(\n serializeForLabel(entry.toolOutput, charLimit),\n charLimit\n );\n }\n return `- ${entry.toolName}(${input}) → ${outcome}`;\n })\n .join('\\n') +\n (omitted > 0\n ? `\\n- …and ${omitted} more tool ${omitted === 1 ? 'call' : 'calls'}`\n : '')\n );\n }\n /** The fallback builder's terminal cue, measured alongside the heading\n * (same sweeps). The default system prompt already describes the\n * output as \"the header of a collapsed activity group\". */\n sections.push('Header:');\n return sections.join('\\n\\n');\n}\n"],"mappings":"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"}
package/dist/esm/run.mjs CHANGED
@@ -2,15 +2,15 @@ import { getTraceIdSeed } from "./langfuseRuntimeContext.mjs";
2
2
  import { hasToolOutputTracingConfig, resolveLangfuseConfig, resolveToolOutputTracingConfig } from "./langfuseConfig.mjs";
3
3
  import { resolveLangfuseRuntimeScope, withLangfuseRuntimeScope } from "./langfuseRuntimeScope.mjs";
4
4
  import { createLangfuseHandler, createLangfuseTraceMetadata, disposeLangfuseHandler, getLangfuseTraceName, isLangfuseCallbackHandler, withLangfuseAttributes } from "./langfuse.mjs";
5
- import { ACTIVITY_LABEL_PROMPT, buildActivityLabelPrompt } from "./prompts/activityLabel.mjs";
6
- import { appendCallbacks, findCallback } from "./utils/callbacks.mjs";
7
5
  import "./common/constants.mjs";
8
6
  import "./common/enum.mjs";
9
7
  import "./common/index.mjs";
8
+ import { ACTIVITY_LABEL_PROMPT, buildActivityLabelPrompt } from "./prompts/activityLabel.mjs";
9
+ import { appendCallbacks, findCallback } from "./utils/callbacks.mjs";
10
10
  import { createCompletionTitleRunnable, createTitleRunnable } from "./utils/title.mjs";
11
11
  import { createTokenCounter, encodingForModel } from "./utils/tokens.mjs";
12
- import { resolveMaxSeals } from "./llm/preempt.mjs";
13
12
  import { initializeLangfuseTracing } from "./instrumentation.mjs";
13
+ import { resolveMaxSeals } from "./llm/preempt.mjs";
14
14
  import { isOpenAILike } from "./utils/llm.mjs";
15
15
  import { HandlerRegistry } from "./events.mjs";
16
16
  import { initializeModel } from "./llm/init.mjs";
@@ -18,6 +18,7 @@ import { executeHooks } from "./hooks/executeHooks.mjs";
18
18
  import "./hooks/index.mjs";
19
19
  import { StandardGraph } from "./graphs/Graph.mjs";
20
20
  import { MultiAgentGraph } from "./graphs/MultiAgentGraph.mjs";
21
+ import { nanoid } from "nanoid";
21
22
  import { PromptTemplate } from "@langchain/core/prompts";
22
23
  import { RunnableLambda } from "@langchain/core/runnables";
23
24
  import { AzureChatOpenAI, ChatOpenAI } from "@langchain/openai";
@@ -376,6 +377,16 @@ var Run = class Run {
376
377
  return this.Graph.getRunMessages();
377
378
  }
378
379
  /**
380
+ * Returns a defensive snapshot of tools discovered by the current run.
381
+ * Pass an agent id for that context, or omit it for the ordered union across
382
+ * contexts. Interrupted state is available immediately for host persistence;
383
+ * completed runs retain their final snapshot through graph cleanup.
384
+ */
385
+ getDiscoveredTools(agentId) {
386
+ if (!this.Graph) throw new Error("Graph not initialized. Make sure to use Run.create() to instantiate the Run.");
387
+ return this.Graph.getDiscoveredTools(agentId);
388
+ }
389
+ /**
379
390
  * Returns the current calibration ratio (EMA of provider-vs-estimate token ratios).
380
391
  * Hosts should persist this value and pass it back as `RunConfig.calibrationRatio`
381
392
  * on the next run for the same conversation so the pruner starts with an accurate
@@ -540,13 +551,22 @@ var Run = class Run {
540
551
  const traceName = config.runName ?? getLangfuseTraceName(traceMetadata);
541
552
  const streamLangfuseConfig = this.getStreamLangfuseConfig(graph);
542
553
  initializeLangfuseTracing(streamLangfuseConfig);
554
+ const streamRuntimeScope = resolveLangfuseRuntimeScope({
555
+ runLangfuse: streamLangfuseConfig,
556
+ langfuseOverlay: this.getStreamToolOutputTracingLangfuseConfig(graph),
557
+ traceIdSeed: streamLangfuseConfig?.deterministicTraceId === true ? this.id : void 0,
558
+ runId: graph.langfuseScopeRunId
559
+ });
543
560
  const langfuseHandler = createLangfuseHandler({
544
561
  langfuse: streamLangfuseConfig,
545
562
  userId,
546
563
  sessionId,
547
564
  traceMetadata,
548
565
  tags: ["librechat", "agent"],
549
- traceIdSeed: streamLangfuseConfig?.deterministicTraceId === true ? this.id : void 0
566
+ traceIdSeed: streamLangfuseConfig?.deterministicTraceId === true ? this.id : void 0,
567
+ runId: graph.langfuseScopeRunId,
568
+ toolOutputTracing: streamRuntimeScope.toolOutputTracing,
569
+ traceName
550
570
  });
551
571
  if (langfuseHandler != null) {
552
572
  config.runName = traceName;
@@ -710,11 +730,7 @@ var Run = class Run {
710
730
  else if (this._haltedReason == null && graph.preemptIncomplete) this._haltedReason = "preempt_incomplete";
711
731
  };
712
732
  try {
713
- await withLangfuseRuntimeScope(resolveLangfuseRuntimeScope({
714
- runLangfuse: streamLangfuseConfig,
715
- langfuseOverlay: this.getStreamToolOutputTracingLangfuseConfig(graph),
716
- traceIdSeed: streamLangfuseConfig?.deterministicTraceId === true ? this.id : void 0
717
- }), () => withLangfuseAttributes({
733
+ await withLangfuseRuntimeScope(streamRuntimeScope, () => withLangfuseAttributes({
718
734
  langfuse: streamLangfuseConfig,
719
735
  userId,
720
736
  sessionId,
@@ -967,19 +983,35 @@ var Run = class Run {
967
983
  }
968
984
  async generateTitle({ provider, inputText, contentParts, titlePrompt, clientOptions, chainOptions, skipLanguage, titleMethod = "completion", titlePromptTemplate }) {
969
985
  let titleLangfuseHandler;
970
- let titleLangfuseConfig;
971
986
  let titleUserId;
972
987
  let titleSessionId;
973
988
  const titleContext = this.Graph == null ? void 0 : this.Graph.agentContexts.get(this.Graph.defaultAgentId);
989
+ const titleLangfuseConfig = resolveLangfuseConfig(this.langfuse, titleContext?.langfuse);
974
990
  const traceMetadata = createLangfuseTraceMetadata({
975
991
  messageId: "title-" + this.id,
976
992
  agentName: titleContext?.name
977
993
  });
978
994
  const titleRunName = getLangfuseTraceName(traceMetadata, "LibreChat Title");
995
+ /** Scope identity carries an opaque per-execution component: public run
996
+ * ids are unrestricted, so a purely derived id (`title-<runId>`) could
997
+ * collide with an ordinary concurrent run literally named that way and
998
+ * defeat foreign-scope rejection. */
999
+ const titleScopeRunId = `title:${this.id}:${nanoid()}`;
1000
+ /** Seed policy mirrors `generateActivityLabel`:
1001
+ * `runWithLangfuseRuntimeContext` SPREADS the surrounding context, so an
1002
+ * absent seed INHERITS an active parent run's and collapses the title
1003
+ * into that run's trace. Seeded when determinism is opted into OR a
1004
+ * parent seed is live; otherwise unseeded, matching the other paths. */
1005
+ const inheritedTraceSeed = getTraceIdSeed();
1006
+ const titleRuntimeScope = resolveLangfuseRuntimeScope({
1007
+ runLangfuse: this.langfuse,
1008
+ langfuseOverlay: titleContext?.langfuse,
1009
+ traceIdSeed: titleLangfuseConfig?.deterministicTraceId === true || inheritedTraceSeed != null ? "title-" + this.id : void 0,
1010
+ runId: titleScopeRunId
1011
+ });
979
1012
  if (chainOptions != null) {
980
1013
  titleUserId = typeof chainOptions.configurable?.user_id === "string" ? chainOptions.configurable.user_id : void 0;
981
1014
  titleSessionId = typeof chainOptions.configurable?.thread_id === "string" ? chainOptions.configurable.thread_id : void 0;
982
- titleLangfuseConfig = resolveLangfuseConfig(this.langfuse, titleContext?.langfuse);
983
1015
  initializeLangfuseTracing(titleLangfuseConfig);
984
1016
  titleLangfuseHandler = createLangfuseHandler({
985
1017
  langfuse: titleLangfuseConfig,
@@ -987,7 +1019,10 @@ var Run = class Run {
987
1019
  sessionId: titleSessionId,
988
1020
  traceMetadata,
989
1021
  tags: ["librechat", "title"],
990
- traceIdSeed: titleLangfuseConfig?.deterministicTraceId === true ? "title-" + this.id : void 0
1022
+ traceIdSeed: titleLangfuseConfig?.deterministicTraceId === true ? "title-" + this.id : void 0,
1023
+ runId: titleScopeRunId,
1024
+ toolOutputTracing: titleRuntimeScope.toolOutputTracing,
1025
+ traceName: chainOptions.runName ?? titleRunName
991
1026
  });
992
1027
  if (titleLangfuseHandler != null) chainOptions.callbacks = appendCallbacks(chainOptions.callbacks, [titleLangfuseHandler]);
993
1028
  }
@@ -1033,18 +1068,12 @@ var Run = class Run {
1033
1068
  }, runtimeConfig));
1034
1069
  try {
1035
1070
  try {
1036
- return await withLangfuseRuntimeScope(resolveLangfuseRuntimeScope({
1037
- runLangfuse: this.langfuse,
1038
- langfuseOverlay: titleContext?.langfuse
1039
- }), () => invokeTitleChain(invokeConfig));
1071
+ return await withLangfuseRuntimeScope(titleRuntimeScope, () => invokeTitleChain(invokeConfig));
1040
1072
  } catch (_e) {
1041
1073
  const langfuseHandler = findCallback(invokeConfig.callbacks, isLangfuseCallbackHandler);
1042
1074
  const { callbacks: _cb, ...rest } = invokeConfig;
1043
1075
  const safeConfig = Object.assign({}, rest, { callbacks: langfuseHandler ? [langfuseHandler] : [] });
1044
- return await withLangfuseRuntimeScope(resolveLangfuseRuntimeScope({
1045
- runLangfuse: this.langfuse,
1046
- langfuseOverlay: titleContext?.langfuse
1047
- }), () => invokeTitleChain(safeConfig));
1076
+ return await withLangfuseRuntimeScope(titleRuntimeScope, () => invokeTitleChain(safeConfig));
1048
1077
  }
1049
1078
  } finally {
1050
1079
  await disposeLangfuseHandler(titleLangfuseHandler);
@@ -1096,10 +1125,13 @@ var Run = class Run {
1096
1125
  * otherwise unseeded, matching the other generation paths. */
1097
1126
  const inheritedTraceSeed = getTraceIdSeed();
1098
1127
  const labelTraceSeed = labelLangfuseConfig?.deterministicTraceId === true || inheritedTraceSeed != null ? traceSeed ?? `activity-label-${this.id}-${labelSeq}` : void 0;
1128
+ /** Opaque per-execution component: see `titleScopeRunId`. */
1129
+ const labelScopeRunId = `activity-label:${this.id}:${labelSeq}:${nanoid()}`;
1099
1130
  const labelRuntimeScope = resolveLangfuseRuntimeScope({
1100
1131
  runLangfuse: this.langfuse,
1101
1132
  langfuseOverlay: labelContext?.langfuse,
1102
- traceIdSeed: labelTraceSeed
1133
+ traceIdSeed: labelTraceSeed,
1134
+ runId: labelScopeRunId
1103
1135
  });
1104
1136
  /** Handler only when a session id resolved from
1105
1137
  * `chainOptions.configurable.thread_id`: without it the label call has
@@ -1115,7 +1147,10 @@ var Run = class Run {
1115
1147
  sessionId: labelSessionId,
1116
1148
  traceMetadata,
1117
1149
  tags: ["librechat", "activity-label"],
1118
- traceIdSeed: labelLangfuseConfig?.deterministicTraceId === true ? labelTraceSeed : void 0
1150
+ traceIdSeed: labelLangfuseConfig?.deterministicTraceId === true ? labelTraceSeed : void 0,
1151
+ runId: labelScopeRunId,
1152
+ toolOutputTracing: labelRuntimeScope.toolOutputTracing,
1153
+ traceName: labelChainOptions.runName ?? labelRunName
1119
1154
  });
1120
1155
  if (labelLangfuseHandler != null) labelChainOptions.callbacks = appendCallbacks(labelChainOptions.callbacks, [labelLangfuseHandler]);
1121
1156
  /** The label prompt becomes Langfuse generation input, so the resolved