@librechat/agents 3.1.64 → 3.1.66-dev.0

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 (126) hide show
  1. package/dist/cjs/common/enum.cjs +13 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +3 -0
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/HookRegistry.cjs +162 -0
  6. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -0
  7. package/dist/cjs/hooks/executeHooks.cjs +276 -0
  8. package/dist/cjs/hooks/executeHooks.cjs.map +1 -0
  9. package/dist/cjs/hooks/matchers.cjs +256 -0
  10. package/dist/cjs/hooks/matchers.cjs.map +1 -0
  11. package/dist/cjs/hooks/types.cjs +27 -0
  12. package/dist/cjs/hooks/types.cjs.map +1 -0
  13. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  14. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +69 -54
  15. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +40 -0
  17. package/dist/cjs/main.cjs.map +1 -1
  18. package/dist/cjs/messages/core.cjs +8 -1
  19. package/dist/cjs/messages/core.cjs.map +1 -1
  20. package/dist/cjs/messages/format.cjs +74 -12
  21. package/dist/cjs/messages/format.cjs.map +1 -1
  22. package/dist/cjs/run.cjs +111 -0
  23. package/dist/cjs/run.cjs.map +1 -1
  24. package/dist/cjs/tools/BashExecutor.cjs +175 -0
  25. package/dist/cjs/tools/BashExecutor.cjs.map +1 -0
  26. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +296 -0
  27. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -0
  28. package/dist/cjs/tools/ReadFile.cjs +43 -0
  29. package/dist/cjs/tools/ReadFile.cjs.map +1 -0
  30. package/dist/cjs/tools/SkillTool.cjs +50 -0
  31. package/dist/cjs/tools/SkillTool.cjs.map +1 -0
  32. package/dist/cjs/tools/ToolNode.cjs +304 -140
  33. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  34. package/dist/cjs/tools/skillCatalog.cjs +84 -0
  35. package/dist/cjs/tools/skillCatalog.cjs.map +1 -0
  36. package/dist/esm/common/enum.mjs +12 -1
  37. package/dist/esm/common/enum.mjs.map +1 -1
  38. package/dist/esm/graphs/Graph.mjs +3 -0
  39. package/dist/esm/graphs/Graph.mjs.map +1 -1
  40. package/dist/esm/hooks/HookRegistry.mjs +160 -0
  41. package/dist/esm/hooks/HookRegistry.mjs.map +1 -0
  42. package/dist/esm/hooks/executeHooks.mjs +273 -0
  43. package/dist/esm/hooks/executeHooks.mjs.map +1 -0
  44. package/dist/esm/hooks/matchers.mjs +251 -0
  45. package/dist/esm/hooks/matchers.mjs.map +1 -0
  46. package/dist/esm/hooks/types.mjs +25 -0
  47. package/dist/esm/hooks/types.mjs.map +1 -0
  48. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  49. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +69 -54
  50. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  51. package/dist/esm/main.mjs +10 -1
  52. package/dist/esm/main.mjs.map +1 -1
  53. package/dist/esm/messages/core.mjs +8 -1
  54. package/dist/esm/messages/core.mjs.map +1 -1
  55. package/dist/esm/messages/format.mjs +66 -4
  56. package/dist/esm/messages/format.mjs.map +1 -1
  57. package/dist/esm/run.mjs +111 -0
  58. package/dist/esm/run.mjs.map +1 -1
  59. package/dist/esm/tools/BashExecutor.mjs +169 -0
  60. package/dist/esm/tools/BashExecutor.mjs.map +1 -0
  61. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +287 -0
  62. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -0
  63. package/dist/esm/tools/ReadFile.mjs +38 -0
  64. package/dist/esm/tools/ReadFile.mjs.map +1 -0
  65. package/dist/esm/tools/SkillTool.mjs +45 -0
  66. package/dist/esm/tools/SkillTool.mjs.map +1 -0
  67. package/dist/esm/tools/ToolNode.mjs +306 -142
  68. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  69. package/dist/esm/tools/skillCatalog.mjs +82 -0
  70. package/dist/esm/tools/skillCatalog.mjs.map +1 -0
  71. package/dist/types/common/enum.d.ts +7 -1
  72. package/dist/types/graphs/Graph.d.ts +2 -0
  73. package/dist/types/hooks/HookRegistry.d.ts +56 -0
  74. package/dist/types/hooks/executeHooks.d.ts +79 -0
  75. package/dist/types/hooks/index.d.ts +6 -0
  76. package/dist/types/hooks/matchers.d.ts +95 -0
  77. package/dist/types/hooks/types.d.ts +309 -0
  78. package/dist/types/index.d.ts +6 -0
  79. package/dist/types/llm/anthropic/types.d.ts +1 -1
  80. package/dist/types/messages/format.d.ts +2 -1
  81. package/dist/types/run.d.ts +1 -0
  82. package/dist/types/tools/BashExecutor.d.ts +45 -0
  83. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +72 -0
  84. package/dist/types/tools/ReadFile.d.ts +28 -0
  85. package/dist/types/tools/SkillTool.d.ts +40 -0
  86. package/dist/types/tools/ToolNode.d.ts +24 -2
  87. package/dist/types/tools/skillCatalog.d.ts +19 -0
  88. package/dist/types/types/index.d.ts +1 -0
  89. package/dist/types/types/run.d.ts +20 -0
  90. package/dist/types/types/skill.d.ts +9 -0
  91. package/dist/types/types/tools.d.ts +38 -1
  92. package/package.json +2 -2
  93. package/src/common/enum.ts +12 -0
  94. package/src/graphs/Graph.ts +4 -0
  95. package/src/hooks/HookRegistry.ts +208 -0
  96. package/src/hooks/__tests__/HookRegistry.test.ts +190 -0
  97. package/src/hooks/__tests__/executeHooks.test.ts +1013 -0
  98. package/src/hooks/__tests__/integration.test.ts +337 -0
  99. package/src/hooks/__tests__/matchers.test.ts +238 -0
  100. package/src/hooks/__tests__/toolHooks.test.ts +669 -0
  101. package/src/hooks/executeHooks.ts +375 -0
  102. package/src/hooks/index.ts +55 -0
  103. package/src/hooks/matchers.ts +280 -0
  104. package/src/hooks/types.ts +388 -0
  105. package/src/index.ts +8 -0
  106. package/src/llm/anthropic/types.ts +1 -1
  107. package/src/llm/anthropic/utils/message_inputs.ts +93 -68
  108. package/src/llm/anthropic/utils/server-tool-inputs.test.ts +349 -0
  109. package/src/messages/core.ts +8 -1
  110. package/src/messages/format.ts +74 -4
  111. package/src/messages/formatAgentMessages.skills.test.ts +334 -0
  112. package/src/run.ts +126 -0
  113. package/src/tools/BashExecutor.ts +205 -0
  114. package/src/tools/BashProgrammaticToolCalling.ts +397 -0
  115. package/src/tools/ReadFile.ts +39 -0
  116. package/src/tools/SkillTool.ts +46 -0
  117. package/src/tools/ToolNode.ts +391 -169
  118. package/src/tools/__tests__/ReadFile.test.ts +44 -0
  119. package/src/tools/__tests__/SkillTool.test.ts +442 -0
  120. package/src/tools/__tests__/ToolNode.session.test.ts +12 -12
  121. package/src/tools/__tests__/skillCatalog.test.ts +161 -0
  122. package/src/tools/skillCatalog.ts +126 -0
  123. package/src/types/index.ts +1 -0
  124. package/src/types/run.ts +20 -0
  125. package/src/types/skill.ts +11 -0
  126. package/src/types/tools.ts +41 -1
