@librechat/agents 3.1.88 → 3.1.89
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 +18 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/stream.cjs +115 -8
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +24 -7
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +8 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +3 -1
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +18 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/stream.mjs +115 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +24 -7
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +8 -1
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +3 -1
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +8 -0
- package/dist/types/types/tools.d.ts +9 -0
- package/dist/types/utils/events.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +1073 -221
- package/src/graphs/Graph.ts +20 -5
- package/src/specs/subagent.test.ts +87 -1
- package/src/stream.ts +163 -12
- package/src/tools/ToolNode.ts +134 -111
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +278 -14
- package/src/types/tools.ts +9 -0
- package/src/utils/events.ts +4 -2
package/src/graphs/Graph.ts
CHANGED
|
@@ -145,6 +145,14 @@ export abstract class Graph<
|
|
|
145
145
|
/** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
|
|
146
146
|
invokedToolIds?: Set<string>;
|
|
147
147
|
handlerRegistry: HandlerRegistry | undefined;
|
|
148
|
+
/**
|
|
149
|
+
* True when event-driven tool execution can be routed through callbacks even
|
|
150
|
+
* though this graph intentionally does not own the full handler registry.
|
|
151
|
+
* Self-spawned subagent graphs use this shape: their callback forwarder sends
|
|
152
|
+
* `ON_TOOL_EXECUTE` to the parent's handler, while child run-step events stay
|
|
153
|
+
* wrapped as `ON_SUBAGENT_UPDATE` instead of leaking as parent events.
|
|
154
|
+
*/
|
|
155
|
+
eventToolExecutionAvailable: boolean = false;
|
|
148
156
|
hookRegistry: HookRegistry | undefined;
|
|
149
157
|
/**
|
|
150
158
|
* Run-scoped HITL configuration. When `humanInTheLoop?.enabled` is
|
|
@@ -167,10 +175,8 @@ export abstract class Graph<
|
|
|
167
175
|
eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
|
|
168
176
|
eagerEventToolExecutions: Map<string, t.EagerEventToolExecution> = new Map();
|
|
169
177
|
eagerEventToolUsageCount: Map<string, number> = new Map();
|
|
170
|
-
private eagerEventToolUsageCountsByAgentId: Map<
|
|
171
|
-
|
|
172
|
-
Map<string, number>
|
|
173
|
-
> = new Map();
|
|
178
|
+
private eagerEventToolUsageCountsByAgentId: Map<string, Map<string, number>> =
|
|
179
|
+
new Map();
|
|
174
180
|
eagerEventToolCallChunks: Map<string, t.EagerEventToolCallChunkState> =
|
|
175
181
|
new Map();
|
|
176
182
|
/**
|
|
@@ -1554,7 +1560,16 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1554
1560
|
parentAgentId: agentContext.agentId,
|
|
1555
1561
|
tokenCounter: agentContext.tokenCounter,
|
|
1556
1562
|
maxDepth: effectiveSubagentDepth,
|
|
1557
|
-
createChildGraph: (input): StandardGraph =>
|
|
1563
|
+
createChildGraph: (input): StandardGraph => {
|
|
1564
|
+
const childGraph = new StandardGraph(input);
|
|
1565
|
+
childGraph.toolOutputReferences = this.toolOutputReferences;
|
|
1566
|
+
childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
|
|
1567
|
+
childGraph.toolExecution = this.toolExecution;
|
|
1568
|
+
childGraph.eventToolExecutionAvailable =
|
|
1569
|
+
this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
|
|
1570
|
+
null;
|
|
1571
|
+
return childGraph;
|
|
1572
|
+
},
|
|
1558
1573
|
});
|
|
1559
1574
|
|
|
1560
1575
|
const subagentTool = tool(async (rawInput, config) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HumanMessage } from '@langchain/core/messages';
|
|
1
|
+
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
|
2
2
|
import { FakeListChatModel } from '@langchain/core/utils/testing';
|
|
3
3
|
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
4
4
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
@@ -220,6 +220,92 @@ describe('Subagent Integration', () => {
|
|
|
220
220
|
expect(subagentTool).toBeDefined();
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
+
it('inherits eager event-tool settings into self-spawn child graphs', async () => {
|
|
224
|
+
const originalCreateWorkflow = StandardGraph.prototype.createWorkflow;
|
|
225
|
+
const observedChildGraphs: Array<{
|
|
226
|
+
eagerEventToolExecution: StandardGraph['eagerEventToolExecution'];
|
|
227
|
+
toolOutputReferences: StandardGraph['toolOutputReferences'];
|
|
228
|
+
eventToolExecutionAvailable: boolean;
|
|
229
|
+
}> = [];
|
|
230
|
+
const createWorkflowSpy = jest
|
|
231
|
+
.spyOn(StandardGraph.prototype, 'createWorkflow')
|
|
232
|
+
.mockImplementation(function (this: StandardGraph) {
|
|
233
|
+
if (this.runId?.includes('_sub_') === true) {
|
|
234
|
+
observedChildGraphs.push({
|
|
235
|
+
eagerEventToolExecution: this.eagerEventToolExecution,
|
|
236
|
+
toolOutputReferences: this.toolOutputReferences,
|
|
237
|
+
eventToolExecutionAvailable: this.eventToolExecutionAvailable,
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
invoke: jest.fn(async () => ({
|
|
241
|
+
messages: [new AIMessage('child done')],
|
|
242
|
+
})),
|
|
243
|
+
} as unknown as ReturnType<StandardGraph['createWorkflow']>;
|
|
244
|
+
}
|
|
245
|
+
return originalCreateWorkflow.call(this);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const agentWithSelfSpawn: t.AgentInputs = {
|
|
249
|
+
agentId: 'self-parent',
|
|
250
|
+
provider: Providers.OPENAI,
|
|
251
|
+
clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
|
|
252
|
+
instructions: 'Agent with self-spawn for context isolation.',
|
|
253
|
+
maxContextTokens: 8000,
|
|
254
|
+
toolDefinitions: [{ name: 'mcp_lookup' }],
|
|
255
|
+
subagentConfigs: [
|
|
256
|
+
{
|
|
257
|
+
type: 'isolated',
|
|
258
|
+
name: 'Isolated Worker',
|
|
259
|
+
description: 'Runs a task with isolated context',
|
|
260
|
+
self: true,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const run = await Run.create<t.IState>({
|
|
266
|
+
runId: `self-spawn-eager-${Date.now()}`,
|
|
267
|
+
graphConfig: {
|
|
268
|
+
type: 'standard',
|
|
269
|
+
agents: [agentWithSelfSpawn],
|
|
270
|
+
},
|
|
271
|
+
customHandlers: {
|
|
272
|
+
[GraphEvents.ON_TOOL_EXECUTE]: {
|
|
273
|
+
handle: async () => undefined,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
eagerEventToolExecution: { enabled: true },
|
|
277
|
+
toolOutputReferences: { enabled: true },
|
|
278
|
+
returnContent: true,
|
|
279
|
+
skipCleanup: true,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const context = (run.Graph as StandardGraph).agentContexts.get(
|
|
283
|
+
'self-parent'
|
|
284
|
+
);
|
|
285
|
+
const subagentTool = (context?.graphTools as t.GenericTool[]).find(
|
|
286
|
+
(tool) => 'name' in tool && tool.name === Constants.SUBAGENT
|
|
287
|
+
);
|
|
288
|
+
expect(subagentTool).toBeDefined();
|
|
289
|
+
|
|
290
|
+
await subagentTool!.invoke(
|
|
291
|
+
{
|
|
292
|
+
description: 'Use your MCP tool.',
|
|
293
|
+
subagent_type: 'isolated',
|
|
294
|
+
},
|
|
295
|
+
callerConfig
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
expect(observedChildGraphs).toEqual([
|
|
299
|
+
{
|
|
300
|
+
eagerEventToolExecution: { enabled: true },
|
|
301
|
+
toolOutputReferences: { enabled: true },
|
|
302
|
+
eventToolExecutionAvailable: true,
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
createWorkflowSpy.mockRestore();
|
|
307
|
+
});
|
|
308
|
+
|
|
223
309
|
it('should not create subagent tool when maxSubagentDepth is 0', async () => {
|
|
224
310
|
const agentWithZeroDepth: t.AgentInputs = {
|
|
225
311
|
...createParentAgent(),
|
package/src/stream.ts
CHANGED
|
@@ -27,11 +27,16 @@ import {
|
|
|
27
27
|
coerceRecordArgs,
|
|
28
28
|
normalizeError,
|
|
29
29
|
} from '@/tools/eagerEventExecution';
|
|
30
|
+
import {
|
|
31
|
+
calculateMaxToolResultChars,
|
|
32
|
+
truncateToolResultContent,
|
|
33
|
+
} from '@/utils/truncation';
|
|
30
34
|
import {
|
|
31
35
|
getStreamedToolCallSeal,
|
|
32
36
|
getStreamedToolCallAdapter,
|
|
33
37
|
type StreamedToolCallSeal,
|
|
34
38
|
} from '@/tools/streamedToolCallSeals';
|
|
39
|
+
import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
|
|
35
40
|
|
|
36
41
|
const LOCAL_CODING_BUNDLE_NAME_SET: ReadonlySet<string> = new Set(
|
|
37
42
|
LOCAL_CODING_BUNDLE_NAMES
|
|
@@ -98,11 +103,22 @@ function getNonEmptyValue(possibleValues: string[]): string | undefined {
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
function isBatchSensitiveToolExecution(graph: StandardGraph): boolean {
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
+
return graph.hookRegistry != null || graph.humanInTheLoop?.enabled === true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasToolOutputReference(value: unknown): boolean {
|
|
110
|
+
if (typeof value === 'string') {
|
|
111
|
+
return TOOL_OUTPUT_REF_PATTERN.test(value);
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
return value.some((item) => hasToolOutputReference(item));
|
|
115
|
+
}
|
|
116
|
+
if (value !== null && typeof value === 'object') {
|
|
117
|
+
return Object.values(value as Record<string, unknown>).some((item) =>
|
|
118
|
+
hasToolOutputReference(item)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
function isDirectGraphTool(
|
|
@@ -194,7 +210,10 @@ function isEagerToolExecutionEnabledForBatch(args: {
|
|
|
194
210
|
) {
|
|
195
211
|
return false;
|
|
196
212
|
}
|
|
197
|
-
if (
|
|
213
|
+
if (
|
|
214
|
+
graph.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) == null &&
|
|
215
|
+
graph.eventToolExecutionAvailable !== true
|
|
216
|
+
) {
|
|
198
217
|
return false;
|
|
199
218
|
}
|
|
200
219
|
return true;
|
|
@@ -257,13 +276,49 @@ function hasPotentialDirectToolInStreamContext(args: {
|
|
|
257
276
|
if ((agentContext?.graphTools?.length ?? 0) > 0) {
|
|
258
277
|
return true;
|
|
259
278
|
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function hasDirectToolCallChunkInBatch(args: {
|
|
283
|
+
graph: StandardGraph;
|
|
284
|
+
agentContext?: AgentContext;
|
|
285
|
+
toolCallChunks?: ToolCallChunk[];
|
|
286
|
+
}): boolean {
|
|
287
|
+
const { graph, agentContext, toolCallChunks } = args;
|
|
260
288
|
return (
|
|
261
|
-
|
|
262
|
-
|
|
289
|
+
toolCallChunks?.some(
|
|
290
|
+
(toolCallChunk) =>
|
|
291
|
+
toolCallChunk.name != null &&
|
|
292
|
+
toolCallChunk.name !== '' &&
|
|
293
|
+
(isDirectGraphTool(toolCallChunk.name, agentContext) ||
|
|
294
|
+
isDirectLocalTool(toolCallChunk.name, graph))
|
|
263
295
|
) === true
|
|
264
296
|
);
|
|
265
297
|
}
|
|
266
298
|
|
|
299
|
+
function hasDirectToolCallChunkStateInStep(args: {
|
|
300
|
+
graph: StandardGraph;
|
|
301
|
+
agentContext?: AgentContext;
|
|
302
|
+
stepKey: string;
|
|
303
|
+
}): boolean {
|
|
304
|
+
const { graph, agentContext, stepKey } = args;
|
|
305
|
+
const prefix = `${stepKey}\u0000`;
|
|
306
|
+
for (const [key, state] of graph.eagerEventToolCallChunks) {
|
|
307
|
+
if (!key.startsWith(prefix)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const name = state.name;
|
|
311
|
+
if (
|
|
312
|
+
name != null &&
|
|
313
|
+
name !== '' &&
|
|
314
|
+
(isDirectGraphTool(name, agentContext) || isDirectLocalTool(name, graph))
|
|
315
|
+
) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
267
322
|
type EagerToolExecutionEntry = {
|
|
268
323
|
id: string;
|
|
269
324
|
toolName: string;
|
|
@@ -298,6 +353,12 @@ function createEagerToolExecutionPlan(args: {
|
|
|
298
353
|
if (hasDirectToolCallInBatch({ graph, agentContext, toolCalls })) {
|
|
299
354
|
return undefined;
|
|
300
355
|
}
|
|
356
|
+
if (
|
|
357
|
+
graph.toolOutputReferences?.enabled === true &&
|
|
358
|
+
toolCalls.some((toolCall) => hasToolOutputReference(toolCall.args))
|
|
359
|
+
) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
301
362
|
|
|
302
363
|
const candidateToolCalls = skipExisting
|
|
303
364
|
? toolCalls.filter((toolCall) => {
|
|
@@ -369,6 +430,7 @@ function startEagerToolExecutions(args: {
|
|
|
369
430
|
return;
|
|
370
431
|
}
|
|
371
432
|
|
|
433
|
+
const records: t.EagerEventToolExecution[] = [];
|
|
372
434
|
const promise: Promise<t.EagerEventToolExecutionOutcome> = new Promise<
|
|
373
435
|
t.ToolExecuteResult[]
|
|
374
436
|
>((resolve, reject) => {
|
|
@@ -407,20 +469,104 @@ function startEagerToolExecutions(args: {
|
|
|
407
469
|
})
|
|
408
470
|
.catch(reject);
|
|
409
471
|
}).then(
|
|
410
|
-
(results): t.EagerEventToolExecutionOutcome =>
|
|
472
|
+
async (results): Promise<t.EagerEventToolExecutionOutcome> => {
|
|
473
|
+
await dispatchEagerToolCompletions({
|
|
474
|
+
graph,
|
|
475
|
+
agentContext,
|
|
476
|
+
records,
|
|
477
|
+
results,
|
|
478
|
+
});
|
|
479
|
+
return { results };
|
|
480
|
+
},
|
|
411
481
|
(error): t.EagerEventToolExecutionOutcome => ({
|
|
412
482
|
error: normalizeError(error),
|
|
413
483
|
})
|
|
414
484
|
);
|
|
415
485
|
|
|
416
486
|
for (const entry of entries) {
|
|
417
|
-
|
|
487
|
+
const record: t.EagerEventToolExecution = {
|
|
418
488
|
toolCallId: entry.id,
|
|
419
489
|
toolName: entry.toolName,
|
|
420
490
|
args: entry.coercedArgs,
|
|
421
491
|
request: entry.request,
|
|
422
492
|
promise,
|
|
423
|
-
}
|
|
493
|
+
};
|
|
494
|
+
records.push(record);
|
|
495
|
+
graph.eagerEventToolExecutions.set(entry.id, record);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function dispatchEagerToolCompletions(args: {
|
|
500
|
+
graph: StandardGraph;
|
|
501
|
+
agentContext?: AgentContext;
|
|
502
|
+
records: t.EagerEventToolExecution[];
|
|
503
|
+
results: t.ToolExecuteResult[];
|
|
504
|
+
}): Promise<void> {
|
|
505
|
+
const { graph, agentContext, records, results } = args;
|
|
506
|
+
const recordById = new Map(
|
|
507
|
+
records.map((record) => [record.toolCallId, record])
|
|
508
|
+
);
|
|
509
|
+
const maxToolResultChars =
|
|
510
|
+
agentContext?.maxToolResultChars ??
|
|
511
|
+
calculateMaxToolResultChars(agentContext?.maxContextTokens);
|
|
512
|
+
|
|
513
|
+
for (const result of results) {
|
|
514
|
+
const record = recordById.get(result.toolCallId);
|
|
515
|
+
if (record == null) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (graph.eagerEventToolExecutions.get(result.toolCallId) !== record) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const stepId =
|
|
522
|
+
record.request.stepId ??
|
|
523
|
+
graph.toolCallStepIds.get(result.toolCallId) ??
|
|
524
|
+
'';
|
|
525
|
+
if (stepId === '') {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const output =
|
|
529
|
+
result.status === 'error'
|
|
530
|
+
? `Error: ${result.errorMessage ?? 'Unknown error'}\n Please fix your mistakes.`
|
|
531
|
+
: truncateToolResultContent(
|
|
532
|
+
typeof result.content === 'string'
|
|
533
|
+
? result.content
|
|
534
|
+
: JSON.stringify(result.content),
|
|
535
|
+
maxToolResultChars
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const dispatched = await safeDispatchCustomEvent(
|
|
540
|
+
GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
541
|
+
{
|
|
542
|
+
result: {
|
|
543
|
+
id: stepId,
|
|
544
|
+
index: record.request.turn ?? 0,
|
|
545
|
+
type: 'tool_call' as const,
|
|
546
|
+
eager: true,
|
|
547
|
+
tool_call: {
|
|
548
|
+
args: JSON.stringify(record.request.args),
|
|
549
|
+
name: record.toolName,
|
|
550
|
+
id: result.toolCallId,
|
|
551
|
+
output,
|
|
552
|
+
progress: 1,
|
|
553
|
+
} as t.ProcessedToolCall,
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
graph.config
|
|
557
|
+
);
|
|
558
|
+
if (dispatched === false) {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
record.completionDispatched = true;
|
|
562
|
+
} catch (error) {
|
|
563
|
+
// Let ToolNode dispatch the completion through the normal path later.
|
|
564
|
+
|
|
565
|
+
console.warn(
|
|
566
|
+
`[stream] eager completion dispatch failed for toolCallId=${result.toolCallId}:`,
|
|
567
|
+
error instanceof Error ? error.message : error
|
|
568
|
+
);
|
|
569
|
+
}
|
|
424
570
|
}
|
|
425
571
|
}
|
|
426
572
|
|
|
@@ -699,6 +845,8 @@ function startReadyStreamedEagerToolExecutions(args: {
|
|
|
699
845
|
} = args;
|
|
700
846
|
if (
|
|
701
847
|
hasPotentialDirectToolInStreamContext({ graph, agentContext }) ||
|
|
848
|
+
hasDirectToolCallChunkInBatch({ graph, agentContext, toolCallChunks }) ||
|
|
849
|
+
hasDirectToolCallChunkStateInStep({ graph, agentContext, stepKey }) ||
|
|
702
850
|
!isEagerToolExecutionEnabledForBatch({ graph, metadata, agentContext })
|
|
703
851
|
) {
|
|
704
852
|
return;
|
|
@@ -1265,9 +1413,12 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
1265
1413
|
|
|
1266
1414
|
const existingContent = contentParts[index] as
|
|
1267
1415
|
| (Omit<t.ToolCallContent, 'tool_call'> & {
|
|
1268
|
-
tool_call?: t.ToolCallPart;
|
|
1416
|
+
tool_call?: t.ToolCallPart & t.PartMetadata;
|
|
1269
1417
|
})
|
|
1270
1418
|
| undefined;
|
|
1419
|
+
if (!finalUpdate && existingContent?.tool_call?.progress === 1) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1271
1422
|
|
|
1272
1423
|
/** When args are a valid object, they are likely already invoked */
|
|
1273
1424
|
let args =
|