@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.
- package/dist/cjs/graphs/Graph.cjs +7 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +13 -2
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +3 -1
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +84 -55
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +195 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +7 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +13 -2
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +3 -1
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +85 -56
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +195 -1
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +9 -2
- package/dist/types/llm/invoke.d.ts +29 -3
- package/dist/types/tools/ToolNode.d.ts +11 -13
- package/dist/types/tools/toolOutputReferences.d.ts +31 -0
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/messages.d.ts +26 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +8 -1
- package/src/llm/invoke.test.ts +446 -0
- package/src/llm/invoke.ts +45 -5
- package/src/tools/BashExecutor.ts +3 -1
- package/src/tools/ToolNode.ts +94 -81
- package/src/tools/__tests__/BashExecutor.test.ts +13 -0
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +98 -55
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +479 -0
- package/src/tools/toolOutputReferences.ts +235 -0
- package/src/types/index.ts +1 -0
- 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
|
+
}
|
package/src/types/index.ts
CHANGED
package/src/types/messages.ts
CHANGED
|
@@ -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
|
+
}
|