@@ -0,0 +1,349 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
3
+ import type { BaseMessage } from '@langchain/core/messages';
4
+ import { _convertMessagesToAnthropicPayload } from './message_inputs';
5
+
6
+ describe('_convertMessagesToAnthropicPayload — server tool use (web search) multi-turn', () => {
7
+ it('corrects tool_use blocks with srvtoolu_ IDs to server_tool_use', () => {
8
+ const messageHistory: BaseMessage[] = [
9
+ new HumanMessage('search for X and Y'),
10
+ new AIMessage({
11
+ content: [
12
+ { type: 'text', text: 'I will search for that.' },
13
+ {
14
+ type: 'tool_use',
15
+ id: 'srvtoolu_1',
16
+ name: 'web_search',
17
+ input: { query: 'X' },
18
+ },
19
+ {
20
+ type: 'web_search_tool_result',
21
+ tool_use_id: 'srvtoolu_1',
22
+ content: [
23
+ {
24
+ type: 'web_search_result',
25
+ url: 'https://example.com',
26
+ title: 'Result',
27
+ encrypted_content: 'abc',
28
+ page_age: '1d',
29
+ },
30
+ ],
31
+ },
32
+ {
33
+ type: 'tool_use',
34
+ id: 'srvtoolu_2',
35
+ name: 'web_search',
36
+ input: { query: 'Y' },
37
+ },
38
+ {
39
+ type: 'web_search_tool_result',
40
+ tool_use_id: 'srvtoolu_2',
41
+ content: [
42
+ {
43
+ type: 'web_search_result',
44
+ url: 'https://example2.com',
45
+ title: 'Result 2',
46
+ encrypted_content: 'def',
47
+ page_age: '2d',
48
+ },
49
+ ],
50
+ },
51
+ { type: 'text', text: 'Here are the results.' },
52
+ ],
53
+ tool_calls: [
54
+ {
55
+ id: 'srvtoolu_1',
56
+ name: 'web_search',
57
+ args: { query: 'X' },
58
+ type: 'tool_call',
59
+ },
60
+ {
61
+ id: 'srvtoolu_2',
62
+ name: 'web_search',
63
+ args: { query: 'Y' },
64
+ type: 'tool_call',
65
+ },
66
+ ],
67
+ }),
68
+ new HumanMessage('follow up question'),
69
+ ];
70
+
71
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
72
+ expect(messages).toHaveLength(3);
73
+
74
+ const assistantContent = messages[1].content as any[];
75
+ const serverToolBlocks = assistantContent.filter(
76
+ (b: any) => b.type === 'server_tool_use'
77
+ );
78
+ const searchResultBlocks = assistantContent.filter(
79
+ (b: any) => b.type === 'web_search_tool_result'
80
+ );
81
+ const regularToolUseBlocks = assistantContent.filter(
82
+ (b: any) => b.type === 'tool_use' && b.id?.startsWith('srvtoolu_')
83
+ );
84
+
85
+ expect(serverToolBlocks).toHaveLength(2);
86
+ expect(searchResultBlocks).toHaveLength(2);
87
+ expect(regularToolUseBlocks).toHaveLength(0);
88
+
89
+ // Verify blocks are clean (no extra streaming properties)
90
+ for (const b of serverToolBlocks) {
91
+ expect(Object.keys(b).sort()).toEqual(
92
+ ['id', 'input', 'name', 'type'].sort()
93
+ );
94
+ }
95
+ for (const b of searchResultBlocks) {
96
+ expect(Object.keys(b).sort()).toEqual(
97
+ ['content', 'tool_use_id', 'type'].sort()
98
+ );
99
+ }
100
+ });
101
+
102
+ it('corrects text-typed server tool blocks back to correct types', () => {
103
+ const messageHistory: BaseMessage[] = [
104
+ new HumanMessage('search for X'),
105
+ new AIMessage({
106
+ content: [
107
+ {
108
+ type: 'text',
109
+ id: 'srvtoolu_1',
110
+ name: 'web_search',
111
+ input: '{"query":"X"}',
112
+ },
113
+ {
114
+ type: 'text',
115
+ tool_use_id: 'srvtoolu_1',
116
+ content: [
117
+ {
118
+ type: 'web_search_result',
119
+ url: 'https://example.com',
120
+ title: 'Result',
121
+ encrypted_content: 'abc',
122
+ page_age: '1d',
123
+ },
124
+ ],
125
+ },
126
+ { type: 'text', text: 'Found results.' },
127
+ ],
128
+ tool_calls: [
129
+ {
130
+ id: 'srvtoolu_1',
131
+ name: 'web_search',
132
+ args: { query: 'X' },
133
+ type: 'tool_call',
134
+ },
135
+ ],
136
+ }),
137
+ new HumanMessage('follow up'),
138
+ ];
139
+
140
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
141
+ const assistantContent = messages[1].content as any[];
142
+
143
+ expect(assistantContent[0]).toEqual({
144
+ type: 'server_tool_use',
145
+ id: 'srvtoolu_1',
146
+ name: 'web_search',
147
+ input: { query: 'X' },
148
+ });
149
+ expect(assistantContent[1].type).toBe('web_search_tool_result');
150
+ expect(assistantContent[1].tool_use_id).toBe('srvtoolu_1');
151
+ expect(assistantContent[2]).toEqual({
152
+ type: 'text',
153
+ text: 'Found results.',
154
+ });
155
+ });
156
+
157
+ it('filters server tool calls when content is a string', () => {
158
+ const messageHistory: BaseMessage[] = [
159
+ new HumanMessage('search for X'),
160
+ new AIMessage({
161
+ content: 'I searched and found results.',
162
+ tool_calls: [
163
+ {
164
+ id: 'srvtoolu_1',
165
+ name: 'web_search',
166
+ args: { query: 'X' },
167
+ type: 'tool_call',
168
+ },
169
+ {
170
+ id: 'toolu_regular',
171
+ name: 'calculator',
172
+ args: { expr: '2+2' },
173
+ type: 'tool_call',
174
+ },
175
+ ],
176
+ }),
177
+ new ToolMessage({
178
+ content: '4',
179
+ tool_call_id: 'toolu_regular',
180
+ }),
181
+ new HumanMessage('follow up'),
182
+ ];
183
+
184
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
185
+ const assistantContent = messages[1].content as any[];
186
+
187
+ const toolUseBlocks = assistantContent.filter(
188
+ (b: any) => b.type === 'tool_use'
189
+ );
190
+ expect(toolUseBlocks).toHaveLength(1);
191
+ expect(toolUseBlocks[0].id).toBe('toolu_regular');
192
+
193
+ const serverToolBlocks = assistantContent.filter((b: any) =>
194
+ b.id?.startsWith('srvtoolu_')
195
+ );
196
+ expect(serverToolBlocks).toHaveLength(0);
197
+ });
198
+
199
+ it('handles empty string content with only server tool calls', () => {
200
+ const messageHistory: BaseMessage[] = [
201
+ new HumanMessage('search for X'),
202
+ new AIMessage({
203
+ content: '',
204
+ tool_calls: [
205
+ {
206
+ id: 'srvtoolu_1',
207
+ name: 'web_search',
208
+ args: { query: 'X' },
209
+ type: 'tool_call',
210
+ },
211
+ ],
212
+ }),
213
+ new HumanMessage('follow up'),
214
+ ];
215
+
216
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
217
+ const assistantContent = messages[1].content as any[];
218
+ expect(assistantContent).toHaveLength(1);
219
+ expect(assistantContent[0].type).toBe('text');
220
+ expect(assistantContent[0].text).toBe(' ');
221
+ });
222
+
223
+ it('preserves regular tool_use blocks alongside corrected server tool blocks', () => {
224
+ const messageHistory: BaseMessage[] = [
225
+ new HumanMessage('search for X and calculate 2+2'),
226
+ new AIMessage({
227
+ content: [
228
+ { type: 'text', text: 'Let me help.' },
229
+ {
230
+ type: 'tool_use',
231
+ id: 'srvtoolu_1',
232
+ name: 'web_search',
233
+ input: { query: 'X' },
234
+ },
235
+ {
236
+ type: 'web_search_tool_result',
237
+ tool_use_id: 'srvtoolu_1',
238
+ content: [
239
+ {
240
+ type: 'web_search_result',
241
+ url: 'https://example.com',
242
+ title: 'Result',
243
+ encrypted_content: 'abc',
244
+ page_age: '1d',
245
+ },
246
+ ],
247
+ },
248
+ {
249
+ type: 'tool_use',
250
+ id: 'toolu_calc',
251
+ name: 'calculator',
252
+ input: { expr: '2+2' },
253
+ },
254
+ ],
255
+ tool_calls: [
256
+ {
257
+ id: 'srvtoolu_1',
258
+ name: 'web_search',
259
+ args: { query: 'X' },
260
+ type: 'tool_call',
261
+ },
262
+ {
263
+ id: 'toolu_calc',
264
+ name: 'calculator',
265
+ args: { expr: '2+2' },
266
+ type: 'tool_call',
267
+ },
268
+ ],
269
+ }),
270
+ new ToolMessage({
271
+ content: '4',
272
+ tool_call_id: 'toolu_calc',
273
+ }),
274
+ new HumanMessage('follow up'),
275
+ ];
276
+
277
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
278
+ const assistantContent = messages[1].content as any[];
279
+
280
+ const serverToolUse = assistantContent.filter(
281
+ (b: any) => b.type === 'server_tool_use'
282
+ );
283
+ const webSearchResult = assistantContent.filter(
284
+ (b: any) => b.type === 'web_search_tool_result'
285
+ );
286
+ const regularToolUse = assistantContent.filter(
287
+ (b: any) => b.type === 'tool_use' && !b.id?.startsWith('srvtoolu_')
288
+ );
289
+
290
+ expect(serverToolUse).toHaveLength(1);
291
+ expect(serverToolUse[0].id).toBe('srvtoolu_1');
292
+ expect(webSearchResult).toHaveLength(1);
293
+ expect(regularToolUse).toHaveLength(1);
294
+ expect(regularToolUse[0].id).toBe('toolu_calc');
295
+ });
296
+
297
+ it('filters out empty text blocks from array content', () => {
298
+ const messageHistory: BaseMessage[] = [
299
+ new HumanMessage('search for X'),
300
+ new AIMessage({
301
+ content: [
302
+ { type: 'text', text: '' },
303
+ {
304
+ type: 'server_tool_use',
305
+ id: 'srvtoolu_1',
306
+ name: 'web_search',
307
+ input: { query: 'X' },
308
+ },
309
+ {
310
+ type: 'web_search_tool_result',
311
+ tool_use_id: 'srvtoolu_1',
312
+ content: [
313
+ {
314
+ type: 'web_search_result',
315
+ url: 'https://example.com',
316
+ title: 'Result',
317
+ encrypted_content: 'abc',
318
+ page_age: '1d',
319
+ },
320
+ ],
321
+ },
322
+ { type: 'text', text: '' },
323
+ { type: 'text', text: 'Here are the results.' },
324
+ ],
325
+ tool_calls: [
326
+ {
327
+ id: 'srvtoolu_1',
328
+ name: 'web_search',
329
+ args: { query: 'X' },
330
+ type: 'tool_call',
331
+ },
332
+ ],
333
+ }),
334
+ new HumanMessage('follow up'),
335
+ ];
336
+
337
+ const { messages } = _convertMessagesToAnthropicPayload(messageHistory);
338
+ const assistantContent = messages[1].content as any[];
339
+
340
+ const emptyTextBlocks = assistantContent.filter(
341
+ (b: any) => b.type === 'text' && b.text === ''
342
+ );
343
+ expect(emptyTextBlocks).toHaveLength(0);
344
+
345
+ const textBlocks = assistantContent.filter((b: any) => b.type === 'text');
346
+ expect(textBlocks).toHaveLength(1);
347
+ expect(textBlocks[0].text).toBe('Here are the results.');
348
+ });
349
+ });
@@ -41,7 +41,14 @@ User: ${userMessage[1]}
41
41
  const _allowedTypes = ['image_url', 'text', 'tool_use', 'tool_result'];
