@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
|
@@ -206,3 +206,194 @@ describe('handoff tool naming', () => {
|
|
|
206
206
|
expect(transferName).toBe('lc_transfer_to_test');
|
|
207
207
|
});
|
|
208
208
|
});
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// prepareHandoffMessages
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
describe('prepareHandoffMessages', () => {
|
|
214
|
+
it('returns empty array for empty input', () => {
|
|
215
|
+
expect(MultiAgentGraph.prepareHandoffMessages([])).toEqual([]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('passes through plain HumanMessage and text-only AIMessage unchanged', () => {
|
|
219
|
+
const messages: BaseMessage[] = [
|
|
220
|
+
new HumanMessage('Hello'),
|
|
221
|
+
new AIMessage('Hi there'),
|
|
222
|
+
];
|
|
223
|
+
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
224
|
+
expect(result).toHaveLength(2);
|
|
225
|
+
expect(result[0].getType()).toBe('human');
|
|
226
|
+
expect(result[0].content).toBe('Hello');
|
|
227
|
+
expect(result[1].getType()).toBe('ai');
|
|
228
|
+
expect(result[1].content).toBe('Hi there');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('strips orphaned tool_use blocks (no matching tool_result)', () => {
|
|
232
|
+
const messages: BaseMessage[] = [
|
|
233
|
+
new HumanMessage('Do something'),
|
|
234
|
+
new AIMessage({
|
|
235
|
+
content: 'Let me call a tool',
|
|
236
|
+
tool_calls: [
|
|
237
|
+
{ name: 'lc_handoff_to_researcher', args: { instructions: 'research this' }, id: 'tc1', type: 'tool_call' },
|
|
238
|
+
],
|
|
239
|
+
}),
|
|
240
|
+
// No ToolMessage for tc1 — it's orphaned
|
|
241
|
+
];
|
|
242
|
+
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
243
|
+
expect(result).toHaveLength(2);
|
|
244
|
+
expect(result[0].content).toBe('Do something');
|
|
245
|
+
// AI message should be converted to text-only (no tool_calls)
|
|
246
|
+
expect(result[1].getType()).toBe('ai');
|
|
247
|
+
expect(result[1].content).toContain('Let me call a tool');
|
|
248
|
+
// Should NOT contain tool_calls
|
|
249
|
+
const aiMsg = result[1] as AIMessage;
|
|
250
|
+
expect(aiMsg.tool_calls ?? []).toHaveLength(0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('compacts paired tool_use/tool_result into text summaries', () => {
|
|
254
|
+
const messages: BaseMessage[] = [
|
|
255
|
+
new HumanMessage('Search for data'),
|
|
256
|
+
new AIMessage({
|
|
257
|
+
content: 'I will search for you',
|
|
258
|
+
tool_calls: [
|
|
259
|
+
{ name: 'web_search', args: { query: 'test' }, id: 'tc1', type: 'tool_call' },
|
|
260
|
+
],
|
|
261
|
+
}),
|
|
262
|
+
new ToolMessage({ content: 'Found 3 results about testing', tool_call_id: 'tc1', name: 'web_search' }),
|
|
263
|
+
new AIMessage('Based on the search, here is what I found.'),
|
|
264
|
+
];
|
|
265
|
+
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
266
|
+
|
|
267
|
+
// Should have: HumanMessage, compacted AIMessage (with tool summary), final AIMessage
|
|
268
|
+
expect(result).toHaveLength(3);
|
|
269
|
+
|
|
270
|
+
// ToolMessage should be removed
|
|
271
|
+
expect(result.every((m) => m.getType() !== 'tool')).toBe(true);
|
|
272
|
+
|
|
273
|
+
// Compacted AI message should contain tool summary
|
|
274
|
+
const compactedAI = result[1];
|
|
275
|
+
expect(compactedAI.content).toContain('I will search for you');
|
|
276
|
+
expect(compactedAI.content).toContain('web_search');
|
|
277
|
+
expect(compactedAI.content).toContain('Found 3 results');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('removes ALL ToolMessages from output', () => {
|
|
281
|
+
const messages: BaseMessage[] = [
|
|
282
|
+
new HumanMessage('task'),
|
|
283
|
+
new AIMessage({
|
|
284
|
+
content: '',
|
|
285
|
+
tool_calls: [
|
|
286
|
+
{ name: 'tool_a', args: {}, id: 'tc1', type: 'tool_call' },
|
|
287
|
+
{ name: 'tool_b', args: {}, id: 'tc2', type: 'tool_call' },
|
|
288
|
+
],
|
|
289
|
+
}),
|
|
290
|
+
new ToolMessage({ content: 'result a', tool_call_id: 'tc1', name: 'tool_a' }),
|
|
291
|
+
new ToolMessage({ content: 'result b', tool_call_id: 'tc2', name: 'tool_b' }),
|
|
292
|
+
new AIMessage('Final answer.'),
|
|
293
|
+
];
|
|
294
|
+
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
295
|
+
|
|
296
|
+
// Zero ToolMessages in output
|
|
297
|
+
const toolMsgs = result.filter((m) => m.getType() === 'tool');
|
|
298
|
+
expect(toolMsgs).toHaveLength(0);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('handles multi-hop scenario: multiple tool interactions compacted', () => {
|
|
302
|
+
// Simulates: Orchestrator called researcher (handoff), got result,
|
|
303
|
+
// then called writer (handoff) — the writer should get clean messages
|
|
304
|
+
const messages: BaseMessage[] = [
|
|
305
|
+
new HumanMessage('Research and write a report'),
|
|
306
|
+
// Orchestrator's first tool call (handoff to researcher)
|
|
307
|
+
new AIMessage({
|
|
308
|
+
content: 'I will delegate to the researcher first.',
|
|
309
|
+
tool_calls: [
|
|
310
|
+
{ name: 'lc_handoff_to_researcher', args: { instructions: 'research AI trends' }, id: 'h1', type: 'tool_call' },
|
|
311
|
+
],
|
|
312
|
+
}),
|
|
313
|
+
new ToolMessage({ content: 'Research findings: AI adoption is growing rapidly...', tool_call_id: 'h1', name: 'lc_handoff_to_researcher' }),
|
|
314
|
+
// Orchestrator's response after getting research back
|
|
315
|
+
new AIMessage({
|
|
316
|
+
content: 'Now I will delegate to the writer.',
|
|
317
|
+
tool_calls: [
|
|
318
|
+
{ name: 'lc_handoff_to_writer', args: { instructions: 'write report based on findings' }, id: 'h2', type: 'tool_call' },
|
|
319
|
+
],
|
|
320
|
+
}),
|
|
321
|
+
// h2 is orphaned — writer hasn't returned yet (this is the current handoff)
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
325
|
+
|
|
326
|
+
// No ToolMessages
|
|
327
|
+
expect(result.filter((m) => m.getType() === 'tool')).toHaveLength(0);
|
|
328
|
+
|
|
329
|
+
// No tool_calls on any AI message
|
|
330
|
+
for (const msg of result) {
|
|
331
|
+
if (msg.getType() === 'ai') {
|
|
332
|
+
expect((msg as AIMessage).tool_calls ?? []).toHaveLength(0);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Should have: HumanMessage + 2 compacted/cleaned AI messages
|
|
337
|
+
expect(result).toHaveLength(3);
|
|
338
|
+
|
|
339
|
+
// First AI message should have researcher tool summary (paired)
|
|
340
|
+
expect(result[1].content).toContain('lc_handoff_to_researcher');
|
|
341
|
+
expect(result[1].content).toContain('AI adoption');
|
|
342
|
+
|
|
343
|
+
// Second AI message should be text-only (orphaned tool_use stripped)
|
|
344
|
+
expect(result[2].content).toContain('Now I will delegate to the writer');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('preserves message order', () => {
|
|
348
|
+
const messages: BaseMessage[] = [
|
|
349
|
+
new HumanMessage('Step 1'),
|
|
350
|
+
new AIMessage('Response 1'),
|
|
351
|
+
new HumanMessage('Step 2'),
|
|
352
|
+
new AIMessage('Response 2'),
|
|
353
|
+
];
|
|
354
|
+
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
355
|
+
expect(result).toHaveLength(4);
|
|
356
|
+
expect(result[0].content).toBe('Step 1');
|
|
357
|
+
expect(result[1].content).toBe('Response 1');
|
|
358
|
+
expect(result[2].content).toBe('Step 2');
|
|
359
|
+
expect(result[3].content).toBe('Response 2');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('drops AI messages that have only tool_calls and no text content', () => {
|
|
363
|
+
const messages: BaseMessage[] = [
|
|
364
|
+
new HumanMessage('Do it'),
|
|
365
|
+
new AIMessage({
|
|
366
|
+
content: '',
|
|
367
|
+
tool_calls: [
|
|
368
|
+
{ name: 'orphan_tool', args: {}, id: 'orp1', type: 'tool_call' },
|
|
369
|
+
],
|
|
370
|
+
}),
|
|
371
|
+
];
|
|
372
|
+
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
373
|
+
// AI message with empty content + orphaned tool = nothing to keep
|
|
374
|
+
expect(result).toHaveLength(1);
|
|
375
|
+
expect(result[0].content).toBe('Do it');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('handles AI messages with array content (text blocks)', () => {
|
|
379
|
+
const messages: BaseMessage[] = [
|
|
380
|
+
new HumanMessage('task'),
|
|
381
|
+
new AIMessage({
|
|
382
|
+
content: [
|
|
383
|
+
{ type: 'text', text: 'Part one.' },
|
|
384
|
+
{ type: 'text', text: 'Part two.' },
|
|
385
|
+
],
|
|
386
|
+
tool_calls: [
|
|
387
|
+
{ name: 'some_tool', args: {}, id: 'tc1', type: 'tool_call' },
|
|
388
|
+
],
|
|
389
|
+
}),
|
|
390
|
+
new ToolMessage({ content: 'tool output', tool_call_id: 'tc1', name: 'some_tool' }),
|
|
391
|
+
];
|
|
392
|
+
const result = MultiAgentGraph.prepareHandoffMessages(messages);
|
|
393
|
+
expect(result).toHaveLength(2);
|
|
394
|
+
const aiContent = result[1].content as string;
|
|
395
|
+
expect(aiContent).toContain('Part one.');
|
|
396
|
+
expect(aiContent).toContain('Part two.');
|
|
397
|
+
expect(aiContent).toContain('some_tool');
|
|
398
|
+
});
|
|
399
|
+
});
|
package/src/llm/bedrock/index.ts
CHANGED
|
@@ -167,6 +167,23 @@ export class IllumaBedrockConverse extends ChatBedrockConverse {
|
|
|
167
167
|
const params = super.invocationParams(options);
|
|
168
168
|
|
|
169
169
|
// Add cachePoint to tools array if promptCache is enabled and tools exist
|
|
170
|
+
/**
|
|
171
|
+
* Bedrock requires all toolSpec.description fields to be non-empty strings.
|
|
172
|
+
* Some tools (e.g., MCP-sourced or dynamically created) may have empty or
|
|
173
|
+
* missing descriptions. Patch them here to avoid Bedrock validation errors.
|
|
174
|
+
*/
|
|
175
|
+
if (
|
|
176
|
+
params.toolConfig?.tools &&
|
|
177
|
+
Array.isArray(params.toolConfig.tools)
|
|
178
|
+
) {
|
|
179
|
+
for (const t of params.toolConfig.tools) {
|
|
180
|
+
const spec = (t as { toolSpec?: { description?: string } }).toolSpec;
|
|
181
|
+
if (spec && (!spec.description || spec.description === '')) {
|
|
182
|
+
spec.description = spec.description || `Tool: ${(spec as { name?: string }).name ?? 'unknown'}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
170
187
|
// Only Claude models support cachePoint - check model name
|
|
171
188
|
const modelId = this.model.toLowerCase();
|
|
172
189
|
const isClaudeModel =
|
package/src/run.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/run.ts
|
|
2
2
|
import './instrumentation';
|
|
3
3
|
import { ObservabilityCallbackHandler } from '@illuma-ai/observability-langchain';
|
|
4
|
-
import { Command } from '@langchain/langgraph';
|
|
4
|
+
import { Command, isGraphInterrupt } from '@langchain/langgraph';
|
|
5
5
|
import { PromptTemplate } from '@langchain/core/prompts';
|
|
6
6
|
import { RunnableLambda } from '@langchain/core/runnables';
|
|
7
7
|
import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
|
|
@@ -349,20 +349,29 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
349
349
|
ignoreCustomEvent: true,
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
-
|
|
353
|
-
|
|
352
|
+
try {
|
|
353
|
+
for await (const event of stream) {
|
|
354
|
+
const { data, metadata, ...info } = event;
|
|
354
355
|
|
|
355
|
-
|
|
356
|
+
const eventName: t.EventName = info.event;
|
|
356
357
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
358
|
+
/** Skip custom events as they're handled by our callback */
|
|
359
|
+
if (eventName === GraphEvents.ON_CUSTOM_EVENT) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
361
362
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
const handler = this.handlerRegistry?.getHandler(eventName);
|
|
364
|
+
if (handler) {
|
|
365
|
+
await handler.handle(eventName, data, metadata, this.Graph);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (e) {
|
|
369
|
+
// GraphInterrupt is expected when interrupt() fires (HITL approval flow).
|
|
370
|
+
// Exit gracefully so the host can check hasInterrupts() and resume.
|
|
371
|
+
if (isGraphInterrupt(e)) {
|
|
372
|
+
return undefined;
|
|
365
373
|
}
|
|
374
|
+
throw e;
|
|
366
375
|
}
|
|
367
376
|
|
|
368
377
|
/**
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: Autonomous Handoff with Bedrock
|
|
3
|
+
*
|
|
4
|
+
* Tests the full autonomous handoff pattern where:
|
|
5
|
+
* 1. Orchestrator receives user request
|
|
6
|
+
* 2. Orchestrator delegates to specialist via handoff (subgraph.invoke)
|
|
7
|
+
* 3. Specialist completes work and returns result to orchestrator
|
|
8
|
+
* 4. Orchestrator synthesizes and responds
|
|
9
|
+
*
|
|
10
|
+
* This validates:
|
|
11
|
+
* - Handoff-only children don't cause UNREACHABLE_NODE errors
|
|
12
|
+
* - stripOrphanedToolUse prevents Bedrock tool_use/tool_result mismatch
|
|
13
|
+
* - Empty tool descriptions are sanitized for Bedrock
|
|
14
|
+
* - Orchestrator receives child result and produces final response
|
|
15
|
+
*
|
|
16
|
+
* Requires: BEDROCK_AWS_ACCESS_KEY_ID, BEDROCK_AWS_SECRET_ACCESS_KEY, BEDROCK_AWS_REGION
|
|
17
|
+
*/
|
|
18
|
+
import { config } from 'dotenv';
|
|
19
|
+
config();
|
|
20
|
+
import { resolve } from 'path';
|
|
21
|
+
config({ path: resolve(process.cwd(), '..', 'ranger', '.env'), override: false });
|
|
22
|
+
|
|
23
|
+
// Disable observability for this test (requires @illuma-ai/observability-node)
|
|
24
|
+
delete process.env.ILLUMA_SECRET_KEY;
|
|
25
|
+
delete process.env.ILLUMA_PUBLIC_KEY;
|
|
26
|
+
delete process.env.ILLUMA_BASE_URL;
|
|
27
|
+
|
|
28
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
29
|
+
import { Run } from '@/run';
|
|
30
|
+
import { Providers, GraphEvents } from '@/common';
|
|
31
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
32
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
33
|
+
import type * as t from '@/types';
|
|
34
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
35
|
+
|
|
36
|
+
const bedrockRegion =
|
|
37
|
+
process.env.BEDROCK_AWS_REGION ?? process.env.BEDROCK_AWS_DEFAULT_REGION ?? 'us-east-1';
|
|
38
|
+
|
|
39
|
+
const bedrockOptions = {
|
|
40
|
+
model: 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
|
|
41
|
+
region: bedrockRegion,
|
|
42
|
+
credentials: {
|
|
43
|
+
accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!,
|
|
44
|
+
secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function createBedrockAgent(
|
|
49
|
+
agentId: string,
|
|
50
|
+
name: string,
|
|
51
|
+
description: string,
|
|
52
|
+
instructions: string
|
|
53
|
+
): t.AgentInputs {
|
|
54
|
+
return {
|
|
55
|
+
agentId,
|
|
56
|
+
name,
|
|
57
|
+
description,
|
|
58
|
+
provider: Providers.BEDROCK,
|
|
59
|
+
clientOptions: bedrockOptions,
|
|
60
|
+
instructions,
|
|
61
|
+
maxContextTokens: 16000,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function testAutonomousHandoff() {
|
|
66
|
+
console.log('=== Autonomous Handoff with Bedrock ===\n');
|
|
67
|
+
|
|
68
|
+
const agents: t.AgentInputs[] = [
|
|
69
|
+
createBedrockAgent(
|
|
70
|
+
'orchestrator',
|
|
71
|
+
'Orchestrator',
|
|
72
|
+
'Routes complex requests to specialist agents and synthesizes results',
|
|
73
|
+
`You are an autonomous orchestrator. Break down user requests and delegate to specialists using your handoff tools.
|
|
74
|
+
|
|
75
|
+
IMPORTANT: You MUST use your handoff tools to delegate work. Do NOT answer questions yourself.
|
|
76
|
+
- Use lc_handoff_to_researcher for any information gathering or research tasks
|
|
77
|
+
- Use lc_handoff_to_writer for any content creation tasks
|
|
78
|
+
|
|
79
|
+
After receiving results from a specialist, synthesize and present the final answer to the user.`
|
|
80
|
+
),
|
|
81
|
+
createBedrockAgent(
|
|
82
|
+
'researcher',
|
|
83
|
+
'Researcher',
|
|
84
|
+
'Finds and analyzes information on any topic',
|
|
85
|
+
`You are a research specialist. When you receive a request:
|
|
86
|
+
1. Analyze what information is needed
|
|
87
|
+
2. Provide a structured, factual response with key findings
|
|
88
|
+
3. Be concise but thorough — aim for 3-5 key points
|
|
89
|
+
|
|
90
|
+
Do NOT delegate or transfer. Just do the research and respond.`
|
|
91
|
+
),
|
|
92
|
+
createBedrockAgent(
|
|
93
|
+
'writer',
|
|
94
|
+
'Writer',
|
|
95
|
+
'Creates professional written content',
|
|
96
|
+
`You are a professional writer. When you receive a request:
|
|
97
|
+
1. Create well-structured, clear content
|
|
98
|
+
2. Use appropriate tone and format
|
|
99
|
+
3. Be concise
|
|
100
|
+
|
|
101
|
+
Do NOT delegate or transfer. Just write the content and respond.`
|
|
102
|
+
),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const edges: t.GraphEdge[] = [
|
|
106
|
+
{ from: 'orchestrator', to: 'researcher', edgeType: 'handoff' },
|
|
107
|
+
{ from: 'orchestrator', to: 'writer', edgeType: 'handoff' },
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// Set up content aggregator and handlers
|
|
111
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
112
|
+
|
|
113
|
+
const customHandlers = {
|
|
114
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
115
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
116
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
117
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
118
|
+
handle: (event: string, data: t.StreamEventData): void => {
|
|
119
|
+
aggregateContent({ event: event as GraphEvents, data: data as t.RunStep });
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
123
|
+
handle: (event: string, data: t.StreamEventData): void => {
|
|
124
|
+
aggregateContent({
|
|
125
|
+
event: event as GraphEvents,
|
|
126
|
+
data: data as unknown as { result: t.ToolEndEvent },
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
131
|
+
handle: (event: string, data: t.StreamEventData): void => {
|
|
132
|
+
aggregateContent({ event: event as GraphEvents, data: data as t.MessageDeltaEvent });
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const run = await Run.create({
|
|
138
|
+
runId: `bedrock-autonomous-handoff-${Date.now()}`,
|
|
139
|
+
graphConfig: {
|
|
140
|
+
type: 'multi-agent',
|
|
141
|
+
agents,
|
|
142
|
+
edges,
|
|
143
|
+
},
|
|
144
|
+
customHandlers,
|
|
145
|
+
returnContent: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const streamConfig: Partial<RunnableConfig> & { version: 'v1' | 'v2'; streamMode: string } = {
|
|
149
|
+
configurable: { thread_id: 'bedrock-autonomous-test' },
|
|
150
|
+
streamMode: 'values',
|
|
151
|
+
version: 'v2',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ── Test 1: Single handoff (Orchestrator → Researcher → back) ──
|
|
155
|
+
console.log('\n--- Test 1: Single Handoff (Research) ---');
|
|
156
|
+
console.log('Query: "What are the top 3 trends in enterprise AI adoption in 2025?"');
|
|
157
|
+
|
|
158
|
+
const messages1 = [
|
|
159
|
+
new HumanMessage('What are the top 3 trends in enterprise AI adoption in 2025?'),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await run.processStream({ messages: messages1 }, streamConfig);
|
|
164
|
+
|
|
165
|
+
const finalMessages = run.getRunMessages();
|
|
166
|
+
console.log(`\n✓ Graph completed. Total messages: ${finalMessages?.length ?? 0}`);
|
|
167
|
+
|
|
168
|
+
// Check that a handoff happened
|
|
169
|
+
const lastMsg = finalMessages?.[finalMessages.length - 1];
|
|
170
|
+
if (lastMsg) {
|
|
171
|
+
const content = typeof lastMsg.content === 'string'
|
|
172
|
+
? lastMsg.content
|
|
173
|
+
: JSON.stringify(lastMsg.content);
|
|
174
|
+
console.log(`\n✓ Final response (first 500 chars):\n${content.slice(0, 500)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Verify last active agent tracking
|
|
178
|
+
const lastActiveAgentId = run.getLastActiveAgentId?.();
|
|
179
|
+
console.log(`\n✓ lastActiveAgentId: ${lastActiveAgentId}`);
|
|
180
|
+
|
|
181
|
+
console.log('\n✅ Test 1 PASSED — Single handoff completed successfully');
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('\n❌ Test 1 FAILED:', (err as Error).message);
|
|
184
|
+
console.error((err as Error).stack);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Test 2: Multi-hop (Orchestrator → Researcher → back → Writer → back) ──
|
|
188
|
+
console.log('\n\n--- Test 2: Multi-Hop Handoff (Research → Write) ---');
|
|
189
|
+
console.log('Query: "Research AI agent frameworks and write a brief summary email"');
|
|
190
|
+
|
|
191
|
+
const run2 = await Run.create({
|
|
192
|
+
runId: `bedrock-autonomous-handoff-2-${Date.now()}`,
|
|
193
|
+
graphConfig: {
|
|
194
|
+
type: 'multi-agent',
|
|
195
|
+
agents,
|
|
196
|
+
edges,
|
|
197
|
+
},
|
|
198
|
+
returnContent: true,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const messages2 = [
|
|
202
|
+
new HumanMessage('Research the latest AI agent frameworks and write a brief 3-sentence summary.'),
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await run2.processStream({ messages: messages2 }, streamConfig);
|
|
207
|
+
|
|
208
|
+
const finalMessages2 = run2.getRunMessages();
|
|
209
|
+
console.log(`\n✓ Graph completed. Total messages: ${finalMessages2?.length ?? 0}`);
|
|
210
|
+
|
|
211
|
+
const lastMsg2 = finalMessages2?.[finalMessages2.length - 1];
|
|
212
|
+
if (lastMsg2) {
|
|
213
|
+
const content = typeof lastMsg2.content === 'string'
|
|
214
|
+
? lastMsg2.content
|
|
215
|
+
: JSON.stringify(lastMsg2.content);
|
|
216
|
+
console.log(`\n✓ Final response (first 500 chars):\n${content.slice(0, 500)}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const lastActiveAgentId2 = run2.getLastActiveAgentId?.();
|
|
220
|
+
console.log(`\n✓ lastActiveAgentId: ${lastActiveAgentId2}`);
|
|
221
|
+
|
|
222
|
+
console.log('\n✅ Test 2 PASSED — Multi-hop handoff completed successfully');
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error('\n❌ Test 2 FAILED:', (err as Error).message);
|
|
225
|
+
console.error((err as Error).stack);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log('\n\n=== All Tests Complete ===');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
testAutonomousHandoff().catch(console.error);
|