@librechat/agents 3.4.0 → 3.4.2
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/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +165 -57
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
- package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
- package/dist/cjs/graphs/createGraph.cjs +15 -0
- package/dist/cjs/graphs/createGraph.cjs.map +1 -0
- package/dist/cjs/graphs/index.cjs +1 -0
- package/dist/cjs/hooks/HookRegistry.cjs +83 -0
- package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
- package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +60 -14
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/hooks/index.cjs.map +1 -1
- package/dist/cjs/hooks/types.cjs +2 -0
- package/dist/cjs/hooks/types.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +2 -2
- package/dist/cjs/main.cjs +34 -24
- package/dist/cjs/messages/format.cjs +40 -23
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/run.cjs +130 -53
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +37 -2
- package/dist/cjs/session/AgentSession.cjs.map +1 -1
- package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
- package/dist/cjs/stream.cjs +1 -1
- package/dist/cjs/summarization/node.cjs +6 -3
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
- package/dist/cjs/tools/SubagentTool.cjs +4 -4
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +321 -95
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearch.cjs +1 -1
- package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
- package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
- package/dist/cjs/tools/search/rerankers.cjs +154 -1
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +4 -1
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
- package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1114 -233
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
- package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
- package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
- package/dist/cjs/tools/subagent/index.cjs +1 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
- package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
- package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +165 -57
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
- package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
- package/dist/esm/graphs/createGraph.mjs +15 -0
- package/dist/esm/graphs/createGraph.mjs.map +1 -0
- package/dist/esm/graphs/index.mjs +1 -0
- package/dist/esm/hooks/HookRegistry.mjs +83 -0
- package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
- package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
- package/dist/esm/hooks/executeHooks.mjs +60 -14
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/hooks/index.mjs.map +1 -1
- package/dist/esm/hooks/types.mjs +2 -1
- package/dist/esm/hooks/types.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +1 -1
- package/dist/esm/llm/invoke.mjs +2 -2
- package/dist/esm/main.mjs +21 -19
- package/dist/esm/messages/format.mjs +40 -23
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/run.mjs +131 -54
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +37 -2
- package/dist/esm/session/AgentSession.mjs.map +1 -1
- package/dist/esm/session/JsonlSessionStore.mjs +2 -2
- package/dist/esm/stream.mjs +1 -1
- package/dist/esm/summarization/node.mjs +6 -3
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
- package/dist/esm/tools/SubagentTool.mjs +4 -4
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +322 -96
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearch.mjs +1 -1
- package/dist/esm/tools/eagerEventExecution.mjs +1 -1
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
- package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
- package/dist/esm/tools/search/rerankers.mjs +154 -1
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +4 -1
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
- package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1114 -233
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
- package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
- package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
- package/dist/esm/tools/subagent/index.mjs +1 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
- package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
- package/dist/esm/tools/toolOutputReferences.mjs +20 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/agents/AgentContext.d.ts +1 -1
- package/dist/types/graphs/Graph.d.ts +22 -2
- package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
- package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
- package/dist/types/graphs/createGraph.d.ts +13 -0
- package/dist/types/graphs/graphFactory.d.ts +13 -0
- package/dist/types/graphs/index.d.ts +2 -0
- package/dist/types/hooks/HookRegistry.d.ts +10 -1
- package/dist/types/hooks/executeHooks.d.ts +5 -1
- package/dist/types/hooks/index.d.ts +2 -2
- package/dist/types/hooks/types.d.ts +12 -0
- package/dist/types/run.d.ts +6 -3
- package/dist/types/session/AgentSession.d.ts +1 -0
- package/dist/types/tools/SubagentTool.d.ts +1 -1
- package/dist/types/tools/ToolNode.d.ts +23 -15
- package/dist/types/tools/search/rerankers.d.ts +28 -0
- package/dist/types/tools/search/types.d.ts +38 -1
- package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +70 -36
- package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
- package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
- package/dist/types/tools/subagent/index.d.ts +1 -1
- package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
- package/dist/types/tools/toolOutputReferences.d.ts +12 -0
- package/dist/types/types/graph.d.ts +189 -24
- package/dist/types/types/hitl.d.ts +15 -0
- package/package.json +8 -2
- package/src/agents/AgentContext.ts +1 -1
- package/src/graphs/Graph.ts +349 -104
- package/src/graphs/MultiAgentGraph.ts +149 -47
- package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
- package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
- package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
- package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
- package/src/graphs/__tests__/createGraph.test.ts +206 -0
- package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
- package/src/graphs/createGraph.ts +32 -0
- package/src/graphs/graphFactory.ts +12 -0
- package/src/graphs/index.ts +2 -0
- package/src/hooks/HookRegistry.ts +151 -1
- package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
- package/src/hooks/__tests__/executeHooks.test.ts +160 -0
- package/src/hooks/executeHooks.ts +133 -15
- package/src/hooks/index.ts +3 -1
- package/src/hooks/types.ts +16 -0
- package/src/messages/format.ts +54 -26
- package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
- package/src/messages/formatAgentMessages.steer.test.ts +16 -11
- package/src/messages/formatAgentMessages.test.ts +7 -2
- package/src/run.ts +214 -56
- package/src/session/AgentSession.ts +52 -2
- package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
- package/src/specs/graph-subagent.live.test.ts +242 -0
- package/src/specs/graph-subagent.test.ts +322 -0
- package/src/specs/langfuse-routing.integration.test.ts +120 -1
- package/src/specs/subagent.test.ts +130 -4
- package/src/summarization/__tests__/node.test.ts +32 -14
- package/src/summarization/node.ts +15 -11
- package/src/tools/SubagentTool.ts +4 -4
- package/src/tools/ToolNode.ts +643 -157
- package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
- package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
- package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
- package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
- package/src/tools/__tests__/hitl.test.ts +234 -5
- package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
- package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
- package/src/tools/search/rag-api-reranker.test.ts +891 -0
- package/src/tools/search/rerankers.ts +260 -0
- package/src/tools/search/tool.ts +6 -0
- package/src/tools/search/types.ts +45 -1
- package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
- package/src/tools/subagent/SubagentExecutor.ts +2232 -312
- package/src/tools/subagent/SubagentReplay.ts +584 -0
- package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
- package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
- package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
- package/src/tools/subagent/childGraphConfig.ts +694 -0
- package/src/tools/subagent/index.ts +6 -0
- package/src/tools/subagent/runtimeLimits.ts +7 -0
- package/src/tools/toolOutputReferences.ts +40 -1
- package/src/types/graph.ts +222 -24
- package/src/types/hitl.ts +16 -0
package/src/messages/format.ts
CHANGED
|
@@ -339,6 +339,7 @@ interface FormatAssistantMessageOptions {
|
|
|
339
339
|
preserveUnpairedServerToolUses?: boolean;
|
|
340
340
|
preserveReasoningContent?: boolean;
|
|
341
341
|
provider?: Providers;
|
|
342
|
+
sourceMessageId?: string;
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
interface FormatAgentMessagesOptions {
|
|
@@ -514,6 +515,16 @@ function formatAssistantMessage(
|
|
|
514
515
|
| RoleBearingMessage<ToolMessage>
|
|
515
516
|
| RoleBearingMessage<HumanMessage>
|
|
516
517
|
> = [];
|
|
518
|
+
const appendFormattedMessage = (
|
|
519
|
+
formattedMessage: (typeof formattedMessages)[number]
|
|
520
|
+
): void => {
|
|
521
|
+
stampSourceMessageIdentity(
|
|
522
|
+
formattedMessage,
|
|
523
|
+
options?.sourceMessageId,
|
|
524
|
+
formattedMessages.length
|
|
525
|
+
);
|
|
526
|
+
formattedMessages.push(formattedMessage);
|
|
527
|
+
};
|
|
517
528
|
let currentContent: MessageContentComplex[] = [];
|
|
518
529
|
let lastAIMessage: RoleBearingMessage<AIMessage> | null = null;
|
|
519
530
|
let hasReasoning = false;
|
|
@@ -634,7 +645,7 @@ function formatAssistantMessage(
|
|
|
634
645
|
) {
|
|
635
646
|
currentContent.push(part);
|
|
636
647
|
lastAIMessage = createAIMessage(toLangChainContent(currentContent));
|
|
637
|
-
|
|
648
|
+
appendFormattedMessage(lastAIMessage);
|
|
638
649
|
currentContent = [];
|
|
639
650
|
continue;
|
|
640
651
|
}
|
|
@@ -646,13 +657,13 @@ function formatAssistantMessage(
|
|
|
646
657
|
}, '');
|
|
647
658
|
content = `${content}\n${getTextContent(part)}`.trim();
|
|
648
659
|
lastAIMessage = createAIMessage(content);
|
|
649
|
-
|
|
660
|
+
appendFormattedMessage(lastAIMessage);
|
|
650
661
|
currentContent = [];
|
|
651
662
|
continue;
|
|
652
663
|
}
|
|
653
664
|
// Create a new AIMessage with this text and prepare for tool calls
|
|
654
665
|
lastAIMessage = createAIMessage(getTextContent(part));
|
|
655
|
-
|
|
666
|
+
appendFormattedMessage(lastAIMessage);
|
|
656
667
|
} else if (part.type === ContentTypes.TOOL_CALL) {
|
|
657
668
|
// Skip malformed tool call entries without tool_call property
|
|
658
669
|
if (part.tool_call == null) {
|
|
@@ -711,7 +722,7 @@ function formatAssistantMessage(
|
|
|
711
722
|
if (!lastAIMessage) {
|
|
712
723
|
// "Heal" the payload by creating an AIMessage to precede the tool call
|
|
713
724
|
lastAIMessage = createAIMessage('');
|
|
714
|
-
|
|
725
|
+
appendFormattedMessage(lastAIMessage);
|
|
715
726
|
} else {
|
|
716
727
|
attachPendingReasoningContent(lastAIMessage);
|
|
717
728
|
}
|
|
@@ -749,7 +760,7 @@ function formatAssistantMessage(
|
|
|
749
760
|
lastAIMessage.tool_calls.push(tool_call as ToolCall);
|
|
750
761
|
}
|
|
751
762
|
|
|
752
|
-
|
|
763
|
+
appendFormattedMessage(
|
|
753
764
|
withMessageRole(
|
|
754
765
|
new ToolMessage({
|
|
755
766
|
tool_call_id: tool_call.id ?? '',
|
|
@@ -788,14 +799,14 @@ function formatAssistantMessage(
|
|
|
788
799
|
currentContent.some((content) => content.type !== ContentTypes.TEXT)
|
|
789
800
|
) {
|
|
790
801
|
lastAIMessage = createAIMessage(toLangChainContent(currentContent));
|
|
791
|
-
|
|
802
|
+
appendFormattedMessage(lastAIMessage);
|
|
792
803
|
} else {
|
|
793
804
|
const flushed = currentContent
|
|
794
805
|
.reduce((acc, curr) => `${acc}${getTextContent(curr)}\n`, '')
|
|
795
806
|
.trim();
|
|
796
807
|
if (flushed.length > 0) {
|
|
797
808
|
lastAIMessage = createAIMessage(flushed);
|
|
798
|
-
|
|
809
|
+
appendFormattedMessage(lastAIMessage);
|
|
799
810
|
}
|
|
800
811
|
}
|
|
801
812
|
currentContent = [];
|
|
@@ -808,7 +819,7 @@ function formatAssistantMessage(
|
|
|
808
819
|
* assistant reasoning silently vanishes on replay.
|
|
809
820
|
*/
|
|
810
821
|
lastAIMessage = createAIMessage('');
|
|
811
|
-
|
|
822
|
+
appendFormattedMessage(lastAIMessage);
|
|
812
823
|
}
|
|
813
824
|
const steerPart = part as {
|
|
814
825
|
steer?: string;
|
|
@@ -818,7 +829,7 @@ function formatAssistantMessage(
|
|
|
818
829
|
Array.isArray(steerPart.media) && steerPart.media.length > 0
|
|
819
830
|
? toLangChainContent(steerPart.media)
|
|
820
831
|
: (steerPart.steer ?? '');
|
|
821
|
-
|
|
832
|
+
appendFormattedMessage(
|
|
822
833
|
withMessageRole(
|
|
823
834
|
new HumanMessage({
|
|
824
835
|
content: steerContent as MessageContent,
|
|
@@ -856,7 +867,7 @@ function formatAssistantMessage(
|
|
|
856
867
|
let content = '';
|
|
857
868
|
for (const part of currentContent) {
|
|
858
869
|
if (part.type !== ContentTypes.TEXT) {
|
|
859
|
-
|
|
870
|
+
appendFormattedMessage(
|
|
860
871
|
createAIMessage(toLangChainContent(currentContent))
|
|
861
872
|
);
|
|
862
873
|
return formattedMessages;
|
|
@@ -866,10 +877,10 @@ function formatAssistantMessage(
|
|
|
866
877
|
content = content.trim();
|
|
867
878
|
|
|
868
879
|
if (content) {
|
|
869
|
-
|
|
880
|
+
appendFormattedMessage(createAIMessage(content));
|
|
870
881
|
}
|
|
871
882
|
} else if (currentContent.length > 0) {
|
|
872
|
-
|
|
883
|
+
appendFormattedMessage(createAIMessage(toLangChainContent(currentContent)));
|
|
873
884
|
}
|
|
874
885
|
|
|
875
886
|
return formattedMessages;
|
|
@@ -886,6 +897,29 @@ function getSourceMessageId(message: Partial<TMessage>): string | undefined {
|
|
|
886
897
|
return normalized.length > 0 ? normalized : undefined;
|
|
887
898
|
}
|
|
888
899
|
|
|
900
|
+
/**
|
|
901
|
+
* Keeps the first formatted message backward-compatible with its persisted
|
|
902
|
+
* source id and preserves source correlation on every derived message.
|
|
903
|
+
* Derived ids remain unset so the reducer can assign collision-free identities
|
|
904
|
+
* during its existing pass. This also prevents invented provider-shaped ids
|
|
905
|
+
* from leaking into provider request payloads.
|
|
906
|
+
*/
|
|
907
|
+
function stampSourceMessageIdentity(
|
|
908
|
+
message: RoleBearingMessage<BaseMessage>,
|
|
909
|
+
sourceMessageId: string | undefined,
|
|
910
|
+
derivedIndex = 0
|
|
911
|
+
): void {
|
|
912
|
+
if (sourceMessageId == null) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
message.additional_kwargs.sourceMessageId = sourceMessageId;
|
|
916
|
+
if (derivedIndex !== 0) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
message.id = sourceMessageId;
|
|
920
|
+
message.lc_kwargs.id = sourceMessageId;
|
|
921
|
+
}
|
|
922
|
+
|
|
889
923
|
/**
|
|
890
924
|
* Labels all agent content for parallel patterns (fan-out/fan-in)
|
|
891
925
|
* Groups consecutive content by agent and wraps with clear labels
|
|
@@ -1565,9 +1599,7 @@ export const formatAgentMessages = (
|
|
|
1565
1599
|
| RoleBearingMessage<HumanMessage>
|
|
1566
1600
|
| RoleBearingMessage<AIMessage>
|
|
1567
1601
|
| RoleBearingMessage<SystemMessage>;
|
|
1568
|
-
|
|
1569
|
-
formattedMessage.id = sourceMessageId;
|
|
1570
|
-
}
|
|
1602
|
+
stampSourceMessageIdentity(formattedMessage, sourceMessageId);
|
|
1571
1603
|
flushSteerAnchor(formattedMessage);
|
|
1572
1604
|
messages.push(formattedMessage);
|
|
1573
1605
|
|
|
@@ -1746,12 +1778,8 @@ export const formatAgentMessages = (
|
|
|
1746
1778
|
options?.preserveReasoningContent ??
|
|
1747
1779
|
options?.provider === Providers.DEEPSEEK,
|
|
1748
1780
|
provider: options?.provider,
|
|
1781
|
+
sourceMessageId,
|
|
1749
1782
|
});
|
|
1750
|
-
if (sourceMessageId != null && sourceMessageId !== '') {
|
|
1751
|
-
for (const formattedMessage of formattedMessages) {
|
|
1752
|
-
formattedMessage.id = sourceMessageId;
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
1783
|
/**
|
|
1756
1784
|
* A steer that ends an assistant message leaves the replay on a
|
|
1757
1785
|
* `HumanMessage`. The next payload message is itself a user turn, so the
|
|
@@ -1775,12 +1803,12 @@ export const formatAgentMessages = (
|
|
|
1775
1803
|
* the model may simply never answer. So the intent is recorded and flushed
|
|
1776
1804
|
* only when a message actually follows.
|
|
1777
1805
|
*
|
|
1778
|
-
* Pushed AFTER
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1806
|
+
* Pushed AFTER source identity stamping above, deliberately. The anchor is
|
|
1807
|
+
* synthetic rather than derived from the persisted assistant entry, so it
|
|
1808
|
+
* must not claim that entry's source metadata. Left unstamped, it reaches
|
|
1809
|
+
* the reducer with a null id and is assigned a fresh one.
|
|
1810
|
+
* `endsWithSteerMessage` reads only `additional_kwargs.source`, so the
|
|
1811
|
+
* deferral cannot change which messages get anchored.
|
|
1784
1812
|
*/
|
|
1785
1813
|
/**
|
|
1786
1814
|
* Guarded on emission: an assistant entry whose blocks all filtered away
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { convertMessagesToResponsesInput } from '@langchain/openai';
|
|
2
|
+
import { Annotation, END, START, StateGraph } from '@langchain/langgraph';
|
|
3
|
+
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
4
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
5
|
+
import { messagesStateReducer } from './reducer';
|
|
6
|
+
import { formatAgentMessages } from './format';
|
|
7
|
+
import { ContentTypes } from '@/common';
|
|
8
|
+
|
|
9
|
+
const formatToolTurn = () =>
|
|
10
|
+
formatAgentMessages([
|
|
11
|
+
{
|
|
12
|
+
role: 'assistant',
|
|
13
|
+
messageId: 'msg_assistant_1',
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: ContentTypes.TEXT,
|
|
17
|
+
[ContentTypes.TEXT]: 'Running tool',
|
|
18
|
+
tool_call_ids: ['tool_1'],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: ContentTypes.TOOL_CALL,
|
|
22
|
+
tool_call: {
|
|
23
|
+
id: 'tool_1',
|
|
24
|
+
name: 'search',
|
|
25
|
+
args: '{"query":"hello"}',
|
|
26
|
+
output: 'world',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
]).messages;
|
|
32
|
+
|
|
33
|
+
describe('formatAgentMessages reducer compatibility', () => {
|
|
34
|
+
it('does not expose derived message ids as OpenAI Responses item ids', () => {
|
|
35
|
+
const messages = formatAgentMessages([
|
|
36
|
+
{
|
|
37
|
+
role: 'assistant',
|
|
38
|
+
messageId: 'msg_provider_1',
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: ContentTypes.TEXT,
|
|
42
|
+
[ContentTypes.TEXT]: 'Running tool',
|
|
43
|
+
tool_call_ids: ['tool_1'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: ContentTypes.TOOL_CALL,
|
|
47
|
+
tool_call: {
|
|
48
|
+
id: 'tool_1',
|
|
49
|
+
name: 'search',
|
|
50
|
+
args: '{"query":"hello"}',
|
|
51
|
+
output: 'world',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: ContentTypes.TEXT,
|
|
56
|
+
[ContentTypes.TEXT]: 'Finished',
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
]).messages;
|
|
61
|
+
const input = convertMessagesToResponsesInput({
|
|
62
|
+
messages,
|
|
63
|
+
zdrEnabled: false,
|
|
64
|
+
model: 'gpt-5.6',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(
|
|
68
|
+
input
|
|
69
|
+
.filter((item) => item.type === 'message')
|
|
70
|
+
.map((item) => ('id' in item ? item.id : undefined))
|
|
71
|
+
).toEqual(['msg_provider_1', undefined]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not let a later source id replace a derived message', () => {
|
|
75
|
+
const messages = formatAgentMessages([
|
|
76
|
+
{
|
|
77
|
+
role: 'assistant',
|
|
78
|
+
messageId: 'a',
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: ContentTypes.TEXT,
|
|
82
|
+
[ContentTypes.TEXT]: 'Running tool',
|
|
83
|
+
tool_call_ids: ['tool_1'],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: ContentTypes.TOOL_CALL,
|
|
87
|
+
tool_call: {
|
|
88
|
+
id: 'tool_1',
|
|
89
|
+
name: 'search',
|
|
90
|
+
args: '{"query":"hello"}',
|
|
91
|
+
output: 'world',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
role: 'user',
|
|
98
|
+
messageId: 'a:derived:1',
|
|
99
|
+
content: 'Continue',
|
|
100
|
+
},
|
|
101
|
+
]).messages;
|
|
102
|
+
|
|
103
|
+
const merged = messagesStateReducer([], messages);
|
|
104
|
+
|
|
105
|
+
expect(merged).toHaveLength(3);
|
|
106
|
+
expect(merged[0]).toBeInstanceOf(AIMessage);
|
|
107
|
+
expect(merged[1]).toBeInstanceOf(ToolMessage);
|
|
108
|
+
expect(merged[2]).toBeInstanceOf(HumanMessage);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('lets the reducer identify derived messages while retaining source correlation', () => {
|
|
112
|
+
const messages = formatToolTurn();
|
|
113
|
+
|
|
114
|
+
expect(messages).toHaveLength(2);
|
|
115
|
+
expect(messages[0]).toBeInstanceOf(AIMessage);
|
|
116
|
+
expect(messages[1]).toBeInstanceOf(ToolMessage);
|
|
117
|
+
expect(messages.map((message) => message.id)).toEqual([
|
|
118
|
+
'msg_assistant_1',
|
|
119
|
+
undefined,
|
|
120
|
+
]);
|
|
121
|
+
expect(messages.map((message) => message.lc_kwargs.id)).toEqual([
|
|
122
|
+
'msg_assistant_1',
|
|
123
|
+
undefined,
|
|
124
|
+
]);
|
|
125
|
+
expect(
|
|
126
|
+
messages.map((message) => message.additional_kwargs.sourceMessageId)
|
|
127
|
+
).toEqual(['msg_assistant_1', 'msg_assistant_1']);
|
|
128
|
+
|
|
129
|
+
const merged = messagesStateReducer([], messages);
|
|
130
|
+
expect(merged).toHaveLength(2);
|
|
131
|
+
expect(merged[0]).toBeInstanceOf(AIMessage);
|
|
132
|
+
expect(merged[1]).toBeInstanceOf(ToolMessage);
|
|
133
|
+
expect(merged[0].id).toBe('msg_assistant_1');
|
|
134
|
+
expect(merged[1].id).toEqual(expect.any(String));
|
|
135
|
+
expect(merged[1].id).not.toBe(merged[0].id);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('preserves the tool-call pair through a StateGraph input write', async () => {
|
|
139
|
+
const State = Annotation.Root({
|
|
140
|
+
messages: Annotation<BaseMessage[]>({
|
|
141
|
+
reducer: messagesStateReducer,
|
|
142
|
+
default: () => [],
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
let observedMessages: BaseMessage[] = [];
|
|
146
|
+
const graph = new StateGraph(State)
|
|
147
|
+
.addNode('capture', (state) => {
|
|
148
|
+
observedMessages = state.messages;
|
|
149
|
+
return {};
|
|
150
|
+
})
|
|
151
|
+
.addEdge(START, 'capture')
|
|
152
|
+
.addEdge('capture', END)
|
|
153
|
+
.compile();
|
|
154
|
+
|
|
155
|
+
await graph.invoke({ messages: formatToolTurn() });
|
|
156
|
+
|
|
157
|
+
expect(observedMessages).toHaveLength(2);
|
|
158
|
+
expect(observedMessages[0]).toBeInstanceOf(AIMessage);
|
|
159
|
+
expect((observedMessages[0] as AIMessage).tool_calls).toHaveLength(1);
|
|
160
|
+
expect(observedMessages[1]).toBeInstanceOf(ToolMessage);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -370,12 +370,9 @@ describe('formatAgentMessages trailing-steer anchor', () => {
|
|
|
370
370
|
expect(following.content).toEqual([{ type: 'text', text: 'Next turn' }]);
|
|
371
371
|
});
|
|
372
372
|
|
|
373
|
-
/**
|
|
374
|
-
*
|
|
375
|
-
|
|
376
|
-
* steer it exists to protect. It must reach the reducer unstamped.
|
|
377
|
-
*/
|
|
378
|
-
it('does not stamp the anchor with the shared source message id', () => {
|
|
373
|
+
/** The synthetic anchor is not derived from the persisted assistant entry,
|
|
374
|
+
* so it must not inherit that entry's source identity metadata. */
|
|
375
|
+
it('does not stamp the anchor with the source message identity', () => {
|
|
379
376
|
const payload: TPayload = [
|
|
380
377
|
{ role: 'user', content: 'Original request' },
|
|
381
378
|
{
|
|
@@ -395,13 +392,20 @@ describe('formatAgentMessages trailing-steer anchor', () => {
|
|
|
395
392
|
(message) => message.additional_kwargs.source === 'steer'
|
|
396
393
|
);
|
|
397
394
|
expect(steerIndex).toBeGreaterThan(0);
|
|
398
|
-
expect(messages[steerIndex].id).
|
|
395
|
+
expect(messages[steerIndex].id).toBeUndefined();
|
|
399
396
|
expect(messages[steerIndex - 1].id).toBe('assistant_1');
|
|
397
|
+
expect(messages[steerIndex].additional_kwargs.sourceMessageId).toBe(
|
|
398
|
+
'assistant_1'
|
|
399
|
+
);
|
|
400
|
+
expect(messages[steerIndex - 1].additional_kwargs.sourceMessageId).toBe(
|
|
401
|
+
'assistant_1'
|
|
402
|
+
);
|
|
400
403
|
|
|
401
404
|
const anchor = messages[steerIndex + 1];
|
|
402
405
|
expect(anchor).toBeInstanceOf(AIMessage);
|
|
403
406
|
expect(anchor.content).not.toBe('');
|
|
404
407
|
expect(anchor.id).not.toBe('assistant_1');
|
|
408
|
+
expect(anchor.additional_kwargs.sourceMessageId).toBeUndefined();
|
|
405
409
|
});
|
|
406
410
|
|
|
407
411
|
/**
|
|
@@ -469,9 +473,7 @@ describe('formatAgentMessages trailing-steer anchor', () => {
|
|
|
469
473
|
const last = messages[messages.length - 1];
|
|
470
474
|
expect(last.additional_kwargs.source).toBe('steer');
|
|
471
475
|
expect(
|
|
472
|
-
messages.some(
|
|
473
|
-
(m) => m instanceof AIMessage && m.content === '_'
|
|
474
|
-
)
|
|
476
|
+
messages.some((m) => m instanceof AIMessage && m.content === '_')
|
|
475
477
|
).toBe(false);
|
|
476
478
|
});
|
|
477
479
|
|
|
@@ -536,7 +538,10 @@ describe('formatAgentMessages trailing-steer anchor', () => {
|
|
|
536
538
|
{
|
|
537
539
|
role: 'assistant',
|
|
538
540
|
content: [
|
|
539
|
-
{
|
|
541
|
+
{
|
|
542
|
+
type: ContentTypes.TEXT,
|
|
543
|
+
[ContentTypes.TEXT]: 'Doing the other thing.',
|
|
544
|
+
},
|
|
540
545
|
],
|
|
541
546
|
},
|
|
542
547
|
];
|
|
@@ -76,7 +76,7 @@ describe('formatAgentMessages', () => {
|
|
|
76
76
|
expect(Object.keys(result.messages[1])).not.toContain('role');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
it('preserves source messageId
|
|
79
|
+
it('preserves source messageId correlation with unique formatted ids', () => {
|
|
80
80
|
const payload: TPayload = [
|
|
81
81
|
{
|
|
82
82
|
role: 'assistant',
|
|
@@ -112,8 +112,13 @@ describe('formatAgentMessages', () => {
|
|
|
112
112
|
'user',
|
|
113
113
|
]);
|
|
114
114
|
expect(result.messages[0].id).toBe('msg_assistant_1');
|
|
115
|
-
expect(result.messages[1].id).
|
|
115
|
+
expect(result.messages[1].id).toBeUndefined();
|
|
116
116
|
expect(result.messages[2].id).toBe('msg_user_1');
|
|
117
|
+
expect(
|
|
118
|
+
result.messages.map(
|
|
119
|
+
(message) => message.additional_kwargs.sourceMessageId
|
|
120
|
+
)
|
|
121
|
+
).toEqual(['msg_assistant_1', 'msg_assistant_1', 'msg_user_1']);
|
|
117
122
|
});
|
|
118
123
|
|
|
119
124
|
it('should handle system messages', () => {
|