@librechat/agents 3.1.81 → 3.1.83
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 +125 -36
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +13 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +50 -13
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +17 -7
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/toolCache.cjs +55 -0
- package/dist/cjs/llm/openrouter/toolCache.cjs.map +1 -0
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +96 -0
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +70 -12
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +125 -36
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +13 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +50 -14
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +17 -7
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/toolCache.mjs +53 -0
- package/dist/esm/llm/openrouter/toolCache.mjs.map +1 -0
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/cache.mjs +96 -1
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +70 -12
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +8 -1
- package/dist/types/agents/__tests__/promptCacheLiveHelpers.d.ts +6 -2
- package/dist/types/llm/openrouter/index.d.ts +1 -0
- package/dist/types/llm/openrouter/toolCache.d.ts +2 -0
- package/dist/types/messages/cache.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +5 -0
- package/dist/types/types/run.d.ts +2 -0
- package/package.json +2 -1
- package/src/agents/AgentContext.ts +191 -40
- package/src/agents/__tests__/AgentContext.anthropic.live.test.ts +0 -4
- package/src/agents/__tests__/AgentContext.openrouter.live.test.ts +128 -0
- package/src/agents/__tests__/AgentContext.test.ts +355 -18
- package/src/agents/__tests__/promptCacheLiveHelpers.ts +8 -2
- package/src/graphs/Graph.ts +24 -0
- package/src/llm/custom-chat-models.smoke.test.ts +76 -0
- package/src/llm/openai/deepseek.test.ts +14 -1
- package/src/llm/openai/index.ts +38 -12
- package/src/llm/openrouter/index.ts +22 -7
- package/src/llm/openrouter/reasoning.test.ts +33 -0
- package/src/llm/openrouter/toolCache.test.ts +83 -0
- package/src/llm/openrouter/toolCache.ts +89 -0
- package/src/messages/cache.test.ts +127 -0
- package/src/messages/cache.ts +143 -0
- package/src/scripts/openrouter_prompt_cache_live.ts +310 -0
- package/src/specs/agent-handoffs.live.test.ts +140 -0
- package/src/specs/agent-handoffs.test.ts +266 -2
- package/src/specs/openrouter.simple.test.ts +15 -8
- package/src/tools/ToolNode.ts +92 -13
- package/src/types/run.ts +2 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// src/specs/agent-handoffs.test.ts
|
|
2
2
|
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
3
|
-
import { HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
3
|
+
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
|
|
4
4
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
5
5
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
6
6
|
import type * as t from '@/types';
|
|
7
|
-
import { Providers, Constants } from '@/common';
|
|
7
|
+
import { Providers, GraphEvents, Constants } from '@/common';
|
|
8
8
|
import { StandardGraph } from '@/graphs/Graph';
|
|
9
|
+
import { ToolNode } from '@/tools/ToolNode';
|
|
10
|
+
import * as events from '@/utils/events';
|
|
9
11
|
import { Run } from '@/run';
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -989,5 +991,267 @@ describe('Agent Handoffs Tests', () => {
|
|
|
989
991
|
`${Constants.LC_TRANSFER_TO_}AgentWithCamelCase`
|
|
990
992
|
);
|
|
991
993
|
});
|
|
994
|
+
|
|
995
|
+
it('should return exact-name guidance for handoff names with extra suffixes', async () => {
|
|
996
|
+
const agents: t.AgentInputs[] = [
|
|
997
|
+
createBasicAgent('router', 'You are a router'),
|
|
998
|
+
createBasicAgent('data_analyst', 'You are a data analyst'),
|
|
999
|
+
];
|
|
1000
|
+
|
|
1001
|
+
const edges: t.GraphEdge[] = [
|
|
1002
|
+
{
|
|
1003
|
+
from: 'router',
|
|
1004
|
+
to: 'data_analyst',
|
|
1005
|
+
edgeType: 'handoff',
|
|
1006
|
+
},
|
|
1007
|
+
];
|
|
1008
|
+
|
|
1009
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
1010
|
+
const correctName = `${Constants.LC_TRANSFER_TO_}data_analyst`;
|
|
1011
|
+
const wrongName = `${correctName}_analyst`;
|
|
1012
|
+
|
|
1013
|
+
run.Graph?.overrideTestModel(
|
|
1014
|
+
['Trying to transfer', 'Stopping after invalid tool name'],
|
|
1015
|
+
10,
|
|
1016
|
+
[
|
|
1017
|
+
{
|
|
1018
|
+
id: 'tool_call_wrong_handoff',
|
|
1019
|
+
name: wrongName,
|
|
1020
|
+
args: { instructions: 'Analyze the uploaded data' },
|
|
1021
|
+
} as ToolCall,
|
|
1022
|
+
]
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
const config: Partial<RunnableConfig> & {
|
|
1026
|
+
version: 'v1' | 'v2';
|
|
1027
|
+
streamMode: string;
|
|
1028
|
+
} = {
|
|
1029
|
+
configurable: {
|
|
1030
|
+
thread_id: 'test-wrong-handoff-name-thread',
|
|
1031
|
+
},
|
|
1032
|
+
streamMode: 'values',
|
|
1033
|
+
version: 'v2' as const,
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
await run.processStream(
|
|
1037
|
+
{ messages: [new HumanMessage('Please analyze my data')] },
|
|
1038
|
+
config
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
const toolMessages = run
|
|
1042
|
+
.getRunMessages()!
|
|
1043
|
+
.filter((msg) => msg.getType() === 'tool') as ToolMessage[];
|
|
1044
|
+
const wrongNameMessage = toolMessages.find(
|
|
1045
|
+
(msg) => msg.name === wrongName
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
expect(wrongNameMessage).toBeDefined();
|
|
1049
|
+
expect(wrongNameMessage?.status).toBe('error');
|
|
1050
|
+
expect(wrongNameMessage?.content).toContain(
|
|
1051
|
+
`Did you mean "${correctName}"`
|
|
1052
|
+
);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('should include toolMap handoffs when direct tool names are present', async () => {
|
|
1056
|
+
const correctName = `${Constants.LC_TRANSFER_TO_}data_analyst`;
|
|
1057
|
+
const wrongName = `${correctName}_analyst`;
|
|
1058
|
+
const handoffTool = new DynamicStructuredTool({
|
|
1059
|
+
name: correctName,
|
|
1060
|
+
description: 'Transfer to data analyst',
|
|
1061
|
+
schema: { type: 'object', properties: {}, required: [] },
|
|
1062
|
+
func: async (): Promise<string> => 'transferred',
|
|
1063
|
+
}) as t.GenericTool;
|
|
1064
|
+
const node = new ToolNode({
|
|
1065
|
+
tools: [handoffTool],
|
|
1066
|
+
directToolNames: new Set(['execute_code']),
|
|
1067
|
+
});
|
|
1068
|
+
const result = (await node.invoke({
|
|
1069
|
+
messages: [
|
|
1070
|
+
new AIMessage({
|
|
1071
|
+
content: '',
|
|
1072
|
+
tool_calls: [{ id: 'wrong_handoff', name: wrongName, args: {} }],
|
|
1073
|
+
}),
|
|
1074
|
+
],
|
|
1075
|
+
})) as { messages: ToolMessage[] };
|
|
1076
|
+
const wrongNameMessage = result.messages.find(
|
|
1077
|
+
(msg) => msg.tool_call_id === 'wrong_handoff'
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
expect(wrongNameMessage).toBeDefined();
|
|
1081
|
+
expect(wrongNameMessage?.status).toBe('error');
|
|
1082
|
+
expect(wrongNameMessage?.content).toContain(
|
|
1083
|
+
`Did you mean "${correctName}"`
|
|
1084
|
+
);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
it('should keep event-driven unknown handoffs local without direct tool names', async () => {
|
|
1088
|
+
const executedToolNames: string[] = [];
|
|
1089
|
+
const correctName = `${Constants.LC_TRANSFER_TO_}data_analyst`;
|
|
1090
|
+
const wrongName = `${correctName}_analyst`;
|
|
1091
|
+
const lookupName = 'lookup_sessions';
|
|
1092
|
+
const handoffTool = new DynamicStructuredTool({
|
|
1093
|
+
name: correctName,
|
|
1094
|
+
description: 'Transfer to data analyst',
|
|
1095
|
+
schema: { type: 'object', properties: {}, required: [] },
|
|
1096
|
+
func: async (): Promise<string> => 'transferred',
|
|
1097
|
+
}) as t.GenericTool;
|
|
1098
|
+
const lookupTool = new DynamicStructuredTool({
|
|
1099
|
+
name: lookupName,
|
|
1100
|
+
description: 'List upload sessions',
|
|
1101
|
+
schema: { type: 'object', properties: {}, required: [] },
|
|
1102
|
+
func: async (): Promise<string> => 'sessions',
|
|
1103
|
+
}) as t.GenericTool;
|
|
1104
|
+
const dispatchSpy = jest
|
|
1105
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
1106
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
1107
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
1111
|
+
executedToolNames.push(
|
|
1112
|
+
...batch.toolCalls.map((toolCall) => toolCall.name)
|
|
1113
|
+
);
|
|
1114
|
+
batch.resolve(
|
|
1115
|
+
batch.toolCalls.map((toolCall) => ({
|
|
1116
|
+
toolCallId: toolCall.id,
|
|
1117
|
+
status: 'success' as const,
|
|
1118
|
+
content: `host result for ${toolCall.name}`,
|
|
1119
|
+
}))
|
|
1120
|
+
);
|
|
1121
|
+
});
|
|
1122
|
+
const node = new ToolNode({
|
|
1123
|
+
tools: [handoffTool, lookupTool],
|
|
1124
|
+
eventDrivenMode: true,
|
|
1125
|
+
toolCallStepIds: new Map([
|
|
1126
|
+
['lookup_call', 'step_lookup'],
|
|
1127
|
+
['wrong_handoff', 'step_wrong_handoff'],
|
|
1128
|
+
]),
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
try {
|
|
1132
|
+
const result = (await node.invoke({
|
|
1133
|
+
messages: [
|
|
1134
|
+
new AIMessage({
|
|
1135
|
+
content: '',
|
|
1136
|
+
tool_calls: [
|
|
1137
|
+
{ id: 'lookup_call', name: lookupName, args: {} },
|
|
1138
|
+
{ id: 'wrong_handoff', name: wrongName, args: {} },
|
|
1139
|
+
],
|
|
1140
|
+
}),
|
|
1141
|
+
],
|
|
1142
|
+
})) as { messages: ToolMessage[] };
|
|
1143
|
+
const wrongNameMessage = result.messages.find(
|
|
1144
|
+
(msg) => msg.tool_call_id === 'wrong_handoff'
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
expect(executedToolNames).toEqual([lookupName]);
|
|
1148
|
+
expect(wrongNameMessage).toBeDefined();
|
|
1149
|
+
expect(wrongNameMessage?.status).toBe('error');
|
|
1150
|
+
expect(wrongNameMessage?.content).toContain(
|
|
1151
|
+
`Did you mean "${correctName}"`
|
|
1152
|
+
);
|
|
1153
|
+
} finally {
|
|
1154
|
+
dispatchSpy.mockRestore();
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it('should not dispatch mistyped graph handoffs to event-driven tool hosts', async () => {
|
|
1159
|
+
const executedToolNames: string[] = [];
|
|
1160
|
+
const agents: t.AgentInputs[] = [
|
|
1161
|
+
{
|
|
1162
|
+
...createBasicAgent('router', 'You are a router'),
|
|
1163
|
+
toolDefinitions: [
|
|
1164
|
+
{
|
|
1165
|
+
name: 'lookup_sessions',
|
|
1166
|
+
description: 'List upload sessions',
|
|
1167
|
+
parameters: {
|
|
1168
|
+
type: 'object',
|
|
1169
|
+
properties: {},
|
|
1170
|
+
required: [],
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
],
|
|
1174
|
+
},
|
|
1175
|
+
createBasicAgent('data_analyst', 'You are a data analyst'),
|
|
1176
|
+
];
|
|
1177
|
+
|
|
1178
|
+
const edges: t.GraphEdge[] = [
|
|
1179
|
+
{
|
|
1180
|
+
from: 'router',
|
|
1181
|
+
to: 'data_analyst',
|
|
1182
|
+
edgeType: 'handoff',
|
|
1183
|
+
},
|
|
1184
|
+
];
|
|
1185
|
+
|
|
1186
|
+
const run = await Run.create({
|
|
1187
|
+
...createTestConfig(agents, edges),
|
|
1188
|
+
customHandlers: {
|
|
1189
|
+
[GraphEvents.ON_TOOL_EXECUTE]: {
|
|
1190
|
+
handle: (_event: string, data: t.StreamEventData): void => {
|
|
1191
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
1192
|
+
executedToolNames.push(
|
|
1193
|
+
...batch.toolCalls.map((toolCall) => toolCall.name)
|
|
1194
|
+
);
|
|
1195
|
+
batch.resolve(
|
|
1196
|
+
batch.toolCalls.map((toolCall) => ({
|
|
1197
|
+
toolCallId: toolCall.id,
|
|
1198
|
+
status: 'success' as const,
|
|
1199
|
+
content: `host result for ${toolCall.name}`,
|
|
1200
|
+
}))
|
|
1201
|
+
);
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
});
|
|
1206
|
+
const correctName = `${Constants.LC_TRANSFER_TO_}data_analyst`;
|
|
1207
|
+
const wrongName = `${correctName}_analyst`;
|
|
1208
|
+
|
|
1209
|
+
run.Graph?.overrideTestModel(
|
|
1210
|
+
['Checking sessions and transferring', 'Handled invalid transfer'],
|
|
1211
|
+
10,
|
|
1212
|
+
[
|
|
1213
|
+
{
|
|
1214
|
+
id: 'tool_call_lookup',
|
|
1215
|
+
name: 'lookup_sessions',
|
|
1216
|
+
args: {},
|
|
1217
|
+
} as ToolCall,
|
|
1218
|
+
{
|
|
1219
|
+
id: 'tool_call_wrong_handoff',
|
|
1220
|
+
name: wrongName,
|
|
1221
|
+
args: { instructions: 'Analyze the upload session data' },
|
|
1222
|
+
} as ToolCall,
|
|
1223
|
+
]
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
const config: Partial<RunnableConfig> & {
|
|
1227
|
+
version: 'v1' | 'v2';
|
|
1228
|
+
streamMode: string;
|
|
1229
|
+
} = {
|
|
1230
|
+
configurable: {
|
|
1231
|
+
thread_id: 'test-event-wrong-handoff-name-thread',
|
|
1232
|
+
},
|
|
1233
|
+
streamMode: 'values',
|
|
1234
|
+
version: 'v2' as const,
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
await run.processStream(
|
|
1238
|
+
{ messages: [new HumanMessage('Check my sessions and analyze them')] },
|
|
1239
|
+
config
|
|
1240
|
+
);
|
|
1241
|
+
|
|
1242
|
+
const toolMessages = run
|
|
1243
|
+
.getRunMessages()!
|
|
1244
|
+
.filter((msg) => msg.getType() === 'tool') as ToolMessage[];
|
|
1245
|
+
const wrongNameMessage = toolMessages.find(
|
|
1246
|
+
(msg) => msg.name === wrongName
|
|
1247
|
+
);
|
|
1248
|
+
|
|
1249
|
+
expect(executedToolNames).toEqual(['lookup_sessions']);
|
|
1250
|
+
expect(wrongNameMessage).toBeDefined();
|
|
1251
|
+
expect(wrongNameMessage?.status).toBe('error');
|
|
1252
|
+
expect(wrongNameMessage?.content).toContain(
|
|
1253
|
+
`Did you mean "${correctName}"`
|
|
1254
|
+
);
|
|
1255
|
+
});
|
|
992
1256
|
});
|
|
993
1257
|
});
|
|
@@ -21,6 +21,11 @@ const hasOpenRouter = (process.env.OPENROUTER_API_KEY ?? '').trim() !== '';
|
|
|
21
21
|
const describeIf = hasOpenRouter ? describe : describe.skip;
|
|
22
22
|
|
|
23
23
|
const provider = Providers.OPENROUTER;
|
|
24
|
+
const LIVE_MAX_TOKENS = 4096;
|
|
25
|
+
const GEMINI_REASONING_MODEL = 'google/gemini-3.1-pro-preview';
|
|
26
|
+
const ANTHROPIC_REASONING_MODEL = 'anthropic/claude-sonnet-4';
|
|
27
|
+
const ANTHROPIC_LATEST_REASONING_MODEL = 'anthropic/claude-sonnet-4.6';
|
|
28
|
+
|
|
24
29
|
describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
25
30
|
jest.setTimeout(60000);
|
|
26
31
|
let run: Run<t.IState>;
|
|
@@ -75,6 +80,7 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
75
80
|
const llmConfig = {
|
|
76
81
|
...baseWithoutReasoning,
|
|
77
82
|
model: opts.model,
|
|
83
|
+
maxTokens: LIVE_MAX_TOKENS,
|
|
78
84
|
...(opts.reasoning != null ? { reasoning: opts.reasoning } : {}),
|
|
79
85
|
} as t.LLMConfig;
|
|
80
86
|
const customHandlers = setupCustomHandlers();
|
|
@@ -178,7 +184,8 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
178
184
|
baseLLMConfig as unknown as Record<string, unknown>;
|
|
179
185
|
const llmConfig = {
|
|
180
186
|
...baseWithoutReasoning,
|
|
181
|
-
model:
|
|
187
|
+
model: ANTHROPIC_REASONING_MODEL,
|
|
188
|
+
maxTokens: LIVE_MAX_TOKENS,
|
|
182
189
|
} as t.LLMConfig;
|
|
183
190
|
const customHandlers = setupCustomHandlers();
|
|
184
191
|
|
|
@@ -214,7 +221,7 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
214
221
|
|
|
215
222
|
test(`${capitalizeFirstLetter(provider)}: Gemini 3 reasons by default (no config)`, async () => {
|
|
216
223
|
await runReasoningTest({
|
|
217
|
-
model:
|
|
224
|
+
model: GEMINI_REASONING_MODEL,
|
|
218
225
|
reasoning: undefined,
|
|
219
226
|
threadId: 'or-gemini-default-1',
|
|
220
227
|
runId: 'or-gemini-default-1',
|
|
@@ -223,7 +230,7 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
223
230
|
|
|
224
231
|
test(`${capitalizeFirstLetter(provider)}: Gemini reasoning with max_tokens`, async () => {
|
|
225
232
|
await runReasoningTest({
|
|
226
|
-
model:
|
|
233
|
+
model: GEMINI_REASONING_MODEL,
|
|
227
234
|
reasoning: { max_tokens: 4000 },
|
|
228
235
|
threadId: 'or-gemini-reasoning-1',
|
|
229
236
|
runId: 'or-gemini-reasoning-1',
|
|
@@ -232,7 +239,7 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
232
239
|
|
|
233
240
|
test(`${capitalizeFirstLetter(provider)}: Gemini reasoning with effort`, async () => {
|
|
234
241
|
await runReasoningTest({
|
|
235
|
-
model:
|
|
242
|
+
model: GEMINI_REASONING_MODEL,
|
|
236
243
|
reasoning: { effort: 'low' },
|
|
237
244
|
threadId: 'or-gemini-effort-1',
|
|
238
245
|
runId: 'or-gemini-effort-1',
|
|
@@ -241,7 +248,7 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
241
248
|
|
|
242
249
|
test(`${capitalizeFirstLetter(provider)}: Anthropic reasoning with max_tokens`, async () => {
|
|
243
250
|
await runReasoningTest({
|
|
244
|
-
model:
|
|
251
|
+
model: ANTHROPIC_REASONING_MODEL,
|
|
245
252
|
reasoning: { max_tokens: 4000 },
|
|
246
253
|
threadId: 'or-anthropic-reasoning-1',
|
|
247
254
|
runId: 'or-anthropic-reasoning-1',
|
|
@@ -250,16 +257,16 @@ describeIf(`${capitalizeFirstLetter(provider)} Streaming Tests`, () => {
|
|
|
250
257
|
|
|
251
258
|
test(`${capitalizeFirstLetter(provider)}: Anthropic sonnet-4 reasoning with effort`, async () => {
|
|
252
259
|
await runReasoningTest({
|
|
253
|
-
model:
|
|
260
|
+
model: ANTHROPIC_REASONING_MODEL,
|
|
254
261
|
reasoning: { effort: 'medium' },
|
|
255
262
|
threadId: 'or-anthropic-effort-s4-1',
|
|
256
263
|
runId: 'or-anthropic-effort-s4-1',
|
|
257
264
|
});
|
|
258
265
|
});
|
|
259
266
|
|
|
260
|
-
test(`${capitalizeFirstLetter(provider)}: Anthropic sonnet-4
|
|
267
|
+
test(`${capitalizeFirstLetter(provider)}: Anthropic sonnet-4.6 reasoning with effort`, async () => {
|
|
261
268
|
await runReasoningTest({
|
|
262
|
-
model:
|
|
269
|
+
model: ANTHROPIC_LATEST_REASONING_MODEL,
|
|
263
270
|
reasoning: { effort: 'medium' },
|
|
264
271
|
threadId: 'or-anthropic-effort-s46-1',
|
|
265
272
|
runId: 'or-anthropic-effort-s46-1',
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -131,6 +131,10 @@ function isSend(value: unknown): value is Send {
|
|
|
131
131
|
return value instanceof Send;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function isHandoffToolName(name: string): boolean {
|
|
135
|
+
return name.startsWith(Constants.LC_TRANSFER_TO_);
|
|
136
|
+
}
|
|
137
|
+
|
|
134
138
|
/**
|
|
135
139
|
* Format a fail-closed diagnostic for malformed approval-decision
|
|
136
140
|
* fields. Hosts deserialize resume payloads from untyped JSON, so
|
|
@@ -581,6 +585,72 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
581
585
|
return this.fileCheckpointer;
|
|
582
586
|
}
|
|
583
587
|
|
|
588
|
+
private *getRegisteredHandoffNames(): IterableIterator<string> {
|
|
589
|
+
if (this.directToolNames != null) {
|
|
590
|
+
for (const toolName of this.directToolNames) {
|
|
591
|
+
yield toolName;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
for (const toolName of this.toolMap.keys()) {
|
|
596
|
+
if (this.directToolNames?.has(toolName) === true) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
yield toolName;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private hasRegisteredHandoffTool(): boolean {
|
|
604
|
+
for (const toolName of this.getRegisteredHandoffNames()) {
|
|
605
|
+
if (isHandoffToolName(toolName)) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private getHandoffToolNameSuggestion(callName: string): string | undefined {
|
|
613
|
+
if (!isHandoffToolName(callName)) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let suggestion: string | undefined;
|
|
618
|
+
for (const toolName of this.getRegisteredHandoffNames()) {
|
|
619
|
+
if (
|
|
620
|
+
!isHandoffToolName(toolName) ||
|
|
621
|
+
toolName.length >= callName.length ||
|
|
622
|
+
!callName.startsWith(toolName)
|
|
623
|
+
) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (suggestion == null || toolName.length > suggestion.length) {
|
|
627
|
+
suggestion = toolName;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return suggestion;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
private shouldHandleUnknownHandoffLocally(
|
|
634
|
+
callName: string,
|
|
635
|
+
hasRegisteredHandoffTool?: boolean
|
|
636
|
+
): boolean {
|
|
637
|
+
if (!isHandoffToolName(callName) || this.toolMap.has(callName)) {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
return hasRegisteredHandoffTool ?? this.hasRegisteredHandoffTool();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private getUnknownToolErrorMessage(callName: string): string {
|
|
644
|
+
const suggestion = this.getHandoffToolNameSuggestion(callName);
|
|
645
|
+
if (suggestion == null) {
|
|
646
|
+
return `Tool "${callName}" not found.`;
|
|
647
|
+
}
|
|
648
|
+
return (
|
|
649
|
+
`Tool "${callName}" not found. Did you mean "${suggestion}"? ` +
|
|
650
|
+
'Handoff tool names must match exactly.'
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
584
654
|
/**
|
|
585
655
|
* Flush the per-Run direct-path turn cache. Called by the Graph at
|
|
586
656
|
* end-of-Run via `clearHeavyState`. The map intentionally survives
|
|
@@ -699,7 +769,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
699
769
|
batchScopeId ?? (config.configurable?.run_id as string | undefined);
|
|
700
770
|
try {
|
|
701
771
|
if (tool === undefined) {
|
|
702
|
-
throw new Error(
|
|
772
|
+
throw new Error(this.getUnknownToolErrorMessage(call.name));
|
|
703
773
|
}
|
|
704
774
|
/**
|
|
705
775
|
* `usageCount` is the per-tool-name invocation index that
|
|
@@ -2742,8 +2812,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2742
2812
|
let outputs: (BaseMessage | Command)[];
|
|
2743
2813
|
|
|
2744
2814
|
if (this.isSendInput(input)) {
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2815
|
+
const isLocalTool =
|
|
2816
|
+
this.directToolNames?.has(input.lg_tool_call.name) === true ||
|
|
2817
|
+
this.shouldHandleUnknownHandoffLocally(input.lg_tool_call.name);
|
|
2818
|
+
if (this.eventDrivenMode && !isLocalTool) {
|
|
2747
2819
|
return this.executeViaEvent([input.lg_tool_call], config, input, {
|
|
2748
2820
|
batchIndices: [0],
|
|
2749
2821
|
turn,
|
|
@@ -2848,28 +2920,35 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
2848
2920
|
}) ?? [];
|
|
2849
2921
|
|
|
2850
2922
|
if (this.eventDrivenMode && filteredCalls.length > 0) {
|
|
2851
|
-
const
|
|
2852
|
-
|
|
2853
|
-
if (!this.directToolNames || this.directToolNames.size === 0) {
|
|
2854
|
-
return this.executeViaEvent(filteredCalls, config, input, {
|
|
2855
|
-
batchIndices: filteredIndices,
|
|
2856
|
-
turn,
|
|
2857
|
-
batchScopeId,
|
|
2858
|
-
});
|
|
2859
|
-
}
|
|
2923
|
+
const directToolNames = this.directToolNames;
|
|
2924
|
+
const hasRegisteredHandoffTool = this.hasRegisteredHandoffTool();
|
|
2860
2925
|
|
|
2861
2926
|
const directEntries: Array<{ call: ToolCall; batchIndex: number }> = [];
|
|
2862
2927
|
const eventEntries: Array<{ call: ToolCall; batchIndex: number }> = [];
|
|
2863
2928
|
for (let i = 0; i < filteredCalls.length; i++) {
|
|
2864
2929
|
const call = filteredCalls[i];
|
|
2865
2930
|
const entry = { call, batchIndex: i };
|
|
2866
|
-
if (
|
|
2931
|
+
if (
|
|
2932
|
+
directToolNames?.has(call.name) === true ||
|
|
2933
|
+
this.shouldHandleUnknownHandoffLocally(
|
|
2934
|
+
call.name,
|
|
2935
|
+
hasRegisteredHandoffTool
|
|
2936
|
+
)
|
|
2937
|
+
) {
|
|
2867
2938
|
directEntries.push(entry);
|
|
2868
2939
|
} else {
|
|
2869
2940
|
eventEntries.push(entry);
|
|
2870
2941
|
}
|
|
2871
2942
|
}
|
|
2872
2943
|
|
|
2944
|
+
if (directEntries.length === 0) {
|
|
2945
|
+
return this.executeViaEvent(filteredCalls, config, input, {
|
|
2946
|
+
batchIndices: eventEntries.map((entry) => entry.batchIndex),
|
|
2947
|
+
turn,
|
|
2948
|
+
batchScopeId,
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2873
2952
|
const directCalls = directEntries.map((e) => e.call);
|
|
2874
2953
|
const directIndices = directEntries.map((e) => e.batchIndex);
|
|
2875
2954
|
const eventCalls = eventEntries.map((e) => e.call);
|
package/src/types/run.ts
CHANGED
|
@@ -216,6 +216,8 @@ export type TokenBudgetBreakdown = {
|
|
|
216
216
|
instructionTokens: number;
|
|
217
217
|
/** Tokens from the system message text alone. */
|
|
218
218
|
systemMessageTokens: number;
|
|
219
|
+
/** Tokens from instruction text emitted outside the system message. */
|
|
220
|
+
dynamicInstructionTokens: number;
|
|
219
221
|
/** Tokens from tool schema definitions. */
|
|
220
222
|
toolSchemaTokens: number;
|
|
221
223
|
/** Tokens from the conversation summary. */
|