@librechat/agents 3.3.9 → 3.3.10
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 +4 -0
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +19 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/run.cjs +10 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +4 -0
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +19 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/run.mjs +10 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +5 -0
- package/dist/types/run.d.ts +7 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +5 -0
- package/src/graphs/Graph.ts +39 -0
- package/src/run.ts +15 -0
- package/src/specs/discovered-tools.test.ts +217 -0
package/src/graphs/Graph.ts
CHANGED
|
@@ -643,6 +643,10 @@ export abstract class Graph<
|
|
|
643
643
|
currentToolMap?: t.ToolMap;
|
|
644
644
|
}): CustomToolNode<T> | ToolNode<T>;
|
|
645
645
|
abstract getRunMessages(): BaseMessage[] | undefined;
|
|
646
|
+
/** Returns a snapshot of deferred tools discovered by this graph. */
|
|
647
|
+
getDiscoveredTools(_agentId?: string): string[] {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
646
650
|
abstract getContentParts(): t.MessageContentComplex[] | undefined;
|
|
647
651
|
abstract generateStepId(stepKey: string): [string, number];
|
|
648
652
|
abstract getKeyList(
|
|
@@ -1006,6 +1010,8 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1006
1010
|
messages: BaseMessage[] = [];
|
|
1007
1011
|
/** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
|
|
1008
1012
|
private cachedRunMessages?: BaseMessage[];
|
|
1013
|
+
/** Per-agent discovery snapshots preserved before contexts are reset on cleanup. */
|
|
1014
|
+
private cachedDiscoveredTools?: Map<string, string[]>;
|
|
1009
1015
|
/** Ids of AI turns the agent node returned THIS run; see isRunProducedMessage. */
|
|
1010
1016
|
protected runProducedAiMessageIds = new Set<string>();
|
|
1011
1017
|
/** Checkpoint scope whose messages match index-keyed tool snapshots. */
|
|
@@ -1137,6 +1143,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1137
1143
|
resetValues(keepContent?: boolean, checkpointScope?: string): void {
|
|
1138
1144
|
this.messages = [];
|
|
1139
1145
|
this.cachedRunMessages = undefined;
|
|
1146
|
+
this.cachedDiscoveredTools = undefined;
|
|
1140
1147
|
this.config = resetIfNotEmpty(this.config, undefined);
|
|
1141
1148
|
if (keepContent !== true) {
|
|
1142
1149
|
this.contentData = resetIfNotEmpty(this.contentData, []);
|
|
@@ -1203,6 +1210,12 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1203
1210
|
|
|
1204
1211
|
override clearHeavyState(): void {
|
|
1205
1212
|
this.cachedRunMessages = this.messages.slice(this.startIndex);
|
|
1213
|
+
this.cachedDiscoveredTools = new Map(
|
|
1214
|
+
Array.from(this.agentContexts, ([agentId, context]) => [
|
|
1215
|
+
agentId,
|
|
1216
|
+
context.getDiscoveredTools(),
|
|
1217
|
+
])
|
|
1218
|
+
);
|
|
1206
1219
|
super.clearHeavyState();
|
|
1207
1220
|
this.messages = [];
|
|
1208
1221
|
this.overrideModel = undefined;
|
|
@@ -1497,6 +1510,32 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1497
1510
|
return this.messages.slice(this.startIndex);
|
|
1498
1511
|
}
|
|
1499
1512
|
|
|
1513
|
+
override getDiscoveredTools(agentId?: string): string[] {
|
|
1514
|
+
if (agentId != null) {
|
|
1515
|
+
const current =
|
|
1516
|
+
this.agentContexts.get(agentId)?.getDiscoveredTools() ?? [];
|
|
1517
|
+
if (current.length > 0 || this.cachedDiscoveredTools == null) {
|
|
1518
|
+
return current;
|
|
1519
|
+
}
|
|
1520
|
+
return [...(this.cachedDiscoveredTools.get(agentId) ?? [])];
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const discoveredTools = new Set<string>();
|
|
1524
|
+
for (const context of this.agentContexts.values()) {
|
|
1525
|
+
for (const toolName of context.getDiscoveredTools()) {
|
|
1526
|
+
discoveredTools.add(toolName);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (discoveredTools.size === 0 && this.cachedDiscoveredTools != null) {
|
|
1530
|
+
for (const snapshot of this.cachedDiscoveredTools.values()) {
|
|
1531
|
+
for (const toolName of snapshot) {
|
|
1532
|
+
discoveredTools.add(toolName);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return Array.from(discoveredTools);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1500
1539
|
/**
|
|
1501
1540
|
* True when THIS RUN produced `message` — the provenance the handoff cue
|
|
1502
1541
|
* gate needs. Tracked as an id set rather than inferred from `startIndex`
|
package/src/run.ts
CHANGED
|
@@ -538,6 +538,21 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
538
538
|
return this.Graph.getRunMessages();
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Returns a defensive snapshot of tools discovered by the current run.
|
|
543
|
+
* Pass an agent id for that context, or omit it for the ordered union across
|
|
544
|
+
* contexts. Interrupted state is available immediately for host persistence;
|
|
545
|
+
* completed runs retain their final snapshot through graph cleanup.
|
|
546
|
+
*/
|
|
547
|
+
getDiscoveredTools(agentId?: string): string[] {
|
|
548
|
+
if (!this.Graph) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
'Graph not initialized. Make sure to use Run.create() to instantiate the Run.'
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return this.Graph.getDiscoveredTools(agentId);
|
|
554
|
+
}
|
|
555
|
+
|
|
541
556
|
/**
|
|
542
557
|
* Returns the current calibration ratio (EMA of provider-vs-estimate token ratios).
|
|
543
558
|
* Hosts should persist this value and pass it back as `RunConfig.calibrationRatio`
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
3
|
+
import { MemorySaver } from '@langchain/langgraph';
|
|
4
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
5
|
+
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
6
|
+
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
7
|
+
import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
|
|
8
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
9
|
+
import type * as t from '@/types';
|
|
10
|
+
import { FakeChatModel } from '@/llm/fake';
|
|
11
|
+
import { StandardGraph } from '@/graphs';
|
|
12
|
+
import { askUserQuestion } from '@/hitl';
|
|
13
|
+
import { Providers } from '@/common';
|
|
14
|
+
import { Run } from '@/run';
|
|
15
|
+
|
|
16
|
+
const DISCOVERED_TOOL = 'save_issue_mcp_linear';
|
|
17
|
+
|
|
18
|
+
const searchTool = tool(
|
|
19
|
+
async ({ query }) => [
|
|
20
|
+
JSON.stringify({
|
|
21
|
+
found: 1,
|
|
22
|
+
tools: [{ name: DISCOVERED_TOOL }],
|
|
23
|
+
query,
|
|
24
|
+
}),
|
|
25
|
+
{ tool_references: [{ tool_name: DISCOVERED_TOOL }] },
|
|
26
|
+
],
|
|
27
|
+
{
|
|
28
|
+
name: 'tool_search',
|
|
29
|
+
description: 'Find a deferred tool.',
|
|
30
|
+
schema: z.object({ query: z.string() }),
|
|
31
|
+
responseFormat: 'content_and_artifact',
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const askTool = tool(
|
|
36
|
+
async (input) => {
|
|
37
|
+
const { answer } = askUserQuestion(input);
|
|
38
|
+
return answer;
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'ask_user_question',
|
|
42
|
+
description: 'Pause until the user answers a question.',
|
|
43
|
+
schema: z.object({ question: z.string() }),
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
class DiscoveryThenAskModel extends FakeChatModel {
|
|
48
|
+
private turn = 0;
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super({ responses: ['', ''] });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async *_streamResponseChunks(
|
|
55
|
+
_messages: BaseMessage[],
|
|
56
|
+
_options: this['ParsedCallOptions'],
|
|
57
|
+
_runManager?: CallbackManagerForLLMRun
|
|
58
|
+
): AsyncGenerator<ChatGenerationChunk> {
|
|
59
|
+
const calls: ToolCall[][] = [
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
name: 'tool_search',
|
|
63
|
+
args: { query: 'save_issue' },
|
|
64
|
+
id: 'search-call',
|
|
65
|
+
type: 'tool_call',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
{
|
|
70
|
+
name: 'ask_user_question',
|
|
71
|
+
args: { question: 'Proceed?' },
|
|
72
|
+
id: 'ask-call',
|
|
73
|
+
type: 'tool_call',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
];
|
|
77
|
+
const toolCallChunks = calls[this.turn++].map(
|
|
78
|
+
(call, index): ToolCallChunk => ({
|
|
79
|
+
name: call.name,
|
|
80
|
+
args: JSON.stringify(call.args),
|
|
81
|
+
id: call.id,
|
|
82
|
+
index,
|
|
83
|
+
type: 'tool_call_chunk',
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
yield this._createResponseChunk('', toolCallChunks);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('Run discovered tools', () => {
|
|
91
|
+
it('exposes discoveries made before an ask-user pause', async () => {
|
|
92
|
+
const run = await Run.create<t.IState>({
|
|
93
|
+
runId: 'discovery-pause-run',
|
|
94
|
+
graphConfig: {
|
|
95
|
+
type: 'standard',
|
|
96
|
+
llmConfig: {
|
|
97
|
+
provider: Providers.OPENAI,
|
|
98
|
+
model: 'gpt-4o-mini',
|
|
99
|
+
streaming: true,
|
|
100
|
+
streamUsage: false,
|
|
101
|
+
},
|
|
102
|
+
instructions: 'Search first, then ask the user.',
|
|
103
|
+
tools: [searchTool, askTool],
|
|
104
|
+
compileOptions: { checkpointer: new MemorySaver() },
|
|
105
|
+
},
|
|
106
|
+
returnContent: true,
|
|
107
|
+
customHandlers: {},
|
|
108
|
+
});
|
|
109
|
+
run.Graph!.overrideModel = new DiscoveryThenAskModel();
|
|
110
|
+
|
|
111
|
+
await run.processStream(
|
|
112
|
+
{ messages: [new HumanMessage('Create a Linear issue')] },
|
|
113
|
+
{
|
|
114
|
+
configurable: { thread_id: 'discovery-pause-thread' },
|
|
115
|
+
version: 'v2',
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(run.getInterrupt()?.payload.type).toBe('ask_user_question');
|
|
120
|
+
// The interrupted inner subgraph has not returned its messages to the outer
|
|
121
|
+
// reducer, which is why reconstructing discoveries from run history fails.
|
|
122
|
+
expect(run.getRunMessages()).toEqual([]);
|
|
123
|
+
expect(run.getDiscoveredTools()).toEqual([DISCOVERED_TOOL]);
|
|
124
|
+
|
|
125
|
+
const snapshot = run.getDiscoveredTools();
|
|
126
|
+
snapshot.push('caller-mutation');
|
|
127
|
+
expect(run.getDiscoveredTools()).toEqual([DISCOVERED_TOOL]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('retains the last snapshot when normal cleanup resets agent contexts', async () => {
|
|
131
|
+
const run = await Run.create<t.IState>({
|
|
132
|
+
runId: 'discovery-cleanup-run',
|
|
133
|
+
graphConfig: {
|
|
134
|
+
type: 'standard',
|
|
135
|
+
llmConfig: {
|
|
136
|
+
provider: Providers.OPENAI,
|
|
137
|
+
model: 'gpt-4o-mini',
|
|
138
|
+
},
|
|
139
|
+
instructions: 'Test discovery cleanup.',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const graph = run.Graph as StandardGraph;
|
|
143
|
+
graph.agentContexts
|
|
144
|
+
.get(graph.defaultAgentId)
|
|
145
|
+
?.markToolsAsDiscovered([DISCOVERED_TOOL]);
|
|
146
|
+
|
|
147
|
+
graph.clearHeavyState();
|
|
148
|
+
|
|
149
|
+
expect(run.getDiscoveredTools()).toEqual([DISCOVERED_TOOL]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('supports per-agent snapshots while returning a deduplicated union by default', async () => {
|
|
153
|
+
const run = await Run.create<t.IState>({
|
|
154
|
+
runId: 'multi-agent-discovery-run',
|
|
155
|
+
graphConfig: {
|
|
156
|
+
type: 'multi-agent',
|
|
157
|
+
agents: [
|
|
158
|
+
{
|
|
159
|
+
agentId: 'researcher',
|
|
160
|
+
provider: Providers.ANTHROPIC,
|
|
161
|
+
clientOptions: {
|
|
162
|
+
modelName: 'claude-haiku-4-5',
|
|
163
|
+
apiKey: 'test-key',
|
|
164
|
+
},
|
|
165
|
+
instructions: 'Research.',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
agentId: 'writer',
|
|
169
|
+
provider: Providers.ANTHROPIC,
|
|
170
|
+
clientOptions: {
|
|
171
|
+
modelName: 'claude-haiku-4-5',
|
|
172
|
+
apiKey: 'test-key',
|
|
173
|
+
},
|
|
174
|
+
instructions: 'Write.',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
edges: [{ from: 'researcher', to: 'writer', edgeType: 'direct' }],
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const graph = run.Graph as StandardGraph;
|
|
181
|
+
graph.agentContexts
|
|
182
|
+
.get('researcher')
|
|
183
|
+
?.markToolsAsDiscovered(['shared_tool', 'research_tool']);
|
|
184
|
+
graph.agentContexts
|
|
185
|
+
.get('writer')
|
|
186
|
+
?.markToolsAsDiscovered(['shared_tool', 'writing_tool']);
|
|
187
|
+
|
|
188
|
+
expect(run.getDiscoveredTools('researcher')).toEqual([
|
|
189
|
+
'shared_tool',
|
|
190
|
+
'research_tool',
|
|
191
|
+
]);
|
|
192
|
+
expect(run.getDiscoveredTools('writer')).toEqual([
|
|
193
|
+
'shared_tool',
|
|
194
|
+
'writing_tool',
|
|
195
|
+
]);
|
|
196
|
+
expect(run.getDiscoveredTools()).toEqual([
|
|
197
|
+
'shared_tool',
|
|
198
|
+
'research_tool',
|
|
199
|
+
'writing_tool',
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
graph.clearHeavyState();
|
|
203
|
+
expect(run.getDiscoveredTools('researcher')).toEqual([
|
|
204
|
+
'shared_tool',
|
|
205
|
+
'research_tool',
|
|
206
|
+
]);
|
|
207
|
+
expect(run.getDiscoveredTools('writer')).toEqual([
|
|
208
|
+
'shared_tool',
|
|
209
|
+
'writing_tool',
|
|
210
|
+
]);
|
|
211
|
+
expect(run.getDiscoveredTools()).toEqual([
|
|
212
|
+
'shared_tool',
|
|
213
|
+
'research_tool',
|
|
214
|
+
'writing_tool',
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
});
|