@illuma-ai/agents 1.1.20 → 1.1.22
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 +12 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +85 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/index.cjs +14 -0
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/run.cjs +20 -9
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +12 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +85 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/bedrock/index.mjs +14 -0
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/run.mjs +20 -9
- package/dist/esm/run.mjs.map +1 -1
- package/dist/types/graphs/MultiAgentGraph.d.ts +17 -0
- package/package.json +1 -1
- package/src/graphs/Graph.ts +12 -1
- package/src/graphs/MultiAgentGraph.ts +105 -1
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +191 -0
- package/src/llm/bedrock/index.ts +17 -0
- package/src/run.ts +20 -11
- package/src/scripts/test-bedrock-handoff-autonomous.ts +231 -0
- package/src/agents/AgentContext.js +0 -782
- package/src/agents/AgentContext.test.js +0 -421
- package/src/agents/__tests__/AgentContext.test.js +0 -678
- package/src/agents/__tests__/resolveStructuredOutputMode.test.js +0 -117
- package/src/common/enum.js +0 -192
- package/src/common/index.js +0 -3
- package/src/events.js +0 -166
- package/src/graphs/Graph.js +0 -1857
- package/src/graphs/MultiAgentGraph.js +0 -1092
- package/src/graphs/__tests__/structured-output.integration.test.js +0 -624
- package/src/graphs/__tests__/structured-output.test.js +0 -144
- package/src/graphs/contextManagement.e2e.test.js +0 -718
- package/src/graphs/contextManagement.test.js +0 -485
- package/src/graphs/handoffValidation.test.js +0 -276
- package/src/graphs/index.js +0 -3
- package/src/index.js +0 -28
- package/src/instrumentation.js +0 -21
- package/src/llm/anthropic/index.js +0 -319
- package/src/llm/anthropic/types.js +0 -46
- package/src/llm/anthropic/utils/message_inputs.js +0 -627
- package/src/llm/anthropic/utils/message_outputs.js +0 -290
- package/src/llm/anthropic/utils/output_parsers.js +0 -89
- package/src/llm/anthropic/utils/tools.js +0 -25
- package/src/llm/bedrock/__tests__/bedrock-caching.test.js +0 -392
- package/src/llm/bedrock/index.js +0 -303
- package/src/llm/bedrock/types.js +0 -2
- package/src/llm/bedrock/utils/index.js +0 -6
- package/src/llm/bedrock/utils/message_inputs.js +0 -463
- package/src/llm/bedrock/utils/message_outputs.js +0 -269
- package/src/llm/fake.js +0 -92
- package/src/llm/google/index.js +0 -215
- package/src/llm/google/types.js +0 -12
- package/src/llm/google/utils/common.js +0 -670
- package/src/llm/google/utils/tools.js +0 -111
- package/src/llm/google/utils/zod_to_genai_parameters.js +0 -47
- package/src/llm/openai/index.js +0 -1033
- package/src/llm/openai/types.js +0 -2
- package/src/llm/openai/utils/index.js +0 -756
- package/src/llm/openai/utils/isReasoningModel.test.js +0 -79
- package/src/llm/openrouter/index.js +0 -261
- package/src/llm/openrouter/reasoning.test.js +0 -181
- package/src/llm/providers.js +0 -36
- package/src/llm/text.js +0 -65
- package/src/llm/vertexai/index.js +0 -402
- package/src/messages/__tests__/tools.test.js +0 -392
- package/src/messages/cache.js +0 -404
- package/src/messages/cache.test.js +0 -1167
- package/src/messages/content.js +0 -48
- package/src/messages/content.test.js +0 -314
- package/src/messages/core.js +0 -359
- package/src/messages/ensureThinkingBlock.test.js +0 -997
- package/src/messages/format.js +0 -973
- package/src/messages/formatAgentMessages.test.js +0 -2278
- package/src/messages/formatAgentMessages.tools.test.js +0 -362
- package/src/messages/formatMessage.test.js +0 -608
- package/src/messages/ids.js +0 -18
- package/src/messages/index.js +0 -9
- package/src/messages/labelContentByAgent.test.js +0 -725
- package/src/messages/prune.js +0 -438
- package/src/messages/reducer.js +0 -60
- package/src/messages/shiftIndexTokenCountMap.test.js +0 -63
- package/src/messages/summarize.js +0 -146
- package/src/messages/summarize.test.js +0 -332
- package/src/messages/tools.js +0 -90
- package/src/mockStream.js +0 -81
- package/src/prompts/collab.js +0 -7
- package/src/prompts/index.js +0 -3
- package/src/prompts/taskmanager.js +0 -58
- package/src/run.js +0 -427
- package/src/schemas/index.js +0 -3
- package/src/schemas/schema-preparation.test.js +0 -370
- package/src/schemas/validate.js +0 -314
- package/src/schemas/validate.test.js +0 -264
- package/src/scripts/abort.js +0 -127
- package/src/scripts/ant_web_search.js +0 -130
- package/src/scripts/ant_web_search_edge_case.js +0 -133
- package/src/scripts/ant_web_search_error_edge_case.js +0 -119
- package/src/scripts/args.js +0 -41
- package/src/scripts/bedrock-cache-debug.js +0 -186
- package/src/scripts/bedrock-content-aggregation-test.js +0 -195
- package/src/scripts/bedrock-merge-test.js +0 -80
- package/src/scripts/bedrock-parallel-tools-test.js +0 -150
- package/src/scripts/caching.js +0 -106
- package/src/scripts/cli.js +0 -152
- package/src/scripts/cli2.js +0 -119
- package/src/scripts/cli3.js +0 -163
- package/src/scripts/cli4.js +0 -165
- package/src/scripts/cli5.js +0 -165
- package/src/scripts/code_exec.js +0 -171
- package/src/scripts/code_exec_files.js +0 -180
- package/src/scripts/code_exec_multi_session.js +0 -185
- package/src/scripts/code_exec_ptc.js +0 -265
- package/src/scripts/code_exec_session.js +0 -217
- package/src/scripts/code_exec_simple.js +0 -120
- package/src/scripts/content.js +0 -111
- package/src/scripts/empty_input.js +0 -125
- package/src/scripts/handoff-test.js +0 -96
- package/src/scripts/image.js +0 -138
- package/src/scripts/memory.js +0 -83
- package/src/scripts/multi-agent-chain.js +0 -271
- package/src/scripts/multi-agent-conditional.js +0 -185
- package/src/scripts/multi-agent-document-review-chain.js +0 -171
- package/src/scripts/multi-agent-hybrid-flow.js +0 -264
- package/src/scripts/multi-agent-parallel-start.js +0 -214
- package/src/scripts/multi-agent-parallel.js +0 -346
- package/src/scripts/multi-agent-sequence.js +0 -184
- package/src/scripts/multi-agent-supervisor.js +0 -324
- package/src/scripts/multi-agent-test.js +0 -147
- package/src/scripts/parallel-asymmetric-tools-test.js +0 -202
- package/src/scripts/parallel-full-metadata-test.js +0 -176
- package/src/scripts/parallel-tools-test.js +0 -256
- package/src/scripts/programmatic_exec.js +0 -277
- package/src/scripts/programmatic_exec_agent.js +0 -168
- package/src/scripts/search.js +0 -118
- package/src/scripts/sequential-full-metadata-test.js +0 -143
- package/src/scripts/simple.js +0 -174
- package/src/scripts/single-agent-metadata-test.js +0 -152
- package/src/scripts/stream.js +0 -113
- package/src/scripts/test-custom-prompt-key.js +0 -132
- package/src/scripts/test-handoff-input.js +0 -143
- package/src/scripts/test-handoff-preamble.js +0 -227
- package/src/scripts/test-handoff-steering.js +0 -353
- package/src/scripts/test-multi-agent-list-handoff.js +0 -318
- package/src/scripts/test-parallel-agent-labeling.js +0 -253
- package/src/scripts/test-parallel-handoffs.js +0 -229
- package/src/scripts/test-thinking-handoff-bedrock.js +0 -132
- package/src/scripts/test-thinking-handoff.js +0 -132
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +0 -140
- package/src/scripts/test-tool-before-handoff-role-order.js +0 -223
- package/src/scripts/test-tools-before-handoff.js +0 -187
- package/src/scripts/test_code_api.js +0 -263
- package/src/scripts/thinking-bedrock.js +0 -128
- package/src/scripts/thinking-vertexai.js +0 -130
- package/src/scripts/thinking.js +0 -134
- package/src/scripts/tool_search.js +0 -114
- package/src/scripts/tools.js +0 -125
- package/src/specs/agent-handoffs-bedrock.integration.test.js +0 -280
- package/src/specs/agent-handoffs.test.js +0 -924
- package/src/specs/anthropic.simple.test.js +0 -287
- package/src/specs/azure.simple.test.js +0 -381
- package/src/specs/cache.simple.test.js +0 -282
- package/src/specs/custom-event-await.test.js +0 -148
- package/src/specs/deepseek.simple.test.js +0 -189
- package/src/specs/emergency-prune.test.js +0 -308
- package/src/specs/moonshot.simple.test.js +0 -237
- package/src/specs/observability.integration.test.js +0 -1337
- package/src/specs/openai.simple.test.js +0 -233
- package/src/specs/openrouter.simple.test.js +0 -202
- package/src/specs/prune.test.js +0 -733
- package/src/specs/reasoning.test.js +0 -144
- package/src/specs/spec.utils.js +0 -4
- package/src/specs/thinking-handoff.test.js +0 -486
- package/src/specs/thinking-prune.test.js +0 -600
- package/src/specs/token-distribution-edge-case.test.js +0 -246
- package/src/specs/token-memoization.test.js +0 -32
- package/src/specs/tokens.test.js +0 -49
- package/src/specs/tool-error.test.js +0 -139
- package/src/splitStream.js +0 -204
- package/src/splitStream.test.js +0 -504
- package/src/stream.js +0 -650
- package/src/stream.test.js +0 -225
- package/src/test/mockTools.js +0 -340
- package/src/tools/BrowserTools.js +0 -245
- package/src/tools/Calculator.js +0 -38
- package/src/tools/Calculator.test.js +0 -225
- package/src/tools/CodeExecutor.js +0 -233
- package/src/tools/ProgrammaticToolCalling.js +0 -602
- package/src/tools/StreamingToolCallBuffer.js +0 -179
- package/src/tools/ToolNode.js +0 -930
- package/src/tools/ToolSearch.js +0 -904
- package/src/tools/__tests__/BrowserTools.test.js +0 -306
- package/src/tools/__tests__/ProgrammaticToolCalling.integration.test.js +0 -276
- package/src/tools/__tests__/ProgrammaticToolCalling.test.js +0 -807
- package/src/tools/__tests__/StreamingToolCallBuffer.test.js +0 -175
- package/src/tools/__tests__/ToolApproval.test.js +0 -675
- package/src/tools/__tests__/ToolNode.recovery.test.js +0 -200
- package/src/tools/__tests__/ToolNode.session.test.js +0 -319
- package/src/tools/__tests__/ToolSearch.integration.test.js +0 -125
- package/src/tools/__tests__/ToolSearch.test.js +0 -812
- package/src/tools/__tests__/handlers.test.js +0 -799
- package/src/tools/__tests__/truncation-recovery.integration.test.js +0 -362
- package/src/tools/handlers.js +0 -306
- package/src/tools/schema.js +0 -25
- package/src/tools/search/anthropic.js +0 -34
- package/src/tools/search/content.js +0 -116
- package/src/tools/search/content.test.js +0 -133
- package/src/tools/search/firecrawl.js +0 -173
- package/src/tools/search/format.js +0 -198
- package/src/tools/search/highlights.js +0 -241
- package/src/tools/search/index.js +0 -3
- package/src/tools/search/jina-reranker.test.js +0 -106
- package/src/tools/search/rerankers.js +0 -165
- package/src/tools/search/schema.js +0 -102
- package/src/tools/search/search.js +0 -561
- package/src/tools/search/serper-scraper.js +0 -126
- package/src/tools/search/test.js +0 -129
- package/src/tools/search/tool.js +0 -453
- package/src/tools/search/types.js +0 -2
- package/src/tools/search/utils.js +0 -59
- package/src/types/graph.js +0 -24
- package/src/types/graph.test.js +0 -192
- package/src/types/index.js +0 -7
- package/src/types/llm.js +0 -2
- package/src/types/messages.js +0 -2
- package/src/types/run.js +0 -2
- package/src/types/stream.js +0 -2
- package/src/types/tools.js +0 -2
- package/src/utils/contextAnalytics.js +0 -79
- package/src/utils/contextAnalytics.test.js +0 -166
- package/src/utils/events.js +0 -26
- package/src/utils/graph.js +0 -11
- package/src/utils/handlers.js +0 -65
- package/src/utils/index.js +0 -10
- package/src/utils/llm.js +0 -21
- package/src/utils/llmConfig.js +0 -205
- package/src/utils/logging.js +0 -37
- package/src/utils/misc.js +0 -51
- package/src/utils/run.js +0 -69
- package/src/utils/schema.js +0 -21
- package/src/utils/title.js +0 -119
- package/src/utils/tokens.js +0 -92
- package/src/utils/toonFormat.js +0 -379
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration test: Full Truncation Recovery Pipeline
|
|
3
|
-
*
|
|
4
|
-
* Tests the end-to-end flow that would catch the Bedrock buffer bug:
|
|
5
|
-
* Streaming chunks → handleToolCallChunks → StreamingToolCallBuffer → handleToolCalls → ToolNode.recoverTruncatedArgs → tool receives recovered args
|
|
6
|
-
*
|
|
7
|
-
* Uses real StreamingToolCallBuffer (not mocked) and real ToolNode with real tool definitions.
|
|
8
|
-
* Only the graph dispatch layer is mocked (since we're not testing SSE dispatch).
|
|
9
|
-
*
|
|
10
|
-
* Verified against live Bedrock API output (see bedrock-live-poc.ts):
|
|
11
|
-
* START chunk: { id, name, index, args: "" }
|
|
12
|
-
* DELTA chunks: { args, index } — NO id, NO name
|
|
13
|
-
*/
|
|
14
|
-
import { z } from 'zod';
|
|
15
|
-
import { tool } from '@langchain/core/tools';
|
|
16
|
-
import { AIMessage } from '@langchain/core/messages';
|
|
17
|
-
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
|
18
|
-
import { StepTypes } from '@/common';
|
|
19
|
-
import { StreamingToolCallBuffer } from '../StreamingToolCallBuffer';
|
|
20
|
-
import { handleToolCallChunks } from '../handlers';
|
|
21
|
-
import { ToolNode } from '../ToolNode';
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Helpers
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
/** Creates a mock graph that delegates buffer operations to a REAL StreamingToolCallBuffer */
|
|
26
|
-
function createIntegrationGraph(buffer) {
|
|
27
|
-
let stepCounter = 0;
|
|
28
|
-
return {
|
|
29
|
-
getStepKey: jest.fn().mockReturnValue('step-key'),
|
|
30
|
-
getStepIdByKey: jest.fn().mockReturnValue('prev-step-id'),
|
|
31
|
-
getRunStep: jest.fn().mockReturnValue({
|
|
32
|
-
type: StepTypes.MESSAGE_CREATION,
|
|
33
|
-
id: 'prev-step-id',
|
|
34
|
-
index: 0,
|
|
35
|
-
stepDetails: { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'msg-1' } },
|
|
36
|
-
usage: null,
|
|
37
|
-
}),
|
|
38
|
-
dispatchRunStep: jest.fn()
|
|
39
|
-
.mockImplementation(async () => `step-${++stepCounter}`),
|
|
40
|
-
dispatchRunStepDelta: jest.fn().mockResolvedValue(undefined),
|
|
41
|
-
toolCallStepIds: new Map(),
|
|
42
|
-
messageStepHasToolCalls: new Map(),
|
|
43
|
-
messageIdsByStepKey: new Map(),
|
|
44
|
-
prelimMessageIdsByStepKey: new Map(),
|
|
45
|
-
streamingToolCallBuffer: buffer, // REAL buffer, not mocked
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
/** Creates a tool that captures received args for assertion */
|
|
49
|
-
function createCaptureTool(name, schema, capturedArgs) {
|
|
50
|
-
return tool(async (input) => {
|
|
51
|
-
capturedArgs.push({ ...input });
|
|
52
|
-
return `OK`;
|
|
53
|
-
}, { name, description: `Test tool: ${name}`, schema });
|
|
54
|
-
}
|
|
55
|
-
/** Simulates Bedrock streaming: START chunk with {id, name, index, args:""}, then DELTA chunks with {args, index} */
|
|
56
|
-
function simulateBedrockChunks(toolCallId, toolName, index, argsJson, chunkSize = 30) {
|
|
57
|
-
const chunks = [];
|
|
58
|
-
// START chunk: id + name + index, args="" (verified from live Bedrock API)
|
|
59
|
-
chunks.push({
|
|
60
|
-
id: toolCallId,
|
|
61
|
-
name: toolName,
|
|
62
|
-
index,
|
|
63
|
-
args: '',
|
|
64
|
-
type: 'tool_call_chunk',
|
|
65
|
-
});
|
|
66
|
-
// DELTA chunks: args + index, NO id, NO name (verified from live Bedrock API)
|
|
67
|
-
for (let i = 0; i < argsJson.length; i += chunkSize) {
|
|
68
|
-
chunks.push({
|
|
69
|
-
id: undefined,
|
|
70
|
-
name: undefined,
|
|
71
|
-
index,
|
|
72
|
-
args: argsJson.substring(i, i + chunkSize),
|
|
73
|
-
type: 'tool_call_chunk',
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
return chunks;
|
|
77
|
-
}
|
|
78
|
-
/** Simulates OpenAI/Anthropic streaming: every chunk has {id, args} */
|
|
79
|
-
function simulateOpenAIChunks(toolCallId, toolName, index, argsJson, chunkSize = 30) {
|
|
80
|
-
const chunks = [];
|
|
81
|
-
// First chunk: id + name + first args fragment
|
|
82
|
-
const firstArgs = argsJson.substring(0, chunkSize);
|
|
83
|
-
chunks.push({
|
|
84
|
-
id: toolCallId,
|
|
85
|
-
name: toolName,
|
|
86
|
-
index,
|
|
87
|
-
args: firstArgs,
|
|
88
|
-
type: 'tool_call_chunk',
|
|
89
|
-
});
|
|
90
|
-
// Subsequent chunks: id + args (OpenAI sends id on every chunk)
|
|
91
|
-
for (let i = chunkSize; i < argsJson.length; i += chunkSize) {
|
|
92
|
-
chunks.push({
|
|
93
|
-
id: toolCallId,
|
|
94
|
-
name: undefined,
|
|
95
|
-
index,
|
|
96
|
-
args: argsJson.substring(i, i + chunkSize),
|
|
97
|
-
type: 'tool_call_chunk',
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
return chunks;
|
|
101
|
-
}
|
|
102
|
-
const contentToolSchema = z.object({
|
|
103
|
-
action: z.string(),
|
|
104
|
-
filename: z.string().optional(),
|
|
105
|
-
content: z.string().optional(),
|
|
106
|
-
});
|
|
107
|
-
const codeExecutorSchema = z.object({
|
|
108
|
-
code: z.string(),
|
|
109
|
-
language: z.string().optional(),
|
|
110
|
-
});
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// Tests
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
describe('Truncation Recovery Pipeline (Integration)', () => {
|
|
115
|
-
let buffer;
|
|
116
|
-
beforeEach(() => {
|
|
117
|
-
buffer = new StreamingToolCallBuffer();
|
|
118
|
-
});
|
|
119
|
-
it('Bedrock truncated write — full pipeline recovery', async () => {
|
|
120
|
-
const capturedArgs = [];
|
|
121
|
-
const graph = createIntegrationGraph(buffer);
|
|
122
|
-
// Full args JSON that Bedrock would stream (including content)
|
|
123
|
-
const fullArgsJson = '{"action":"write","filename":"app.tsx","content":"import React from \\"react\\";\\nexport default function App() {\\n return <div>Hello World</div>;\\n}"}';
|
|
124
|
-
// Simulate Bedrock streaming: START + DELTA chunks
|
|
125
|
-
const chunks = simulateBedrockChunks('tooluse_abc', 'content_tool', 0, fullArgsJson, 20);
|
|
126
|
-
// Feed all chunks through handleToolCallChunks (real buffer accumulation)
|
|
127
|
-
for (const chunk of chunks) {
|
|
128
|
-
await handleToolCallChunks({
|
|
129
|
-
graph: graph,
|
|
130
|
-
stepKey: 'step-key',
|
|
131
|
-
toolCallChunks: [chunk],
|
|
132
|
-
metadata: { run_id: 'test' },
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
// Verify buffer was populated
|
|
136
|
-
expect(buffer.has('tooluse_abc')).toBe(true);
|
|
137
|
-
expect(buffer.getRawArgs('tooluse_abc')).toBe(fullArgsJson);
|
|
138
|
-
// Simulate truncation: parsePartialJson lost the content field
|
|
139
|
-
const truncatedArgs = { action: 'write', filename: 'app.tsx' }; // content MISSING
|
|
140
|
-
// Create ToolNode with real buffer
|
|
141
|
-
const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
|
|
142
|
-
const toolNode = new ToolNode({
|
|
143
|
-
tools: [contentTool],
|
|
144
|
-
streamingToolCallBuffer: buffer,
|
|
145
|
-
});
|
|
146
|
-
// Invoke ToolNode with truncated args
|
|
147
|
-
const aiMsg = new AIMessage({
|
|
148
|
-
content: '',
|
|
149
|
-
tool_calls: [{ id: 'tooluse_abc', name: 'content_tool', args: truncatedArgs }],
|
|
150
|
-
});
|
|
151
|
-
await toolNode.invoke({ messages: [aiMsg] });
|
|
152
|
-
// Assert: tool received RECOVERED content, not truncated
|
|
153
|
-
expect(capturedArgs).toHaveLength(1);
|
|
154
|
-
expect(capturedArgs[0].action).toBe('write');
|
|
155
|
-
expect(capturedArgs[0].filename).toBe('app.tsx');
|
|
156
|
-
expect(capturedArgs[0].content).toBe('import React from "react";\nexport default function App() {\n return <div>Hello World</div>;\n}');
|
|
157
|
-
});
|
|
158
|
-
it('OpenAI/Anthropic chunked write — full pipeline recovery', async () => {
|
|
159
|
-
const capturedArgs = [];
|
|
160
|
-
const graph = createIntegrationGraph(buffer);
|
|
161
|
-
const fullArgsJson = '{"action":"write","filename":"test.tsx","content":"const x = 42;\\nconst y = 100;"}';
|
|
162
|
-
const chunks = simulateOpenAIChunks('tc_openai', 'content_tool', 0, fullArgsJson, 15);
|
|
163
|
-
for (const chunk of chunks) {
|
|
164
|
-
await handleToolCallChunks({
|
|
165
|
-
graph: graph,
|
|
166
|
-
stepKey: 'step-key',
|
|
167
|
-
toolCallChunks: [chunk],
|
|
168
|
-
metadata: { run_id: 'test' },
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
expect(buffer.has('tc_openai')).toBe(true);
|
|
172
|
-
expect(buffer.getRawArgs('tc_openai')).toBe(fullArgsJson);
|
|
173
|
-
// Simulate truncation: content missing
|
|
174
|
-
const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
|
|
175
|
-
const toolNode = new ToolNode({
|
|
176
|
-
tools: [contentTool],
|
|
177
|
-
streamingToolCallBuffer: buffer,
|
|
178
|
-
});
|
|
179
|
-
const aiMsg = new AIMessage({
|
|
180
|
-
content: '',
|
|
181
|
-
tool_calls: [{ id: 'tc_openai', name: 'content_tool', args: { action: 'write', filename: 'test.tsx' } }],
|
|
182
|
-
});
|
|
183
|
-
await toolNode.invoke({ messages: [aiMsg] });
|
|
184
|
-
expect(capturedArgs[0].content).toBe('const x = 42;\nconst y = 100;');
|
|
185
|
-
});
|
|
186
|
-
it('multiple parallel tool calls — no cross-contamination', async () => {
|
|
187
|
-
const capturedArgs = [];
|
|
188
|
-
const graph = createIntegrationGraph(buffer);
|
|
189
|
-
// Two parallel tool calls with different indexes
|
|
190
|
-
const args1 = '{"action":"write","filename":"a.tsx","content":"file A content"}';
|
|
191
|
-
const args2 = '{"action":"write","filename":"b.tsx","content":"file B content"}';
|
|
192
|
-
const chunks1 = simulateBedrockChunks('tc_1', 'content_tool', 0, args1, 15);
|
|
193
|
-
const chunks2 = simulateBedrockChunks('tc_2', 'content_tool', 1, args2, 15);
|
|
194
|
-
// Interleave chunks (realistic concurrent streaming)
|
|
195
|
-
const interleaved = [];
|
|
196
|
-
const maxLen = Math.max(chunks1.length, chunks2.length);
|
|
197
|
-
for (let i = 0; i < maxLen; i++) {
|
|
198
|
-
if (i < chunks1.length)
|
|
199
|
-
interleaved.push(chunks1[i]);
|
|
200
|
-
if (i < chunks2.length)
|
|
201
|
-
interleaved.push(chunks2[i]);
|
|
202
|
-
}
|
|
203
|
-
for (const chunk of interleaved) {
|
|
204
|
-
await handleToolCallChunks({
|
|
205
|
-
graph: graph,
|
|
206
|
-
stepKey: 'step-key',
|
|
207
|
-
toolCallChunks: [chunk],
|
|
208
|
-
metadata: { run_id: 'test' },
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
expect(buffer.getRawArgs('tc_1')).toBe(args1);
|
|
212
|
-
expect(buffer.getRawArgs('tc_2')).toBe(args2);
|
|
213
|
-
const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
|
|
214
|
-
const toolNode = new ToolNode({
|
|
215
|
-
tools: [contentTool],
|
|
216
|
-
streamingToolCallBuffer: buffer,
|
|
217
|
-
});
|
|
218
|
-
// Only tc_1 is truncated (missing content), tc_2 has all fields
|
|
219
|
-
const aiMsg = new AIMessage({
|
|
220
|
-
content: '',
|
|
221
|
-
tool_calls: [
|
|
222
|
-
{ id: 'tc_1', name: 'content_tool', args: { action: 'write', filename: 'a.tsx' } }, // truncated
|
|
223
|
-
{ id: 'tc_2', name: 'content_tool', args: { action: 'write', filename: 'b.tsx', content: 'file B content' } }, // complete
|
|
224
|
-
],
|
|
225
|
-
});
|
|
226
|
-
await toolNode.invoke({ messages: [aiMsg] });
|
|
227
|
-
expect(capturedArgs).toHaveLength(2);
|
|
228
|
-
// tc_1: content recovered from buffer
|
|
229
|
-
expect(capturedArgs[0].content).toBe('file A content');
|
|
230
|
-
// tc_2: content unchanged (already present in parsed args)
|
|
231
|
-
expect(capturedArgs[1].content).toBe('file B content');
|
|
232
|
-
});
|
|
233
|
-
it('buffer cleanup after recovery — no stale data', async () => {
|
|
234
|
-
const capturedArgs = [];
|
|
235
|
-
const graph = createIntegrationGraph(buffer);
|
|
236
|
-
const fullArgs = '{"action":"write","filename":"x.tsx","content":"test"}';
|
|
237
|
-
const chunks = simulateBedrockChunks('tc_cleanup', 'content_tool', 0, fullArgs);
|
|
238
|
-
for (const chunk of chunks) {
|
|
239
|
-
await handleToolCallChunks({
|
|
240
|
-
graph: graph,
|
|
241
|
-
stepKey: 'step-key',
|
|
242
|
-
toolCallChunks: [chunk],
|
|
243
|
-
metadata: { run_id: 'test' },
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
expect(buffer.has('tc_cleanup')).toBe(true);
|
|
247
|
-
const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
|
|
248
|
-
const toolNode = new ToolNode({
|
|
249
|
-
tools: [contentTool],
|
|
250
|
-
streamingToolCallBuffer: buffer,
|
|
251
|
-
});
|
|
252
|
-
const aiMsg = new AIMessage({
|
|
253
|
-
content: '',
|
|
254
|
-
tool_calls: [{ id: 'tc_cleanup', name: 'content_tool', args: { action: 'write' } }],
|
|
255
|
-
});
|
|
256
|
-
await toolNode.invoke({ messages: [aiMsg] });
|
|
257
|
-
// Buffer should be cleared after processing
|
|
258
|
-
expect(buffer.has('tc_cleanup')).toBe(false);
|
|
259
|
-
expect(buffer.getRawArgs('tc_cleanup')).toBeUndefined();
|
|
260
|
-
});
|
|
261
|
-
it('no truncation — buffer populated but not used for overwrite', async () => {
|
|
262
|
-
const capturedArgs = [];
|
|
263
|
-
const graph = createIntegrationGraph(buffer);
|
|
264
|
-
const fullArgs = '{"action":"read","filename":"existing.tsx"}';
|
|
265
|
-
const chunks = simulateBedrockChunks('tc_complete', 'content_tool', 0, fullArgs);
|
|
266
|
-
for (const chunk of chunks) {
|
|
267
|
-
await handleToolCallChunks({
|
|
268
|
-
graph: graph,
|
|
269
|
-
stepKey: 'step-key',
|
|
270
|
-
toolCallChunks: [chunk],
|
|
271
|
-
metadata: { run_id: 'test' },
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
|
|
275
|
-
const toolNode = new ToolNode({
|
|
276
|
-
tools: [contentTool],
|
|
277
|
-
streamingToolCallBuffer: buffer,
|
|
278
|
-
});
|
|
279
|
-
// Complete args — nothing missing
|
|
280
|
-
const aiMsg = new AIMessage({
|
|
281
|
-
content: '',
|
|
282
|
-
tool_calls: [{ id: 'tc_complete', name: 'content_tool', args: { action: 'read', filename: 'existing.tsx' } }],
|
|
283
|
-
});
|
|
284
|
-
await toolNode.invoke({ messages: [aiMsg] });
|
|
285
|
-
expect(capturedArgs[0].action).toBe('read');
|
|
286
|
-
expect(capturedArgs[0].filename).toBe('existing.tsx');
|
|
287
|
-
// No content field in args or buffer — should stay absent
|
|
288
|
-
expect(capturedArgs[0]).not.toHaveProperty('content');
|
|
289
|
-
});
|
|
290
|
-
it('generic tool recovery — non-content_tool (code_executor)', async () => {
|
|
291
|
-
const capturedArgs = [];
|
|
292
|
-
const graph = createIntegrationGraph(buffer);
|
|
293
|
-
const largeCode = 'function fibonacci(n) {\\n if (n <= 1) return n;\\n return fibonacci(n-1) + fibonacci(n-2);\\n}\\nconsole.log(fibonacci(10));';
|
|
294
|
-
const fullArgs = `{"code":"${largeCode}","language":"javascript"}`;
|
|
295
|
-
const chunks = simulateBedrockChunks('tc_code', 'execute_code', 0, fullArgs, 20);
|
|
296
|
-
for (const chunk of chunks) {
|
|
297
|
-
await handleToolCallChunks({
|
|
298
|
-
graph: graph,
|
|
299
|
-
stepKey: 'step-key',
|
|
300
|
-
toolCallChunks: [chunk],
|
|
301
|
-
metadata: { run_id: 'test' },
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
const codeTool = createCaptureTool('execute_code', codeExecutorSchema, capturedArgs);
|
|
305
|
-
const toolNode = new ToolNode({
|
|
306
|
-
tools: [codeTool],
|
|
307
|
-
streamingToolCallBuffer: buffer,
|
|
308
|
-
});
|
|
309
|
-
// Truncated: only has language, code is missing
|
|
310
|
-
const aiMsg = new AIMessage({
|
|
311
|
-
content: '',
|
|
312
|
-
tool_calls: [{ id: 'tc_code', name: 'execute_code', args: { language: 'javascript' } }],
|
|
313
|
-
});
|
|
314
|
-
await toolNode.invoke({ messages: [aiMsg] });
|
|
315
|
-
expect(capturedArgs[0].code).toContain('fibonacci');
|
|
316
|
-
expect(capturedArgs[0].language).toBe('javascript');
|
|
317
|
-
});
|
|
318
|
-
it('Bedrock large content (5000+ chars) — recovery preserves full content', async () => {
|
|
319
|
-
const capturedArgs = [];
|
|
320
|
-
const graph = createIntegrationGraph(buffer);
|
|
321
|
-
// Generate large content (simulating a real dashboard component)
|
|
322
|
-
const lines = [];
|
|
323
|
-
lines.push('import React from \\"react\\";');
|
|
324
|
-
lines.push('import { useState, useEffect } from \\"react\\";');
|
|
325
|
-
for (let i = 0; i < 100; i++) {
|
|
326
|
-
lines.push(`// Component section ${i}`);
|
|
327
|
-
lines.push(`const Section${i} = () => <div>Section ${i}</div>;`);
|
|
328
|
-
}
|
|
329
|
-
lines.push('export default function Dashboard() {');
|
|
330
|
-
lines.push(' return <div>Dashboard</div>;');
|
|
331
|
-
lines.push('}');
|
|
332
|
-
const content = lines.join('\\n');
|
|
333
|
-
const fullArgs = `{"action":"write","filename":"dashboard.tsx","content":"${content}"}`;
|
|
334
|
-
const chunks = simulateBedrockChunks('tc_large', 'content_tool', 0, fullArgs, 50);
|
|
335
|
-
for (const chunk of chunks) {
|
|
336
|
-
await handleToolCallChunks({
|
|
337
|
-
graph: graph,
|
|
338
|
-
stepKey: 'step-key',
|
|
339
|
-
toolCallChunks: [chunk],
|
|
340
|
-
metadata: { run_id: 'test' },
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
const contentTool = createCaptureTool('content_tool', contentToolSchema, capturedArgs);
|
|
344
|
-
const toolNode = new ToolNode({
|
|
345
|
-
tools: [contentTool],
|
|
346
|
-
streamingToolCallBuffer: buffer,
|
|
347
|
-
});
|
|
348
|
-
// Completely empty args (truncation wiped everything)
|
|
349
|
-
const aiMsg = new AIMessage({
|
|
350
|
-
content: '',
|
|
351
|
-
tool_calls: [{ id: 'tc_large', name: 'content_tool', args: {} }],
|
|
352
|
-
});
|
|
353
|
-
await toolNode.invoke({ messages: [aiMsg] });
|
|
354
|
-
expect(capturedArgs[0].action).toBe('write');
|
|
355
|
-
expect(capturedArgs[0].filename).toBe('dashboard.tsx');
|
|
356
|
-
const recoveredContent = capturedArgs[0].content;
|
|
357
|
-
expect(recoveredContent).toContain('import React from "react"');
|
|
358
|
-
expect(recoveredContent).toContain('Section 99');
|
|
359
|
-
expect(recoveredContent).toContain('export default function Dashboard()');
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
//# sourceMappingURL=truncation-recovery.integration.test.js.map
|
package/src/tools/handlers.js
DELETED
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
// src/tools/handlers.ts
|
|
3
|
-
import { nanoid } from 'nanoid';
|
|
4
|
-
import { ToolMessage } from '@langchain/core/messages';
|
|
5
|
-
import { ToolCallTypes, GraphEvents, StepTypes, Providers, Constants, } from '@/common';
|
|
6
|
-
import { coerceAnthropicSearchResults, isAnthropicWebSearchResult, } from '@/tools/search/anthropic';
|
|
7
|
-
import { formatResultsForLLM } from '@/tools/search/format';
|
|
8
|
-
import { getMessageId } from '@/messages';
|
|
9
|
-
export async function handleToolCallChunks({ graph, stepKey, toolCallChunks, metadata, }) {
|
|
10
|
-
let prevStepId;
|
|
11
|
-
let prevRunStep;
|
|
12
|
-
try {
|
|
13
|
-
prevStepId = graph.getStepIdByKey(stepKey);
|
|
14
|
-
prevRunStep = graph.getRunStep(prevStepId);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
/** Edge Case: If no previous step exists, create a new message creation step */
|
|
18
|
-
const message_id = getMessageId(stepKey, graph, true) ?? '';
|
|
19
|
-
prevStepId = await graph.dispatchRunStep(stepKey, {
|
|
20
|
-
type: StepTypes.MESSAGE_CREATION,
|
|
21
|
-
message_creation: {
|
|
22
|
-
message_id,
|
|
23
|
-
},
|
|
24
|
-
}, metadata);
|
|
25
|
-
prevRunStep = graph.getRunStep(prevStepId);
|
|
26
|
-
}
|
|
27
|
-
const _stepId = graph.getStepIdByKey(stepKey);
|
|
28
|
-
/** Edge Case: Tool Call Run Step or `tool_call_ids` never dispatched */
|
|
29
|
-
const tool_calls = prevStepId && prevRunStep && prevRunStep.type === StepTypes.MESSAGE_CREATION
|
|
30
|
-
? []
|
|
31
|
-
: undefined;
|
|
32
|
-
/**
|
|
33
|
-
* Feed streaming tool call buffer — accumulate raw arg strings for truncation recovery.
|
|
34
|
-
*
|
|
35
|
-
* Provider chunk patterns:
|
|
36
|
-
* - OpenAI/Anthropic: every chunk has {id, args} → direct append
|
|
37
|
-
* - Bedrock: START chunk has {id, name, index}, DELTA chunks have {args, index} (no id)
|
|
38
|
-
* → use index-to-id mapping to resolve the target buffer entry
|
|
39
|
-
*/
|
|
40
|
-
for (const toolCallChunk of toolCallChunks) {
|
|
41
|
-
const chunkIndex = toolCallChunk.index;
|
|
42
|
-
// START chunk: has id (and usually name). Store index→id mapping for future DELTA chunks.
|
|
43
|
-
if (toolCallChunk.id) {
|
|
44
|
-
if (typeof chunkIndex === 'number') {
|
|
45
|
-
graph.streamingToolCallBuffer.setIndexMapping(chunkIndex, toolCallChunk.id);
|
|
46
|
-
}
|
|
47
|
-
if (toolCallChunk.name) {
|
|
48
|
-
graph.streamingToolCallBuffer.setToolName(toolCallChunk.id, toolCallChunk.name);
|
|
49
|
-
}
|
|
50
|
-
// Append args if present on the same chunk (OpenAI/Anthropic pattern)
|
|
51
|
-
if (toolCallChunk.args) {
|
|
52
|
-
graph.streamingToolCallBuffer.append(toolCallChunk.id, toolCallChunk.args);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
else if (toolCallChunk.args && typeof chunkIndex === 'number') {
|
|
56
|
-
// DELTA chunk: no id, but has args + index. Resolve id via index mapping (Bedrock pattern).
|
|
57
|
-
const resolvedId = graph.streamingToolCallBuffer.getIdByIndex(chunkIndex);
|
|
58
|
-
if (resolvedId) {
|
|
59
|
-
graph.streamingToolCallBuffer.append(resolvedId, toolCallChunk.args);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/** Edge Case: `id` and `name` fields cannot be empty strings */
|
|
64
|
-
for (const toolCallChunk of toolCallChunks) {
|
|
65
|
-
if (toolCallChunk.name === '') {
|
|
66
|
-
toolCallChunk.name = undefined;
|
|
67
|
-
}
|
|
68
|
-
if (toolCallChunk.id === '') {
|
|
69
|
-
toolCallChunk.id = undefined;
|
|
70
|
-
}
|
|
71
|
-
else if (tool_calls != null &&
|
|
72
|
-
toolCallChunk.id != null &&
|
|
73
|
-
toolCallChunk.name != null) {
|
|
74
|
-
tool_calls.push({
|
|
75
|
-
args: {},
|
|
76
|
-
id: toolCallChunk.id,
|
|
77
|
-
name: toolCallChunk.name,
|
|
78
|
-
type: ToolCallTypes.TOOL_CALL,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
let stepId = _stepId;
|
|
83
|
-
const alreadyDispatched = prevRunStep?.type === StepTypes.MESSAGE_CREATION &&
|
|
84
|
-
graph.messageStepHasToolCalls.has(prevStepId);
|
|
85
|
-
if (prevRunStep?.type === StepTypes.TOOL_CALLS) {
|
|
86
|
-
/**
|
|
87
|
-
* If previous step is already a tool_calls step, use that step ID
|
|
88
|
-
* This ensures tool call deltas are dispatched to the correct step
|
|
89
|
-
*/
|
|
90
|
-
stepId = prevStepId;
|
|
91
|
-
}
|
|
92
|
-
else if (!alreadyDispatched &&
|
|
93
|
-
prevRunStep?.type === StepTypes.MESSAGE_CREATION) {
|
|
94
|
-
/**
|
|
95
|
-
* Create tool_calls step as soon as we receive the first tool call chunk
|
|
96
|
-
* This ensures deltas are always associated with the correct step
|
|
97
|
-
*
|
|
98
|
-
* NOTE: We do NOT dispatch an empty text block here because:
|
|
99
|
-
* - Empty text blocks cause providers (Anthropic, Bedrock) to reject messages
|
|
100
|
-
* - The tool_calls themselves are sufficient for the step
|
|
101
|
-
* - Empty content with tool_call_ids gets stored in conversation history
|
|
102
|
-
* and causes "messages must have non-empty content" errors on replay
|
|
103
|
-
*/
|
|
104
|
-
graph.messageStepHasToolCalls.set(prevStepId, true);
|
|
105
|
-
stepId = await graph.dispatchRunStep(stepKey, {
|
|
106
|
-
type: StepTypes.TOOL_CALLS,
|
|
107
|
-
tool_calls: tool_calls ?? [],
|
|
108
|
-
}, metadata);
|
|
109
|
-
}
|
|
110
|
-
await graph.dispatchRunStepDelta(stepId, {
|
|
111
|
-
type: StepTypes.TOOL_CALLS,
|
|
112
|
-
tool_calls: toolCallChunks,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
export const handleToolCalls = async (toolCalls, metadata, graph) => {
|
|
116
|
-
if (!graph || !metadata) {
|
|
117
|
-
console.warn('Graph or metadata not found in `handleToolCalls`');
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (!toolCalls) {
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (toolCalls.length === 0) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const stepKey = graph.getStepKey(metadata);
|
|
127
|
-
/**
|
|
128
|
-
* Track whether we've already reused an empty TOOL_CALLS step created by
|
|
129
|
-
* handleToolCallChunks during streaming. Only reuse it once (for the first
|
|
130
|
-
* tool call); subsequent parallel tool calls must create their own steps.
|
|
131
|
-
*/
|
|
132
|
-
let reusedChunkStepId;
|
|
133
|
-
for (const tool_call of toolCalls) {
|
|
134
|
-
const toolCallId = tool_call.id ?? `toolu_${nanoid()}`;
|
|
135
|
-
tool_call.id = toolCallId;
|
|
136
|
-
// If this tool call ID was already tracked via handleToolCallChunks,
|
|
137
|
-
// the step exists but may lack the name (Bedrock sends name only at model end).
|
|
138
|
-
// Dispatch a delta with the complete data so the client can fill in the name.
|
|
139
|
-
if (toolCallId && graph.toolCallStepIds.has(toolCallId)) {
|
|
140
|
-
const existingStepId = graph.toolCallStepIds.get(toolCallId);
|
|
141
|
-
if (existingStepId != null && existingStepId !== '') {
|
|
142
|
-
const argsStr = typeof tool_call.args === 'string'
|
|
143
|
-
? tool_call.args
|
|
144
|
-
: JSON.stringify(tool_call.args);
|
|
145
|
-
await graph.dispatchRunStepDelta(existingStepId, {
|
|
146
|
-
type: StepTypes.TOOL_CALLS,
|
|
147
|
-
tool_calls: [{ name: tool_call.name, args: argsStr, id: toolCallId }],
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
let prevStepId = '';
|
|
153
|
-
let prevRunStep;
|
|
154
|
-
try {
|
|
155
|
-
prevStepId = graph.getStepIdByKey(stepKey);
|
|
156
|
-
prevRunStep = graph.getRunStep(prevStepId);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// no previous step
|
|
160
|
-
}
|
|
161
|
-
// If the previous step is TOOL_CALLS (created by handleToolCallChunks),
|
|
162
|
-
// either reuse it (if empty) or dispatch a new TOOL_CALLS step directly —
|
|
163
|
-
// skip the intermediate MESSAGE_CREATION to avoid orphaned gaps.
|
|
164
|
-
if (prevRunStep?.type === StepTypes.TOOL_CALLS) {
|
|
165
|
-
const details = prevRunStep.stepDetails;
|
|
166
|
-
const isEmpty = !details.tool_calls || details.tool_calls.length === 0;
|
|
167
|
-
if (isEmpty && prevStepId !== reusedChunkStepId) {
|
|
168
|
-
graph.toolCallStepIds.set(toolCallId, prevStepId);
|
|
169
|
-
reusedChunkStepId = prevStepId;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
await graph.dispatchRunStep(stepKey, { type: StepTypes.TOOL_CALLS, tool_calls: [tool_call] }, metadata);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* NOTE: We do NOT dispatch empty text blocks with tool_call_ids because:
|
|
177
|
-
* - Empty text blocks cause providers (Anthropic, Bedrock) to reject messages
|
|
178
|
-
* - They get stored in conversation history and cause errors on replay:
|
|
179
|
-
* "messages must have non-empty content" (Anthropic)
|
|
180
|
-
* "The content field in the Message object is empty" (Bedrock)
|
|
181
|
-
* - The tool_calls themselves are sufficient
|
|
182
|
-
*/
|
|
183
|
-
/* If the previous step exists and is a message creation */
|
|
184
|
-
if (prevStepId && prevRunStep) {
|
|
185
|
-
graph.messageStepHasToolCalls.set(prevStepId, true);
|
|
186
|
-
/* If the previous step doesn't exist */
|
|
187
|
-
}
|
|
188
|
-
else if (!prevRunStep) {
|
|
189
|
-
const messageId = getMessageId(stepKey, graph, true) ?? '';
|
|
190
|
-
const stepId = await graph.dispatchRunStep(stepKey, {
|
|
191
|
-
type: StepTypes.MESSAGE_CREATION,
|
|
192
|
-
message_creation: {
|
|
193
|
-
message_id: messageId,
|
|
194
|
-
},
|
|
195
|
-
}, metadata);
|
|
196
|
-
graph.messageStepHasToolCalls.set(stepId, true);
|
|
197
|
-
}
|
|
198
|
-
await graph.dispatchRunStep(stepKey, {
|
|
199
|
-
type: StepTypes.TOOL_CALLS,
|
|
200
|
-
tool_calls: [tool_call],
|
|
201
|
-
}, metadata);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
export const toolResultTypes = new Set([
|
|
205
|
-
// 'tool_use',
|
|
206
|
-
// 'server_tool_use',
|
|
207
|
-
// 'input_json_delta',
|
|
208
|
-
'tool_result',
|
|
209
|
-
'web_search_result',
|
|
210
|
-
'web_search_tool_result',
|
|
211
|
-
]);
|
|
212
|
-
/**
|
|
213
|
-
* Handles the result of a server tool call; in other words, a provider's built-in tool.
|
|
214
|
-
* As of 2025-07-06, only Anthropic handles server tool calls with this pattern.
|
|
215
|
-
*/
|
|
216
|
-
export async function handleServerToolResult({ graph, content, metadata, agentContext, }) {
|
|
217
|
-
let skipHandling = false;
|
|
218
|
-
if (agentContext?.provider !== Providers.ANTHROPIC) {
|
|
219
|
-
return skipHandling;
|
|
220
|
-
}
|
|
221
|
-
if (typeof content === 'string' ||
|
|
222
|
-
content == null ||
|
|
223
|
-
content.length === 0 ||
|
|
224
|
-
(content.length === 1 &&
|
|
225
|
-
content[0].tool_use_id == null)) {
|
|
226
|
-
return skipHandling;
|
|
227
|
-
}
|
|
228
|
-
for (const contentPart of content) {
|
|
229
|
-
const toolUseId = contentPart.tool_use_id;
|
|
230
|
-
if (toolUseId == null || toolUseId === '') {
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const stepId = graph.toolCallStepIds.get(toolUseId);
|
|
234
|
-
if (stepId == null || stepId === '') {
|
|
235
|
-
console.warn(`Tool use ID ${toolUseId} not found in graph, cannot dispatch tool result.`);
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
const runStep = graph.getRunStep(stepId);
|
|
239
|
-
if (!runStep) {
|
|
240
|
-
console.warn(`Run step for ${stepId} does not exist, cannot dispatch tool result.`);
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
else if (runStep.type !== StepTypes.TOOL_CALLS) {
|
|
244
|
-
console.warn(`Run step for ${stepId} is not a tool call step, cannot dispatch tool result.`);
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
const toolCall = runStep.stepDetails.type === StepTypes.TOOL_CALLS
|
|
248
|
-
? runStep.stepDetails.tool_calls?.find((toolCall) => toolCall.id === toolUseId)
|
|
249
|
-
: undefined;
|
|
250
|
-
if (!toolCall) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (contentPart.type === 'web_search_result' ||
|
|
254
|
-
contentPart.type === 'web_search_tool_result') {
|
|
255
|
-
await handleAnthropicSearchResults({
|
|
256
|
-
contentPart: contentPart,
|
|
257
|
-
toolCall,
|
|
258
|
-
metadata,
|
|
259
|
-
graph,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
if (!skipHandling) {
|
|
263
|
-
skipHandling = true;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return skipHandling;
|
|
267
|
-
}
|
|
268
|
-
async function handleAnthropicSearchResults({ contentPart, toolCall, metadata, graph, }) {
|
|
269
|
-
if (!Array.isArray(contentPart.content)) {
|
|
270
|
-
console.warn(`Expected content to be an array, got ${typeof contentPart.content}`);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (!isAnthropicWebSearchResult(contentPart.content[0])) {
|
|
274
|
-
console.warn(`Expected content to be an Anthropic web search result, got ${JSON.stringify(contentPart.content)}`);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const turn = graph.invokedToolIds?.size ?? 0;
|
|
278
|
-
const searchResultData = coerceAnthropicSearchResults({
|
|
279
|
-
turn,
|
|
280
|
-
results: contentPart.content,
|
|
281
|
-
});
|
|
282
|
-
const name = toolCall.name;
|
|
283
|
-
const input = toolCall.args ?? {};
|
|
284
|
-
const artifact = {
|
|
285
|
-
[Constants.WEB_SEARCH]: searchResultData,
|
|
286
|
-
};
|
|
287
|
-
const { output: formattedOutput } = formatResultsForLLM(turn, searchResultData);
|
|
288
|
-
const output = new ToolMessage({
|
|
289
|
-
name,
|
|
290
|
-
artifact,
|
|
291
|
-
content: formattedOutput,
|
|
292
|
-
tool_call_id: toolCall.id,
|
|
293
|
-
});
|
|
294
|
-
const toolEndData = {
|
|
295
|
-
input,
|
|
296
|
-
output,
|
|
297
|
-
};
|
|
298
|
-
await graph.handlerRegistry
|
|
299
|
-
?.getHandler(GraphEvents.TOOL_END)
|
|
300
|
-
?.handle(GraphEvents.TOOL_END, toolEndData, metadata, graph);
|
|
301
|
-
if (graph.invokedToolIds == null) {
|
|
302
|
-
graph.invokedToolIds = new Set();
|
|
303
|
-
}
|
|
304
|
-
graph.invokedToolIds.add(toolCall.id);
|
|
305
|
-
}
|
|
306
|
-
//# sourceMappingURL=handlers.js.map
|