42
42
  const allowedTypesByProvider: Record<string, string[]> = {
43
43
  default: _allowedTypes,
44
- [Providers.ANTHROPIC]: [..._allowedTypes, 'thinking', 'redacted_thinking'],
44
+ [Providers.ANTHROPIC]: [
45
+ ..._allowedTypes,
46
+ 'thinking',
47
+ 'redacted_thinking',
48
+ 'server_tool_use',
49
+ 'web_search_tool_result',
50
+ 'web_search_result',
51
+ ],
45
52
  [Providers.BEDROCK]: [..._allowedTypes, 'reasoning_content'],
46
53
  [Providers.OPENAI]: _allowedTypes,
47
54
  };
@@ -797,18 +797,39 @@ function contentPartCharLength(part: MessageContentComplex): number {
797
797
  return len;
798
798
  }
799
799
 
800
+ /** Extracts the skillName from a skill tool_call's args (string or object). */
801
+ function extractSkillName(args: unknown): string | undefined {
802
+ let parsed: Record<string, unknown> | undefined;
803
+ if (typeof args === 'string') {
804
+ try {
805
+ parsed = JSON.parse(args) as Record<string, unknown>;
806
+ } catch {
807
+ /* malformed args — skip */
808
+ }
809
+ } else {
810
+ parsed = args as Record<string, unknown> | undefined;
811
+ }
812
+ const name = parsed?.skillName;
813
+ return typeof name === 'string' && name !== '' ? name : undefined;
814
+ }
815
+
800
816
  /**
801
817
  * Formats an array of messages for LangChain, handling tool calls and creating ToolMessage instances.
802
818
  *
803
819
  * @param payload - The array of messages to format.
804
820
  * @param indexTokenCountMap - Optional map of message indices to token counts.
805
821
  * @param tools - Optional set of tool names that are allowed in the request.
822
+ * @param skills - Optional map of skill name to body for reconstructing skill HumanMessages.
806
823
  * @returns - Object containing formatted messages and updated indexTokenCountMap if provided.
807
824
  */
