illuma-agents 1.0.10 → 1.0.12
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/LICENSE +1 -1
- package/dist/cjs/agents/AgentContext.cjs +236 -27
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/events.cjs +3 -11
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +44 -18
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +1 -3
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +121 -6
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +18 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +149 -54
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +85 -0
- package/dist/cjs/messages/tools.cjs.map +1 -0
- package/dist/cjs/run.cjs +0 -8
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +4 -0
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +438 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/ToolNode.cjs +53 -15
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/ToolSearchRegex.cjs +455 -0
- package/dist/cjs/tools/ToolSearchRegex.cjs.map +1 -0
- package/dist/cjs/tools/search/schema.cjs +7 -9
- package/dist/cjs/tools/search/schema.cjs.map +1 -1
- package/dist/cjs/utils/run.cjs +5 -1
- package/dist/cjs/utils/run.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +236 -27
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/events.mjs +4 -12
- package/dist/esm/events.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +45 -19
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +1 -3
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +121 -6
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +3 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +149 -54
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/tools.mjs +82 -0
- package/dist/esm/messages/tools.mjs.map +1 -0
- package/dist/esm/run.mjs +0 -8
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +4 -0
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +430 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/ToolNode.mjs +53 -15
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/ToolSearchRegex.mjs +448 -0
- package/dist/esm/tools/ToolSearchRegex.mjs.map +1 -0
- package/dist/esm/tools/search/schema.mjs +7 -9
- package/dist/esm/tools/search/schema.mjs.map +1 -1
- package/dist/esm/utils/run.mjs +5 -1
- package/dist/esm/utils/run.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +72 -5
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/graphs/Graph.d.ts +3 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/llm/bedrock/index.d.ts +31 -4
- package/dist/types/messages/cache.d.ts +23 -8
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/messages/tools.d.ts +17 -0
- package/dist/types/test/mockTools.d.ts +28 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +91 -0
- package/dist/types/tools/ToolNode.d.ts +10 -2
- package/dist/types/tools/ToolSearchRegex.d.ts +80 -0
- package/dist/types/types/graph.d.ts +14 -1
- package/dist/types/types/tools.d.ts +138 -0
- package/package.json +7 -8
- package/src/agents/AgentContext.ts +278 -27
- package/src/agents/__tests__/AgentContext.test.ts +805 -0
- package/src/common/enum.ts +2 -0
- package/src/events.ts +5 -12
- package/src/graphs/Graph.ts +57 -19
- package/src/index.ts +2 -0
- package/src/instrumentation.ts +1 -4
- package/src/llm/bedrock/__tests__/bedrock-caching.test.ts +473 -0
- package/src/llm/bedrock/index.ts +149 -12
- package/src/messages/__tests__/tools.test.ts +473 -0
- package/src/messages/cache.ts +163 -61
- package/src/messages/index.ts +1 -0
- package/src/messages/tools.ts +99 -0
- package/src/run.ts +0 -9
- package/src/scripts/code_exec_ptc.ts +334 -0
- package/src/scripts/image.ts +178 -0
- package/src/scripts/programmatic_exec.ts +396 -0
- package/src/scripts/programmatic_exec_agent.ts +231 -0
- package/src/scripts/test-tools-before-handoff.ts +5 -1
- package/src/scripts/tool_search_regex.ts +162 -0
- package/src/scripts/tools.ts +4 -1
- package/src/specs/thinking-prune.test.ts +52 -118
- package/src/test/mockTools.ts +366 -0
- package/src/tools/CodeExecutor.ts +4 -0
- package/src/tools/ProgrammaticToolCalling.ts +558 -0
- package/src/tools/ToolNode.ts +59 -18
- package/src/tools/ToolSearchRegex.ts +535 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.ts +318 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +853 -0
- package/src/tools/__tests__/ToolSearchRegex.integration.test.ts +161 -0
- package/src/tools/__tests__/ToolSearchRegex.test.ts +232 -0
- package/src/tools/search/jina-reranker.test.ts +16 -16
- package/src/tools/search/schema.ts +7 -9
- package/src/types/graph.ts +14 -1
- package/src/types/tools.ts +166 -0
- package/src/utils/run.ts +5 -1
- package/src/tools/search/direct-url.test.ts +0 -530
package/src/llm/bedrock/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optimized ChatBedrockConverse wrapper that fixes contentBlockIndex conflicts
|
|
3
|
+
* and adds prompt caching support for Bedrock Converse API.
|
|
3
4
|
*
|
|
4
5
|
* Bedrock sends the same contentBlockIndex for both text and tool_use content blocks,
|
|
5
6
|
* causing LangChain's merge logic to fail with "field[contentBlockIndex] already exists"
|
|
@@ -9,18 +10,35 @@
|
|
|
9
10
|
* The contentBlockIndex field is only used internally by Bedrock's streaming protocol
|
|
10
11
|
* and isn't needed by application logic - the index field on tool_call_chunks serves
|
|
11
12
|
* the purpose of tracking tool call ordering.
|
|
13
|
+
*
|
|
14
|
+
* PROMPT CACHING:
|
|
15
|
+
* When promptCache: true is set, this wrapper adds cachePoint markers to the tools array
|
|
16
|
+
* to enable Bedrock prompt caching for tool definitions. This allows tool schemas to be
|
|
17
|
+
* cached and reused across requests, reducing latency and costs.
|
|
18
|
+
*
|
|
19
|
+
* CACHE TOKEN EXTRACTION:
|
|
20
|
+
* LangChain AWS doesn't extract cacheReadInputTokens/cacheWriteInputTokens from Bedrock's
|
|
21
|
+
* response. This wrapper adds input_token_details to usage_metadata with cache information.
|
|
12
22
|
*/
|
|
13
23
|
|
|
14
24
|
import { ChatBedrockConverse } from '@langchain/aws';
|
|
15
25
|
import type { ChatBedrockConverseInput } from '@langchain/aws';
|
|
16
26
|
import { AIMessageChunk } from '@langchain/core/messages';
|
|
17
|
-
import type { BaseMessage } from '@langchain/core/messages';
|
|
27
|
+
import type { BaseMessage, UsageMetadata } from '@langchain/core/messages';
|
|
18
28
|
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
|
19
29
|
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
|
20
30
|
|
|
31
|
+
/** Extended input type with promptCache option */
|
|
32
|
+
export interface CustomChatBedrockConverseInput extends ChatBedrockConverseInput {
|
|
33
|
+
promptCache?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
22
|
-
|
|
37
|
+
promptCache: boolean;
|
|
38
|
+
|
|
39
|
+
constructor(fields?: CustomChatBedrockConverseInput) {
|
|
23
40
|
super(fields);
|
|
41
|
+
this.promptCache = fields?.promptCache ?? false;
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
static lc_name(): string {
|
|
@@ -28,9 +46,92 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
/**
|
|
31
|
-
* Override
|
|
32
|
-
* This
|
|
33
|
-
*
|
|
49
|
+
* Override invocationParams to add cachePoint to tools when promptCache is enabled.
|
|
50
|
+
* This enables Bedrock prompt caching for tool definitions.
|
|
51
|
+
*
|
|
52
|
+
* STRATEGY: Separate cachePoints for core tools and MCP tools
|
|
53
|
+
* - Core tools (web_search, execute_code, etc.) are stable → cache first
|
|
54
|
+
* - MCP tools (have '_mcp_' in name) are dynamic → cache separately after
|
|
55
|
+
* - This allows core tools to stay cached when MCP selection changes
|
|
56
|
+
*
|
|
57
|
+
* NOTE: Only Claude models support cachePoint - Nova and other models will reject it.
|
|
58
|
+
*/
|
|
59
|
+
invocationParams(
|
|
60
|
+
options?: this['ParsedCallOptions']
|
|
61
|
+
): ReturnType<ChatBedrockConverse['invocationParams']> {
|
|
62
|
+
const params = super.invocationParams(options);
|
|
63
|
+
|
|
64
|
+
// Add cachePoint to tools array if promptCache is enabled and tools exist
|
|
65
|
+
// Only Claude models support cachePoint - check model name
|
|
66
|
+
const modelId = this.model?.toLowerCase() ?? '';
|
|
67
|
+
const isClaudeModel = modelId.includes('claude') || modelId.includes('anthropic');
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
this.promptCache &&
|
|
71
|
+
isClaudeModel &&
|
|
72
|
+
params.toolConfig?.tools &&
|
|
73
|
+
Array.isArray(params.toolConfig.tools) &&
|
|
74
|
+
params.toolConfig.tools.length > 0
|
|
75
|
+
) {
|
|
76
|
+
// Separate core tools from MCP tools
|
|
77
|
+
// MCP tools have '_mcp_' in their name (e.g., 'search_emails_mcp_Google-Workspace')
|
|
78
|
+
const coreTools: typeof params.toolConfig.tools = [];
|
|
79
|
+
const mcpTools: typeof params.toolConfig.tools = [];
|
|
80
|
+
const coreToolNames: string[] = [];
|
|
81
|
+
const mcpToolNames: string[] = [];
|
|
82
|
+
|
|
83
|
+
for (const tool of params.toolConfig.tools) {
|
|
84
|
+
// Check if tool has a name property with '_mcp_' pattern
|
|
85
|
+
const toolName = (tool as { toolSpec?: { name?: string } })?.toolSpec?.name ?? '';
|
|
86
|
+
if (toolName.includes('_mcp_')) {
|
|
87
|
+
mcpTools.push(tool);
|
|
88
|
+
mcpToolNames.push(toolName);
|
|
89
|
+
} else {
|
|
90
|
+
coreTools.push(tool);
|
|
91
|
+
coreToolNames.push(toolName);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Always log cache structure (INFO level for tracking)
|
|
96
|
+
console.log(`[Cache] 🔧 Tools | Core: [${coreToolNames.join(', ')}] (${coreTools.length}) | MCP: [${mcpToolNames.join(', ')}] (${mcpTools.length})`);
|
|
97
|
+
|
|
98
|
+
// Build tools array with strategic cachePoints:
|
|
99
|
+
// [CoreTool1, CoreTool2, cachePoint] + [MCPTool1, MCPTool2, cachePoint]
|
|
100
|
+
const toolsWithCache: typeof params.toolConfig.tools = [];
|
|
101
|
+
let cachePointCount = 0;
|
|
102
|
+
|
|
103
|
+
// Add core tools with cachePoint (if any)
|
|
104
|
+
if (coreTools.length > 0) {
|
|
105
|
+
toolsWithCache.push(...coreTools);
|
|
106
|
+
toolsWithCache.push({ cachePoint: { type: 'default' } });
|
|
107
|
+
cachePointCount++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add MCP tools with their own cachePoint (if any)
|
|
111
|
+
if (mcpTools.length > 0) {
|
|
112
|
+
toolsWithCache.push(...mcpTools);
|
|
113
|
+
toolsWithCache.push({ cachePoint: { type: 'default' } });
|
|
114
|
+
cachePointCount++;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If no tools at all (shouldn't happen but safety check)
|
|
118
|
+
if (toolsWithCache.length === 0) {
|
|
119
|
+
toolsWithCache.push({ cachePoint: { type: 'default' } });
|
|
120
|
+
cachePointCount++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`[Cache] 📍 Tool cachePoints: ${cachePointCount} | Order: [${coreToolNames.length > 0 ? 'CoreTools→CP' : ''}${mcpToolNames.length > 0 ? '→MCPTools→CP' : ''}]`);
|
|
124
|
+
|
|
125
|
+
params.toolConfig.tools = toolsWithCache;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return params;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Override _streamResponseChunks to:
|
|
133
|
+
* 1. Strip contentBlockIndex from response_metadata to prevent merge conflicts
|
|
134
|
+
* 2. Extract cacheReadInputTokens/cacheWriteInputTokens and add to usage_metadata
|
|
34
135
|
*/
|
|
35
136
|
async *_streamResponseChunks(
|
|
36
137
|
messages: BaseMessage[],
|
|
@@ -50,21 +151,57 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
|
|
|
50
151
|
(chunk.message as Partial<AIMessageChunk>).response_metadata &&
|
|
51
152
|
typeof chunk.message.response_metadata === 'object'
|
|
52
153
|
) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
154
|
+
const responseMetadata = chunk.message.response_metadata as Record<string, unknown>;
|
|
155
|
+
let needsModification = false;
|
|
156
|
+
let cleanedMetadata = responseMetadata;
|
|
57
157
|
|
|
158
|
+
// Check if contentBlockIndex exists anywhere in response_metadata
|
|
159
|
+
const hasContentBlockIndex = this.hasContentBlockIndex(responseMetadata);
|
|
58
160
|
if (hasContentBlockIndex) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
161
|
+
cleanedMetadata = this.removeContentBlockIndex(responseMetadata) as Record<string, unknown>;
|
|
162
|
+
needsModification = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Extract cache tokens from metadata.usage (Bedrock streaming format)
|
|
166
|
+
// The metadata chunk contains usage with cacheReadInputTokens/cacheWriteInputTokens
|
|
167
|
+
const metadata = responseMetadata.metadata as Record<string, unknown> | undefined;
|
|
168
|
+
const usage = (metadata?.usage ?? responseMetadata.usage) as Record<string, unknown> | undefined;
|
|
169
|
+
|
|
170
|
+
let enhancedUsageMetadata: UsageMetadata | undefined = chunk.message.usage_metadata;
|
|
171
|
+
|
|
172
|
+
if (usage) {
|
|
173
|
+
const cacheRead = (usage.cacheReadInputTokens as number) ?? 0;
|
|
174
|
+
const cacheWrite = (usage.cacheWriteInputTokens as number) ?? 0;
|
|
175
|
+
const inputTokens = (usage.inputTokens as number) ?? 0;
|
|
176
|
+
const outputTokens = (usage.outputTokens as number) ?? 0;
|
|
177
|
+
|
|
178
|
+
if (cacheRead > 0 || cacheWrite > 0) {
|
|
179
|
+
// Always log cache results for tracking
|
|
180
|
+
const cacheStatus = cacheRead > 0 && cacheWrite === 0 ? '✅ HIT' :
|
|
181
|
+
cacheWrite > 0 && cacheRead === 0 ? '📝 WRITE' :
|
|
182
|
+
cacheRead > 0 && cacheWrite > 0 ? '🔄 PARTIAL' : '❌ MISS';
|
|
183
|
+
console.log(`[Cache] ${cacheStatus} | read=${cacheRead} | write=${cacheWrite} | input=${inputTokens} | output=${outputTokens}`);
|
|
184
|
+
|
|
185
|
+
needsModification = true;
|
|
186
|
+
enhancedUsageMetadata = {
|
|
187
|
+
input_tokens: chunk.message.usage_metadata?.input_tokens ?? inputTokens,
|
|
188
|
+
output_tokens: chunk.message.usage_metadata?.output_tokens ?? outputTokens,
|
|
189
|
+
total_tokens: chunk.message.usage_metadata?.total_tokens ?? (usage.totalTokens as number) ?? 0,
|
|
190
|
+
input_token_details: {
|
|
191
|
+
cache_read: cacheRead,
|
|
192
|
+
cache_creation: cacheWrite,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
62
197
|
|
|
198
|
+
if (needsModification) {
|
|
63
199
|
yield new ChatGenerationChunk({
|
|
64
200
|
text: chunk.text,
|
|
65
201
|
message: new AIMessageChunk({
|
|
66
202
|
...chunk.message,
|
|
67
203
|
response_metadata: cleanedMetadata,
|
|
204
|
+
usage_metadata: enhancedUsageMetadata,
|
|
68
205
|
}),
|
|
69
206
|
generationInfo: chunk.generationInfo,
|
|
70
207
|
});
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// src/messages/__tests__/tools.test.ts
|
|
2
|
+
import {
|
|
3
|
+
AIMessageChunk,
|
|
4
|
+
ToolMessage,
|
|
5
|
+
HumanMessage,
|
|
6
|
+
} from '@langchain/core/messages';
|
|
7
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
8
|
+
import { extractToolDiscoveries, hasToolSearchInCurrentTurn } from '../tools';
|
|
9
|
+
import { Constants } from '@/common';
|
|
10
|
+
|
|
11
|
+
describe('Tool Discovery Functions', () => {
|
|
12
|
+
/**
|
|
13
|
+
* Helper to create an AIMessageChunk with tool calls
|
|
14
|
+
*/
|
|
15
|
+
const createAIMessage = (
|
|
16
|
+
content: string,
|
|
17
|
+
toolCalls: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
args: Record<string, unknown>;
|
|
21
|
+
}>
|
|
22
|
+
): AIMessageChunk => {
|
|
23
|
+
return new AIMessageChunk({
|
|
24
|
+
content,
|
|
25
|
+
tool_calls: toolCalls.map((tc) => ({
|
|
26
|
+
id: tc.id,
|
|
27
|
+
name: tc.name,
|
|
28
|
+
args: tc.args,
|
|
29
|
+
type: 'tool_call' as const,
|
|
30
|
+
})),
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Helper to create a ToolMessage (tool search result)
|
|
36
|
+
*/
|
|
37
|
+
const createToolSearchResult = (
|
|
38
|
+
toolCallId: string,
|
|
39
|
+
discoveredTools: string[]
|
|
40
|
+
): ToolMessage => {
|
|
41
|
+
return new ToolMessage({
|
|
42
|
+
content: `Found ${discoveredTools.length} tools`,
|
|
43
|
+
tool_call_id: toolCallId,
|
|
44
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
45
|
+
artifact: {
|
|
46
|
+
tool_references: discoveredTools.map((name) => ({
|
|
47
|
+
tool_name: name,
|
|
48
|
+
match_score: 0.9,
|
|
49
|
+
matched_field: 'description',
|
|
50
|
+
snippet: 'Test snippet',
|
|
51
|
+
})),
|
|
52
|
+
metadata: {
|
|
53
|
+
total_searched: 10,
|
|
54
|
+
pattern: 'test',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Helper to create a regular ToolMessage (non-search)
|
|
62
|
+
*/
|
|
63
|
+
const createRegularToolMessage = (
|
|
64
|
+
toolCallId: string,
|
|
65
|
+
name: string,
|
|
66
|
+
content: string
|
|
67
|
+
): ToolMessage => {
|
|
68
|
+
return new ToolMessage({
|
|
69
|
+
content,
|
|
70
|
+
tool_call_id: toolCallId,
|
|
71
|
+
name,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
describe('extractToolDiscoveries', () => {
|
|
76
|
+
it('extracts tool names from a single tool search result', () => {
|
|
77
|
+
const messages: BaseMessage[] = [
|
|
78
|
+
new HumanMessage('Search for database tools'),
|
|
79
|
+
createAIMessage('Searching...', [
|
|
80
|
+
{
|
|
81
|
+
id: 'call_1',
|
|
82
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
83
|
+
args: { pattern: 'database' },
|
|
84
|
+
},
|
|
85
|
+
]),
|
|
86
|
+
createToolSearchResult('call_1', ['database_query', 'database_insert']),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const result = extractToolDiscoveries(messages);
|
|
90
|
+
|
|
91
|
+
expect(result).toEqual(['database_query', 'database_insert']);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('extracts tool names from multiple tool search results in same turn', () => {
|
|
95
|
+
const messages: BaseMessage[] = [
|
|
96
|
+
new HumanMessage('Search for tools'),
|
|
97
|
+
createAIMessage('Searching...', [
|
|
98
|
+
{
|
|
99
|
+
id: 'call_1',
|
|
100
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
101
|
+
args: { pattern: 'database' },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'call_2',
|
|
105
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
106
|
+
args: { pattern: 'file' },
|
|
107
|
+
},
|
|
108
|
+
]),
|
|
109
|
+
createToolSearchResult('call_1', ['database_query']),
|
|
110
|
+
createToolSearchResult('call_2', ['file_read', 'file_write']),
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const result = extractToolDiscoveries(messages);
|
|
114
|
+
|
|
115
|
+
expect(result).toEqual(['database_query', 'file_read', 'file_write']);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns empty array when no messages', () => {
|
|
119
|
+
const result = extractToolDiscoveries([]);
|
|
120
|
+
expect(result).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns empty array when last message is not a ToolMessage', () => {
|
|
124
|
+
const messages: BaseMessage[] = [
|
|
125
|
+
new HumanMessage('Hello'),
|
|
126
|
+
createAIMessage('Hi there!', []),
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const result = extractToolDiscoveries(messages);
|
|
130
|
+
|
|
131
|
+
expect(result).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns empty array when no AI message with tool calls found', () => {
|
|
135
|
+
const messages: BaseMessage[] = [
|
|
136
|
+
new HumanMessage('Hello'),
|
|
137
|
+
new ToolMessage({
|
|
138
|
+
content: 'Some result',
|
|
139
|
+
tool_call_id: 'orphan_call',
|
|
140
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
141
|
+
}),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const result = extractToolDiscoveries(messages);
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('ignores tool search results from previous turns', () => {
|
|
150
|
+
const messages: BaseMessage[] = [
|
|
151
|
+
// Turn 1: Previous search
|
|
152
|
+
new HumanMessage('Search for old tools'),
|
|
153
|
+
createAIMessage('Searching...', [
|
|
154
|
+
{
|
|
155
|
+
id: 'old_call',
|
|
156
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
157
|
+
args: { pattern: 'old' },
|
|
158
|
+
},
|
|
159
|
+
]),
|
|
160
|
+
createToolSearchResult('old_call', ['old_tool_1', 'old_tool_2']),
|
|
161
|
+
// Turn 2: Current turn
|
|
162
|
+
new HumanMessage('Search for new tools'),
|
|
163
|
+
createAIMessage('Searching again...', [
|
|
164
|
+
{
|
|
165
|
+
id: 'new_call',
|
|
166
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
167
|
+
args: { pattern: 'new' },
|
|
168
|
+
},
|
|
169
|
+
]),
|
|
170
|
+
createToolSearchResult('new_call', ['new_tool']),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const result = extractToolDiscoveries(messages);
|
|
174
|
+
|
|
175
|
+
// Should only return tools from current turn
|
|
176
|
+
expect(result).toEqual(['new_tool']);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('ignores non-search tool results', () => {
|
|
180
|
+
const messages: BaseMessage[] = [
|
|
181
|
+
new HumanMessage('Do some work'),
|
|
182
|
+
createAIMessage('Working...', [
|
|
183
|
+
{
|
|
184
|
+
id: 'search_call',
|
|
185
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
186
|
+
args: { pattern: 'test' },
|
|
187
|
+
},
|
|
188
|
+
{ id: 'other_call', name: 'get_weather', args: { city: 'NYC' } },
|
|
189
|
+
]),
|
|
190
|
+
createToolSearchResult('search_call', ['found_tool']),
|
|
191
|
+
createRegularToolMessage('other_call', 'get_weather', '{"temp": 72}'),
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const result = extractToolDiscoveries(messages);
|
|
195
|
+
|
|
196
|
+
expect(result).toEqual(['found_tool']);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('handles empty tool_references in artifact', () => {
|
|
200
|
+
const messages: BaseMessage[] = [
|
|
201
|
+
new HumanMessage('Search'),
|
|
202
|
+
createAIMessage('Searching...', [
|
|
203
|
+
{
|
|
204
|
+
id: 'call_1',
|
|
205
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
206
|
+
args: { pattern: 'xyz' },
|
|
207
|
+
},
|
|
208
|
+
]),
|
|
209
|
+
new ToolMessage({
|
|
210
|
+
content: 'No tools found',
|
|
211
|
+
tool_call_id: 'call_1',
|
|
212
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
213
|
+
artifact: {
|
|
214
|
+
tool_references: [],
|
|
215
|
+
metadata: { total_searched: 10, pattern: 'xyz' },
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const result = extractToolDiscoveries(messages);
|
|
221
|
+
|
|
222
|
+
expect(result).toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('handles missing artifact', () => {
|
|
226
|
+
const messages: BaseMessage[] = [
|
|
227
|
+
new HumanMessage('Search'),
|
|
228
|
+
createAIMessage('Searching...', [
|
|
229
|
+
{
|
|
230
|
+
id: 'call_1',
|
|
231
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
232
|
+
args: { pattern: 'test' },
|
|
233
|
+
},
|
|
234
|
+
]),
|
|
235
|
+
new ToolMessage({
|
|
236
|
+
content: 'Error occurred',
|
|
237
|
+
tool_call_id: 'call_1',
|
|
238
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
239
|
+
// No artifact
|
|
240
|
+
}),
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const result = extractToolDiscoveries(messages);
|
|
244
|
+
|
|
245
|
+
expect(result).toEqual([]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('ignores tool messages with wrong tool_call_id', () => {
|
|
249
|
+
const messages: BaseMessage[] = [
|
|
250
|
+
new HumanMessage('Search'),
|
|
251
|
+
createAIMessage('Searching...', [
|
|
252
|
+
{
|
|
253
|
+
id: 'call_1',
|
|
254
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
255
|
+
args: { pattern: 'test' },
|
|
256
|
+
},
|
|
257
|
+
]),
|
|
258
|
+
// This has a different tool_call_id that doesn't match the AI message
|
|
259
|
+
createToolSearchResult('wrong_id', ['should_not_appear']),
|
|
260
|
+
createToolSearchResult('call_1', ['correct_tool']),
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
const result = extractToolDiscoveries(messages);
|
|
264
|
+
|
|
265
|
+
expect(result).toEqual(['correct_tool']);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('only looks at messages after the latest AI parent', () => {
|
|
269
|
+
const messages: BaseMessage[] = [
|
|
270
|
+
// First AI message with search
|
|
271
|
+
createAIMessage('First search', [
|
|
272
|
+
{
|
|
273
|
+
id: 'first_call',
|
|
274
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
275
|
+
args: { pattern: 'first' },
|
|
276
|
+
},
|
|
277
|
+
]),
|
|
278
|
+
createToolSearchResult('first_call', ['first_tool']),
|
|
279
|
+
// Second AI message - this is the "latest AI parent" for the last tool message
|
|
280
|
+
createAIMessage('Second search', [
|
|
281
|
+
{
|
|
282
|
+
id: 'second_call',
|
|
283
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
284
|
+
args: { pattern: 'second' },
|
|
285
|
+
},
|
|
286
|
+
]),
|
|
287
|
+
createToolSearchResult('second_call', ['second_tool']),
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const result = extractToolDiscoveries(messages);
|
|
291
|
+
|
|
292
|
+
// Should only find second_tool (from the turn of the latest AI parent)
|
|
293
|
+
expect(result).toEqual(['second_tool']);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('hasToolSearchInCurrentTurn', () => {
|
|
298
|
+
it('returns true when current turn has tool search results', () => {
|
|
299
|
+
const messages: BaseMessage[] = [
|
|
300
|
+
new HumanMessage('Search'),
|
|
301
|
+
createAIMessage('Searching...', [
|
|
302
|
+
{
|
|
303
|
+
id: 'call_1',
|
|
304
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
305
|
+
args: { pattern: 'test' },
|
|
306
|
+
},
|
|
307
|
+
]),
|
|
308
|
+
createToolSearchResult('call_1', ['found_tool']),
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
312
|
+
|
|
313
|
+
expect(result).toBe(true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('returns false when no messages', () => {
|
|
317
|
+
const result = hasToolSearchInCurrentTurn([]);
|
|
318
|
+
expect(result).toBe(false);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('returns false when last message is not a ToolMessage', () => {
|
|
322
|
+
const messages: BaseMessage[] = [
|
|
323
|
+
new HumanMessage('Hello'),
|
|
324
|
+
createAIMessage('Hi!', []),
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
328
|
+
|
|
329
|
+
expect(result).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('returns false when no AI parent found', () => {
|
|
333
|
+
const messages: BaseMessage[] = [
|
|
334
|
+
new HumanMessage('Hello'),
|
|
335
|
+
new ToolMessage({
|
|
336
|
+
content: 'Result',
|
|
337
|
+
tool_call_id: 'orphan',
|
|
338
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
339
|
+
}),
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
343
|
+
|
|
344
|
+
expect(result).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('returns false when current turn has no tool search (only other tools)', () => {
|
|
348
|
+
const messages: BaseMessage[] = [
|
|
349
|
+
new HumanMessage('Get weather'),
|
|
350
|
+
createAIMessage('Getting weather...', [
|
|
351
|
+
{ id: 'call_1', name: 'get_weather', args: { city: 'NYC' } },
|
|
352
|
+
]),
|
|
353
|
+
createRegularToolMessage('call_1', 'get_weather', '{"temp": 72}'),
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
357
|
+
|
|
358
|
+
expect(result).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('returns true even with mixed tool types in current turn', () => {
|
|
362
|
+
const messages: BaseMessage[] = [
|
|
363
|
+
new HumanMessage('Search and get weather'),
|
|
364
|
+
createAIMessage('Working...', [
|
|
365
|
+
{
|
|
366
|
+
id: 'search_call',
|
|
367
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
368
|
+
args: { pattern: 'test' },
|
|
369
|
+
},
|
|
370
|
+
{ id: 'weather_call', name: 'get_weather', args: { city: 'NYC' } },
|
|
371
|
+
]),
|
|
372
|
+
createRegularToolMessage('weather_call', 'get_weather', '{"temp": 72}'),
|
|
373
|
+
createToolSearchResult('search_call', ['found_tool']),
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
377
|
+
|
|
378
|
+
expect(result).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('returns false for tool search from previous turn', () => {
|
|
382
|
+
const messages: BaseMessage[] = [
|
|
383
|
+
// Previous turn with search
|
|
384
|
+
createAIMessage('Searching...', [
|
|
385
|
+
{
|
|
386
|
+
id: 'old_call',
|
|
387
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
388
|
+
args: { pattern: 'old' },
|
|
389
|
+
},
|
|
390
|
+
]),
|
|
391
|
+
createToolSearchResult('old_call', ['old_tool']),
|
|
392
|
+
// Current turn without search
|
|
393
|
+
new HumanMessage('Get weather now'),
|
|
394
|
+
createAIMessage('Getting weather...', [
|
|
395
|
+
{ id: 'weather_call', name: 'get_weather', args: { city: 'NYC' } },
|
|
396
|
+
]),
|
|
397
|
+
createRegularToolMessage('weather_call', 'get_weather', '{"temp": 72}'),
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
const result = hasToolSearchInCurrentTurn(messages);
|
|
401
|
+
|
|
402
|
+
expect(result).toBe(false);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('Integration: extractToolDiscoveries + hasToolSearchInCurrentTurn', () => {
|
|
407
|
+
it('hasToolSearchInCurrentTurn is true when extractToolDiscoveries returns results', () => {
|
|
408
|
+
const messages: BaseMessage[] = [
|
|
409
|
+
new HumanMessage('Search'),
|
|
410
|
+
createAIMessage('Searching...', [
|
|
411
|
+
{
|
|
412
|
+
id: 'call_1',
|
|
413
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
414
|
+
args: { pattern: 'test' },
|
|
415
|
+
},
|
|
416
|
+
]),
|
|
417
|
+
createToolSearchResult('call_1', ['tool_a', 'tool_b']),
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
const hasSearch = hasToolSearchInCurrentTurn(messages);
|
|
421
|
+
const discoveries = extractToolDiscoveries(messages);
|
|
422
|
+
|
|
423
|
+
expect(hasSearch).toBe(true);
|
|
424
|
+
expect(discoveries.length).toBeGreaterThan(0);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('both return empty/false for non-search turns', () => {
|
|
428
|
+
const messages: BaseMessage[] = [
|
|
429
|
+
new HumanMessage('Get weather'),
|
|
430
|
+
createAIMessage('Getting...', [
|
|
431
|
+
{ id: 'call_1', name: 'get_weather', args: { city: 'NYC' } },
|
|
432
|
+
]),
|
|
433
|
+
createRegularToolMessage('call_1', 'get_weather', '{"temp": 72}'),
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
const hasSearch = hasToolSearchInCurrentTurn(messages);
|
|
437
|
+
const discoveries = extractToolDiscoveries(messages);
|
|
438
|
+
|
|
439
|
+
expect(hasSearch).toBe(false);
|
|
440
|
+
expect(discoveries).toEqual([]);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('hasToolSearchInCurrentTurn can be used as quick check before extraction', () => {
|
|
444
|
+
const messagesWithSearch: BaseMessage[] = [
|
|
445
|
+
new HumanMessage('Search'),
|
|
446
|
+
createAIMessage('Searching...', [
|
|
447
|
+
{
|
|
448
|
+
id: 'call_1',
|
|
449
|
+
name: Constants.TOOL_SEARCH_REGEX,
|
|
450
|
+
args: { pattern: 'test' },
|
|
451
|
+
},
|
|
452
|
+
]),
|
|
453
|
+
createToolSearchResult('call_1', ['tool_a']),
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
const messagesWithoutSearch: BaseMessage[] = [
|
|
457
|
+
new HumanMessage('Hello'),
|
|
458
|
+
createAIMessage('Hi!', []),
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
// Pattern: quick check first, then extract only if needed
|
|
462
|
+
if (hasToolSearchInCurrentTurn(messagesWithSearch)) {
|
|
463
|
+
const discoveries = extractToolDiscoveries(messagesWithSearch);
|
|
464
|
+
expect(discoveries).toEqual(['tool_a']);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (hasToolSearchInCurrentTurn(messagesWithoutSearch)) {
|
|
468
|
+
// This should not execute
|
|
469
|
+
expect(true).toBe(false);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|