@librechat/agents 3.1.71-dev.0 → 3.1.71

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 (38) hide show
  1. package/dist/cjs/graphs/Graph.cjs +7 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/invoke.cjs +13 -2
  4. package/dist/cjs/llm/invoke.cjs.map +1 -1
  5. package/dist/cjs/tools/BashExecutor.cjs +3 -1
  6. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  7. package/dist/cjs/tools/ToolNode.cjs +84 -55
  8. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  9. package/dist/cjs/tools/toolOutputReferences.cjs +195 -0
  10. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  11. package/dist/esm/graphs/Graph.mjs +7 -0
  12. package/dist/esm/graphs/Graph.mjs.map +1 -1
  13. package/dist/esm/llm/invoke.mjs +13 -2
  14. package/dist/esm/llm/invoke.mjs.map +1 -1
  15. package/dist/esm/tools/BashExecutor.mjs +3 -1
  16. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  17. package/dist/esm/tools/ToolNode.mjs +85 -56
  18. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  19. package/dist/esm/tools/toolOutputReferences.mjs +195 -1
  20. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  21. package/dist/types/graphs/Graph.d.ts +9 -2
  22. package/dist/types/llm/invoke.d.ts +29 -3
  23. package/dist/types/tools/ToolNode.d.ts +11 -13
  24. package/dist/types/tools/toolOutputReferences.d.ts +31 -0
  25. package/dist/types/types/index.d.ts +1 -0
  26. package/dist/types/types/messages.d.ts +26 -0
  27. package/package.json +1 -1
  28. package/src/graphs/Graph.ts +8 -1
  29. package/src/llm/invoke.test.ts +446 -0
  30. package/src/llm/invoke.ts +45 -5
  31. package/src/tools/BashExecutor.ts +3 -1
  32. package/src/tools/ToolNode.ts +94 -81
  33. package/src/tools/__tests__/BashExecutor.test.ts +13 -0
  34. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +98 -55
  35. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +479 -0
  36. package/src/tools/toolOutputReferences.ts +235 -0
  37. package/src/types/index.ts +1 -0
  38. package/src/types/messages.ts +27 -0
@@ -22,6 +22,8 @@
22
22
  * complete, verbatim output with no injected fields.
23
23
  */
24
24
 