808
825
  export const formatAgentMessages = (
809
826
  payload: TPayload,
810
827
  indexTokenCountMap?: Record<number, number | undefined>,
811
- tools?: Set<string>
828
+ tools?: Set<string>,
829
+ /** Pre-resolved skill bodies keyed by skill name. When present, HumanMessages
830
+ * are reconstructed after skill ToolMessages to restore skill instructions
831
+ * that were only in LangGraph state during the original run. */
832
+ skills?: Map<string, string>
812
833
  ): {
813
834
  messages: Array<HumanMessage | AIMessage | SystemMessage | ToolMessage>;
814
835
  indexTokenCountMap?: Record<number, number>;
@@ -902,6 +923,7 @@ export const formatAgentMessages = (
902
923
  * - Dynamically expand the set when tool_search results are encountered
903
924
  */
904
925
  let processedMessage = message;
926
+ let pendingSkillNames: Set<string> | undefined;
905
927
  if (discoveredTools) {
906
928
  const content = message.content;
907
929
  if (content != null && Array.isArray(content)) {
@@ -950,8 +972,17 @@ export const formatAgentMessages = (
950
972
  }
951
973
 
952
974
  if (discoveredTools.has(toolName)) {
953
- /** Valid tool - keep it */
954
975
  filteredContent.push(part);
976
+ if (
977
+ toolName === Constants.SKILL_TOOL &&
978
+ skills?.size != null &&
979
+ skills.size > 0
980
+ ) {
981
+ const skillName = extractSkillName(part.tool_call.args) ?? '';
982
+ if (skillName) {
983
+ (pendingSkillNames ??= new Set()).add(skillName);
984
+ }
985
+ }
955
986
  } else {
956
987
  /** Invalid tool - convert to string for context preservation */
957
988
  if (
@@ -1027,6 +1058,25 @@ export const formatAgentMessages = (
1027
1058
  }
1028
1059
  }
1029
1060
 
1061
+ /** When tools filtering is off, still detect skill tool_calls for body reconstruction */
1062
+ if (!discoveredTools && skills?.size != null && skills.size > 0) {
1063
+ const content = processedMessage.content;
1064
+ if (Array.isArray(content)) {
1065
+ for (const part of content) {
1066
+ if (
1067
+ part.type !== ContentTypes.TOOL_CALL ||
1068
+ part.tool_call?.name !== Constants.SKILL_TOOL
1069
+ ) {
1070
+ continue;
1071
+ }
1072
+ const skillName = extractSkillName(part.tool_call.args) ?? '';
1073
+ if (skillName) {
1074
+ (pendingSkillNames ??= new Set()).add(skillName);
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+
1030
1080
  const formattedMessages = formatAssistantMessage(processedMessage);
1031
1081
  if (sourceMessageId != null && sourceMessageId !== '') {
1032
1082
  for (const formattedMessage of formattedMessages) {
@@ -1035,9 +1085,29 @@ export const formatAgentMessages = (
1035
1085
  }
1036
1086
  messages.push(...formattedMessages);
1037
1087
 
1038
- // Update the index mapping for this assistant message
1039
- // Store all indices that were created from this original message
1088
+ // Capture index range BEFORE skill body injection so injected
1089
+ // HumanMessages are excluded from the assistant's token distribution.
1040
1090
  const endMessageIndex = messages.length;
1091
+
1092
+ if (pendingSkillNames?.size != null && pendingSkillNames.size > 0) {
1093
+ for (const skillName of pendingSkillNames) {
1094
+ const body = skills?.get(skillName) ?? '';
1095
+ if (body) {
1096
+ messages.push(
1097
+ new HumanMessage({
1098
+ content: body,
1099
+ additional_kwargs: {
1100
+ role: 'user',
1101
+ isMeta: true,
1102
+ source: 'skill',
1103
+ skillName,
1104
+ },
1105
+ })
1106
+ );
1107
+ }
1108
+ }
1109
+ }
1110
+
1041
1111
  const resultIndices = [];
1042
1112
  for (let j = startMessageIndex; j < endMessageIndex; j++) {
1043
1113
  resultIndices.push(j);