25
+ import { ToolMessage } from '@langchain/core/messages';
26
+ import type { BaseMessage } from '@langchain/core/messages';
25
27
  import {
26
28
  calculateMaxTotalToolOutputSize,
27
29
  HARD_MAX_TOOL_RESULT_CHARS,
@@ -243,6 +245,16 @@ export class ToolOutputReferenceRegistry {
243
245
  return this.runStates.get(this.keyFor(runId))?.entries.get(key);
244
246
  }
245
247
 
248
+ /**
249
+ * Returns `true` when `key` is currently stored in `runId`'s bucket.
250
+ * Used by {@link annotateMessagesForLLM} to gate transient annotation
251
+ * on whether the registry still owns the referenced output (a stale
252
+ * `_refKey` from a prior run silently no-ops here).
253
+ */
254
+ has(runId: string | undefined, key: string): boolean {
255
+ return this.runStates.get(this.keyFor(runId))?.entries.has(key) ?? false;
256
+ }
257
+
246
258
  /** Total number of registered outputs across every run bucket. */
247
259
  get size(): number {
248
260
  let n = 0;
@@ -588,3 +600,226 @@ function arraysShallowEqual(a: unknown, b: readonly string[]): boolean {
588
600
  }
589
601
  return true;
590
602
  }
603
+
604
+ /**
605
+ * Lazy projection that, given a registry and a runId, returns a new
606
+ * `messages` array where each `ToolMessage` carrying ref metadata is
607
+ * projected into a transient copy with annotated content (when the ref
608
+ * is live in the registry) and with the framework-owned `additional_
609
+ * kwargs` keys (`_refKey`, `_refScope`, `_unresolvedRefs`) stripped
610
+ * regardless of whether annotation applied. The original input array
611
+ * and its messages are never mutated.
612
+ *
613
+ * Annotation is gated on registry presence: a stale `_refKey` from a
614
+ * prior run (e.g. one that survived in persisted history) silently
615
+ * no-ops on the *content* side. The strip-metadata side still runs so
616
+ * stale framework keys never leak onto the wire under any custom or
617
+ * future provider serializer that might transmit `additional_kwargs`.
618
+ * `_unresolvedRefs` is always meaningful and is not gated.
619
+ *
620
+ * **Feature-disabled fast path:** when the host hasn't enabled the
621
+ * tool-output-reference feature, the registry is `undefined` and this
622
+ * function returns the input array reference-equal *without iterating
623
+ * a single message*. The loop is exclusive to the feature-enabled
624
+ * code path.
625
+ */
626
+ export function annotateMessagesForLLM(
627
+ messages: BaseMessage[],
628
+ registry: ToolOutputReferenceRegistry | undefined,
629
+ runId: string | undefined
630
+ ): BaseMessage[] {
631
+ if (registry == null) return messages;
632
+
633
+ /**
634
+ * Lazy-allocate the output array so the common case (no ToolMessage
635
+ * carries framework metadata) returns the input reference-equal with
636
+ * zero allocations beyond the per-message predicate checks.
637
+ */
638
+ let out: BaseMessage[] | undefined;
639
+ for (let i = 0; i < messages.length; i++) {
640
+ const m = messages[i];
641
+ if (m._getType() !== 'tool') continue;
642
+ /**
643
+ * `additional_kwargs` is untyped at the LangChain layer
644
+ * (`Record<string, unknown>`), so persisted or client-supplied
645
+ * ToolMessages can carry arbitrary shapes — including primitives
646
+ * (a malformed serializer might write a string, or `null`).
647
+ * Guard with a runtime object check before the `in` probes
648
+ * because the `in` operator throws `TypeError` on primitives.
649
+ * A single malformed message must never crash the provider call
650
+ * path; skip its annotation/strip and continue.
651
+ */
652
+ const rawMeta = m.additional_kwargs as unknown;
653
+ if (rawMeta == null || typeof rawMeta !== 'object') continue;
654
+ const meta = rawMeta as Record<string, unknown>;
655
+ const hasRefKey = '_refKey' in meta;
656
+ const hasRefScope = '_refScope' in meta;
657
+ const hasUnresolvedField = '_unresolvedRefs' in meta;
658
+ if (!hasRefKey && !hasRefScope && !hasUnresolvedField) continue;
659
+
660
+ const refKey = readRefKey(meta);
661
+ const unresolved = readUnresolvedRefs(meta);
662
+
663
+ /**
664
+ * Prefer the message-stamped `_refScope` for the registry lookup.
665
+ * For named runs it equals the current `runId`; for anonymous
666
+ * invocations it carries the per-batch synthetic scope minted by
667
+ * ToolNode (`\0anon-<n>`), which `runId` from config cannot
668
+ * recover. Falling back to `runId` keeps backward compatibility
669
+ * with messages stamped before this field existed.
670
+ */
671
+ const lookupScope = readRefScope(meta) ?? runId;
672
+ const liveRef =
673
+ refKey != null && registry.has(lookupScope, refKey) ? refKey : undefined;
674
+ const annotates = liveRef != null || unresolved.length > 0;
675
+
676
+ const tm = m as ToolMessage;
677
+ let nextContent: ToolMessage['content'] = tm.content;
678
+
679
+ if (annotates && typeof tm.content === 'string') {
680
+ nextContent = annotateToolOutputWithReference(
681
+ tm.content,
682
+ liveRef,
683
+ unresolved
684
+ );
685
+ } else if (
686
+ annotates &&
687
+ Array.isArray(tm.content) &&
688
+ unresolved.length > 0
689
+ ) {
690
+ const warningBlock = {
691
+ type: 'text' as const,
692
+ text: `[unresolved refs: ${unresolved.join(', ')}]`,
693
+ };
694
+ /**
695
+ * `as unknown as ToolMessage['content']` is unavoidable here:
696
+ * LangChain's content union (`MessageContentComplex[] |
697
+ * DataContentBlock[] | string`) does not accept a freshly built
698
+ * mixed array literal even though the structural shape is valid
699
+ * at runtime. The double-cast is structurally safe — we
700
+ * preserve every block from `tm.content` and prepend a single
701
+ * `{ type: 'text', text }` block that all providers accept.
702
+ */
703
+ nextContent = [
704
+ warningBlock,
705
+ ...tm.content,
706
+ ] as unknown as ToolMessage['content'];
707
+ }
708
+
709
+ /**
710
+ * Project unconditionally: even when no annotation applies (stale
711
+ * `_refKey` or non-annotatable content), `cloneToolMessageWithContent`
712
+ * runs `stripFrameworkRefMetadata` on `additional_kwargs` so the
713
+ * framework-owned keys never reach the wire.
714
+ */
715
+ out ??= messages.slice();
716
+ out[i] = cloneToolMessageWithContent(tm, nextContent);
717
+ }
718
+
719
+ return out ?? messages;
720
+ }
721
+
722
+ /**
723
+ * Reads `_refKey` defensively from untyped `additional_kwargs`. Returns
724
+ * undefined for non-string values so a malformed field cannot poison
725
+ * the registry lookup or downstream string operations.
726
+ */
727
+ function readRefKey(
728
+ meta: Record<string, unknown> | undefined
729
+ ): string | undefined {
730
+ const v = meta?._refKey;
731
+ return typeof v === 'string' ? v : undefined;
732
+ }
733
+
734
+ /**
735
+ * Reads `_refScope` defensively from untyped `additional_kwargs`.
736
+ * Mirrors {@link readRefKey} — non-string scopes are dropped (the
737
+ * caller falls back to the run-derived scope) rather than passed into
738
+ * the registry as a malformed key.
739
+ */
740
+ function readRefScope(
741
+ meta: Record<string, unknown> | undefined
742
+ ): string | undefined {
743
+ const v = meta?._refScope;
744
+ return typeof v === 'string' ? v : undefined;
745
+ }
746
+
747
+ /**
748
+ * Reads `_unresolvedRefs` defensively from untyped `additional_kwargs`.
749
+ * Returns an empty array for any non-array value, and filters out
750
+ * non-string entries from a real array. Without this guard, a hydrated
751
+ * ToolMessage carrying e.g. `_unresolvedRefs: 'tool0turn0'` would crash
752
+ * `attemptInvoke` on the eventual `.length` / `.join(...)` call.
753
+ */
754
+ function readUnresolvedRefs(
755
+ meta: Record<string, unknown> | undefined
756
+ ): string[] {
757
+ const v = meta?._unresolvedRefs;
758
+ if (!Array.isArray(v)) return [];
759
+ const out: string[] = [];
760
+ for (const item of v) {
761
+ if (typeof item === 'string') out.push(item);
762
+ }
763
+ return out;
764
+ }
765
+
766
+ /**
767
+ * Builds a fresh `ToolMessage` that mirrors `tm`'s identity fields with
768
+ * the supplied `content`. Every `ToolMessage` field but `content` is
769
+ * carried over so the projection is structurally identical to the
770
+ * original from a LangChain serializer's perspective.
771
+ *
772
+ * `additional_kwargs` is rebuilt with the framework-owned ref keys
773
+ * stripped. Defensive: LangChain's standard provider serializers do not
774
+ * transmit `additional_kwargs` to provider HTTP APIs, but a custom
775
+ * adapter or future LangChain change could. Stripping keeps the
776
+ * implementation correct under any serializer behavior at the cost of a
777
+ * shallow object spread per annotated message.
778
+ */
779
+ function cloneToolMessageWithContent(
780
+ tm: ToolMessage,
781
+ content: ToolMessage['content']
782
+ ): ToolMessage {
783
+ return new ToolMessage({
784
+ id: tm.id,
785
+ name: tm.name,
786
+ status: tm.status,
787
+ artifact: tm.artifact,
788
+ tool_call_id: tm.tool_call_id,
789
+ response_metadata: tm.response_metadata,
790
+ additional_kwargs: stripFrameworkRefMetadata(tm.additional_kwargs),
791
+ content,
792
+ });
793
+ }
794
+
795
+ /**
796
+ * Returns a copy of `kwargs` with `_refKey`, `_refScope`, and
797
+ * `_unresolvedRefs` removed. Returns the input reference-equal when
798
+ * none of those keys are present so the no-strip path stays cheap;
799
+ * returns `undefined` when stripping leaves the object empty so the
800
+ * caller can drop the field entirely.
801
+ */
802
+ function stripFrameworkRefMetadata(
803
+ kwargs: Record<string, unknown> | undefined
804
+ ): Record<string, unknown> | undefined {
805
+ if (kwargs == null) return undefined;
806
+ if (
807
+ !('_refKey' in kwargs) &&
808
+ !('_refScope' in kwargs) &&
809
+ !('_unresolvedRefs' in kwargs)
810
+ ) {
811
+ return kwargs;
812
+ }
813
+ const { _refKey, _refScope, _unresolvedRefs, ...rest } = kwargs as Record<
814
+ string,
815
+ unknown
816
+ > & {
817
+ _refKey?: unknown;
818
+ _refScope?: unknown;
819
+ _unresolvedRefs?: unknown;
820
+ };
821
+ void _refKey;
822
+ void _refScope;
823
+ void _unresolvedRefs;
824
+ return Object.keys(rest).length === 0 ? undefined : rest;
825
+ }
@@ -1,6 +1,7 @@
1
1
  // src/types/index.ts
2
2
  export * from './graph';
3
3
  export * from './llm';
4
+ export * from './messages';
4
5
  export * from './run';
5
6
  export * from './skill';
6
7
  export * from './stream';
@@ -2,3 +2,30 @@ import type Anthropic from '@anthropic-ai/sdk';
2
2
  import type { BaseMessage } from '@langchain/core/messages';
3
3
  export type AnthropicMessages = Array<AnthropicMessage | BaseMessage>;
4
4
  export type AnthropicMessage = Anthropic.MessageParam;
5
+
6
+ /**
7
+ * Per-message ref metadata stamped onto a `ToolMessage` at execution
8
+ * time. Read by `annotateMessagesForLLM` to apply transient annotation
9
+ * to a copy of the message right before it goes on the wire to the
10
+ * provider. Never read after the run-scoped registry has been cleared.
11
+ *
12
+ * Lives in `ToolMessage.additional_kwargs`. LangChain's provider
13
+ * serializers don't transmit `additional_kwargs` to provider APIs, so
14
+ * the metadata never leaks even if you forget to clean it.
15
+ */
16
+ export interface ToolMessageRefMetadata {
17
+ /** Key under which this message's untruncated output was registered. */
18
+ _refKey?: string;
19
+ /**
20
+ * Registry bucket scope under which `_refKey` was stored. For named
21
+ * runs this equals `config.configurable.run_id`; for anonymous
22
+ * invocations (no `run_id`) ToolNode mints a per-batch synthetic
23
+ * scope (`\0anon-<n>`) so concurrent batches don't collide. Stamping
24
+ * the scope on the message itself lets `annotateMessagesForLLM`
25
+ * recover it without re-deriving from config — which is impossible
26
+ * for the anonymous case, since the scope is internal to ToolNode.
27
+ */
28
+ _refScope?: string;
29
+ /** Placeholders the model used that could not be resolved this batch. */
30
+ _unresolvedRefs?: string[];
31
+ }