@illuma-ai/agents 1.1.25 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/agents/AgentContext.cjs +20 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/spawnPath.cjs +104 -0
- package/dist/cjs/common/spawnPath.cjs.map +1 -0
- package/dist/cjs/graphs/Graph.cjs +87 -31
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
- package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
- package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/index.cjs +4 -3
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +115 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/memory/citations.cjs +69 -0
- package/dist/cjs/memory/citations.cjs.map +1 -0
- package/dist/cjs/memory/compositeBackend.cjs +60 -0
- package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
- package/dist/cjs/memory/constants.cjs +232 -0
- package/dist/cjs/memory/constants.cjs.map +1 -0
- package/dist/cjs/memory/embeddings.cjs +151 -0
- package/dist/cjs/memory/embeddings.cjs.map +1 -0
- package/dist/cjs/memory/factory.cjs +95 -0
- package/dist/cjs/memory/factory.cjs.map +1 -0
- package/dist/cjs/memory/migrate.cjs +81 -0
- package/dist/cjs/memory/migrate.cjs.map +1 -0
- package/dist/cjs/memory/mmr.cjs +138 -0
- package/dist/cjs/memory/mmr.cjs.map +1 -0
- package/dist/cjs/memory/paths.cjs +217 -0
- package/dist/cjs/memory/paths.cjs.map +1 -0
- package/dist/cjs/memory/pgvectorStore.cjs +225 -0
- package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
- package/dist/cjs/memory/recallTracking.cjs +98 -0
- package/dist/cjs/memory/recallTracking.cjs.map +1 -0
- package/dist/cjs/memory/schema.sql +51 -0
- package/dist/cjs/memory/temporalDecay.cjs +118 -0
- package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
- package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
- package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
- package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
- package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
- package/dist/cjs/run.cjs +16 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +4 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/AskUser.cjs +6 -1
- package/dist/cjs/tools/AskUser.cjs.map +1 -1
- package/dist/cjs/tools/BrowserTools.cjs +1 -1
- package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +127 -10
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/approval/constants.cjs +2 -2
- package/dist/cjs/tools/approval/constants.cjs.map +1 -1
- package/dist/cjs/tools/memory/index.cjs +58 -0
- package/dist/cjs/tools/memory/index.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/shared.cjs +106 -0
- package/dist/cjs/tools/memory/shared.cjs.map +1 -0
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/childAgentContext.cjs +242 -0
- package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +36 -4
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/finishReasons.cjs +44 -0
- package/dist/cjs/utils/finishReasons.cjs.map +1 -0
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/logging.cjs +34 -0
- package/dist/cjs/utils/logging.cjs.map +1 -0
- package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
- package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +20 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/spawnPath.mjs +95 -0
- package/dist/esm/common/spawnPath.mjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +87 -31
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
- package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
- package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
- package/dist/esm/llm/bedrock/index.mjs +4 -3
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +21 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/memory/citations.mjs +64 -0
- package/dist/esm/memory/citations.mjs.map +1 -0
- package/dist/esm/memory/compositeBackend.mjs +58 -0
- package/dist/esm/memory/compositeBackend.mjs.map +1 -0
- package/dist/esm/memory/constants.mjs +198 -0
- package/dist/esm/memory/constants.mjs.map +1 -0
- package/dist/esm/memory/embeddings.mjs +148 -0
- package/dist/esm/memory/embeddings.mjs.map +1 -0
- package/dist/esm/memory/factory.mjs +93 -0
- package/dist/esm/memory/factory.mjs.map +1 -0
- package/dist/esm/memory/migrate.mjs +78 -0
- package/dist/esm/memory/migrate.mjs.map +1 -0
- package/dist/esm/memory/mmr.mjs +130 -0
- package/dist/esm/memory/mmr.mjs.map +1 -0
- package/dist/esm/memory/paths.mjs +207 -0
- package/dist/esm/memory/paths.mjs.map +1 -0
- package/dist/esm/memory/pgvectorStore.mjs +223 -0
- package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
- package/dist/esm/memory/recallTracking.mjs +94 -0
- package/dist/esm/memory/recallTracking.mjs.map +1 -0
- package/dist/esm/memory/schema.sql +51 -0
- package/dist/esm/memory/temporalDecay.mjs +110 -0
- package/dist/esm/memory/temporalDecay.mjs.map +1 -0
- package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
- package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
- package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
- package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
- package/dist/esm/run.mjs +16 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +4 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/AskUser.mjs +6 -1
- package/dist/esm/tools/AskUser.mjs.map +1 -1
- package/dist/esm/tools/BrowserTools.mjs +1 -1
- package/dist/esm/tools/BrowserTools.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +128 -11
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/approval/constants.mjs +2 -2
- package/dist/esm/tools/approval/constants.mjs.map +1 -1
- package/dist/esm/tools/memory/index.mjs +46 -0
- package/dist/esm/tools/memory/index.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
- package/dist/esm/tools/memory/shared.mjs +98 -0
- package/dist/esm/tools/memory/shared.mjs.map +1 -0
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/childAgentContext.mjs +237 -0
- package/dist/esm/utils/childAgentContext.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +36 -5
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/finishReasons.mjs +41 -0
- package/dist/esm/utils/finishReasons.mjs.map +1 -0
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/logging.mjs +31 -0
- package/dist/esm/utils/logging.mjs.map +1 -0
- package/dist/esm/utils/toolCallNormalization.mjs +247 -0
- package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
- package/dist/types/common/index.d.ts +1 -0
- package/dist/types/common/spawnPath.d.ts +59 -0
- package/dist/types/graphs/HandoffRegistry.d.ts +97 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
- package/dist/types/graphs/index.d.ts +1 -0
- package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
- package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
- package/dist/types/memory/citations.d.ts +39 -0
- package/dist/types/memory/compositeBackend.d.ts +30 -0
- package/dist/types/memory/constants.d.ts +121 -0
- package/dist/types/memory/embeddings.d.ts +15 -0
- package/dist/types/memory/factory.d.ts +23 -0
- package/dist/types/memory/index.d.ts +21 -0
- package/dist/types/memory/migrate.d.ts +14 -0
- package/dist/types/memory/mmr.d.ts +50 -0
- package/dist/types/memory/paths.d.ts +107 -0
- package/dist/types/memory/pgvectorStore.d.ts +56 -0
- package/dist/types/memory/recallTracking.d.ts +30 -0
- package/dist/types/memory/temporalDecay.d.ts +53 -0
- package/dist/types/memory/types.d.ts +182 -0
- package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/AskUser.d.ts +1 -1
- package/dist/types/tools/BrowserTools.d.ts +2 -2
- package/dist/types/tools/approval/constants.d.ts +2 -2
- package/dist/types/tools/memory/index.d.ts +39 -0
- package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
- package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
- package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
- package/dist/types/tools/memory/shared.d.ts +106 -0
- package/dist/types/types/graph.d.ts +16 -3
- package/dist/types/utils/childAgentContext.d.ts +99 -0
- package/dist/types/utils/events.d.ts +21 -0
- package/dist/types/utils/finishReasons.d.ts +32 -0
- package/dist/types/utils/logging.d.ts +2 -0
- package/dist/types/utils/toolCallNormalization.d.ts +44 -0
- package/package.json +6 -4
- package/src/agents/AgentContext.ts +26 -3
- package/src/common/__tests__/enum.test.ts +4 -2
- package/src/common/__tests__/spawnPath.test.ts +110 -0
- package/src/common/index.ts +1 -0
- package/src/common/spawnPath.ts +101 -0
- package/src/graphs/Graph.ts +94 -43
- package/src/graphs/HandoffRegistry.ts +199 -0
- package/src/graphs/MultiAgentGraph.ts +694 -226
- package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
- package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
- package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
- package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
- package/src/graphs/contextManagement.e2e.test.ts +1 -1
- package/src/graphs/index.ts +1 -0
- package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
- package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
- package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
- package/src/graphs/phases/flushLoop.ts +303 -0
- package/src/graphs/phases/memoryFlushPhase.ts +209 -0
- package/src/index.ts +30 -1
- package/src/llm/bedrock/index.ts +4 -5
- package/src/memory/__tests__/citations.test.ts +61 -0
- package/src/memory/__tests__/compositeBackend.test.ts +79 -0
- package/src/memory/__tests__/isolation.test.ts +206 -0
- package/src/memory/__tests__/mmr.test.ts +148 -0
- package/src/memory/__tests__/mockBackend.ts +161 -0
- package/src/memory/__tests__/paths.test.ts +168 -0
- package/src/memory/__tests__/recallTracking.test.ts +96 -0
- package/src/memory/__tests__/temporalDecay.test.ts +151 -0
- package/src/memory/citations.ts +80 -0
- package/src/memory/compositeBackend.ts +99 -0
- package/src/memory/constants.ts +229 -0
- package/src/memory/embeddings.ts +188 -0
- package/src/memory/factory.ts +111 -0
- package/src/memory/index.ts +46 -0
- package/src/memory/migrate.ts +116 -0
- package/src/memory/mmr.ts +161 -0
- package/src/memory/paths.ts +258 -0
- package/src/memory/pgvectorStore.ts +324 -0
- package/src/memory/recallTracking.ts +127 -0
- package/src/memory/schema.sql +51 -0
- package/src/memory/temporalDecay.ts +134 -0
- package/src/memory/types.ts +185 -0
- package/src/nodes/ApprovalGateNode.ts +4 -10
- package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
- package/src/prompts/memoryFlushPrompt.ts +78 -0
- package/src/run.ts +17 -6
- package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
- package/src/specs/agent-handoffs.test.ts +8 -2
- package/src/stream.ts +4 -6
- package/src/tools/AskUser.ts +7 -2
- package/src/tools/BrowserTools.ts +3 -5
- package/src/tools/ToolNode.ts +150 -13
- package/src/tools/__tests__/ToolApproval.test.ts +22 -9
- package/src/tools/approval/__tests__/constants.test.ts +4 -4
- package/src/tools/approval/constants.ts +2 -2
- package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
- package/src/tools/memory/index.ts +96 -0
- package/src/tools/memory/memoryAppendTool.ts +101 -0
- package/src/tools/memory/memoryGetTool.ts +53 -0
- package/src/tools/memory/memorySearchTool.ts +80 -0
- package/src/tools/memory/shared.ts +169 -0
- package/src/tools/search/search.test.ts +6 -1
- package/src/types/graph.ts +16 -3
- package/src/utils/__tests__/childAgentContext.test.ts +217 -0
- package/src/utils/__tests__/finishReasons.test.ts +55 -0
- package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
- package/src/utils/childAgentContext.ts +259 -0
- package/src/utils/events.ts +37 -4
- package/src/utils/finishReasons.ts +40 -0
- package/src/utils/llm.ts +0 -1
- package/src/utils/logging.ts +45 -8
- package/src/utils/toolCallNormalization.ts +271 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for child-agent context preparation.
|
|
3
|
+
*
|
|
4
|
+
* These strategies are the primary defense against schema confusion and
|
|
5
|
+
* Bedrock/VertexAI message-shape rejections, so their behavior must be
|
|
6
|
+
* locked down with fixtures that mirror real provider output shapes.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
AIMessage,
|
|
10
|
+
HumanMessage,
|
|
11
|
+
SystemMessage,
|
|
12
|
+
ToolMessage,
|
|
13
|
+
} from '@langchain/core/messages';
|
|
14
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
15
|
+
import {
|
|
16
|
+
HANDOFF_TAIL_CONTEXT_PREFIX,
|
|
17
|
+
buildIsolatedChildPrompt,
|
|
18
|
+
prepareHandoffMessages,
|
|
19
|
+
prepareIsolatedChildMessages,
|
|
20
|
+
} from '../childAgentContext';
|
|
21
|
+
|
|
22
|
+
describe('buildIsolatedChildPrompt', () => {
|
|
23
|
+
it('wraps upstream text in the prior-step / your-task sections', () => {
|
|
24
|
+
const out = buildIsolatedChildPrompt('RESEARCH FINDINGS');
|
|
25
|
+
expect(out).toContain('## Prior step output');
|
|
26
|
+
expect(out).toContain('RESEARCH FINDINGS');
|
|
27
|
+
expect(out).toContain('## Your task');
|
|
28
|
+
expect(out).toMatch(/You MUST now/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('instructs the model to call tools directly without asking for clarification', () => {
|
|
32
|
+
const out = buildIsolatedChildPrompt('x');
|
|
33
|
+
expect(out).toMatch(/call it directly/);
|
|
34
|
+
expect(out).toMatch(/do not ask for clarification/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('prepareHandoffMessages', () => {
|
|
39
|
+
it('returns an empty array unchanged', () => {
|
|
40
|
+
expect(prepareHandoffMessages([])).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('preserves system and human messages verbatim', () => {
|
|
44
|
+
const msgs: BaseMessage[] = [
|
|
45
|
+
new SystemMessage('sys'),
|
|
46
|
+
new HumanMessage('hello'),
|
|
47
|
+
];
|
|
48
|
+
const out = prepareHandoffMessages(msgs);
|
|
49
|
+
expect(out).toHaveLength(2);
|
|
50
|
+
expect(out[0].getType()).toBe('system');
|
|
51
|
+
expect(out[1].getType()).toBe('human');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('drops orphaned tool_use blocks (handoff tool has no matching result)', () => {
|
|
55
|
+
const ai = new AIMessage({
|
|
56
|
+
content: 'transferring',
|
|
57
|
+
tool_calls: [{ name: 'handoff_to_writer', args: {}, id: 'call_1' }],
|
|
58
|
+
});
|
|
59
|
+
const out = prepareHandoffMessages([new HumanMessage('go'), ai]);
|
|
60
|
+
// The assistant tail becomes a HumanMessage with the context prefix
|
|
61
|
+
// because the orphaned tool_use was stripped, leaving only text.
|
|
62
|
+
expect(out).toHaveLength(2);
|
|
63
|
+
expect(out[1].getType()).toBe('human');
|
|
64
|
+
expect(out[1].content).toContain('transferring');
|
|
65
|
+
expect(out[1].content).toContain(HANDOFF_TAIL_CONTEXT_PREFIX);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('compacts paired tool_use/tool_result into a text summary', () => {
|
|
69
|
+
const ai = new AIMessage({
|
|
70
|
+
content: '',
|
|
71
|
+
tool_calls: [{ name: 'search', args: { q: 'x' }, id: 'call_a' }],
|
|
72
|
+
});
|
|
73
|
+
const toolResult = new ToolMessage({
|
|
74
|
+
content: 'hit: 42',
|
|
75
|
+
tool_call_id: 'call_a',
|
|
76
|
+
});
|
|
77
|
+
const out = prepareHandoffMessages([
|
|
78
|
+
new HumanMessage('find x'),
|
|
79
|
+
ai,
|
|
80
|
+
toolResult,
|
|
81
|
+
new HumanMessage('what did you find?'),
|
|
82
|
+
]);
|
|
83
|
+
// The paired tool_use+result should have become a text summary on the
|
|
84
|
+
// rewritten AI message; the original ToolMessage should be gone.
|
|
85
|
+
expect(out.some((m) => m.getType() === 'tool')).toBe(false);
|
|
86
|
+
const rewrittenAI = out.find((m) => m.getType() === 'ai');
|
|
87
|
+
expect(rewrittenAI).toBeDefined();
|
|
88
|
+
expect(String(rewrittenAI?.content)).toContain('[Tool "search"');
|
|
89
|
+
expect(String(rewrittenAI?.content)).toContain('hit: 42');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('truncates long tool_result content at 500 chars in the summary', () => {
|
|
93
|
+
const long = 'x'.repeat(1200);
|
|
94
|
+
const ai = new AIMessage({
|
|
95
|
+
content: '',
|
|
96
|
+
tool_calls: [{ name: 'dump', args: {}, id: 'call_big' }],
|
|
97
|
+
});
|
|
98
|
+
const tool = new ToolMessage({ content: long, tool_call_id: 'call_big' });
|
|
99
|
+
const out = prepareHandoffMessages([new HumanMessage('hi'), ai, tool]);
|
|
100
|
+
// After the paired compaction the tail is an AIMessage with no text, so
|
|
101
|
+
// it gets dropped (empty) — find the one that carries the summary by
|
|
102
|
+
// scanning for the "[Tool" marker in any message.
|
|
103
|
+
const blob = out.map((m) => String(m.content)).join(' ');
|
|
104
|
+
expect(blob).toContain('[Tool "dump"');
|
|
105
|
+
// The truncated slice is 500 chars of x, not the full 1200.
|
|
106
|
+
const match = blob.match(/x+/);
|
|
107
|
+
expect(match).toBeTruthy();
|
|
108
|
+
expect((match?.[0].length ?? 0) <= 500).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('ensures the tail message is a HumanMessage by rewriting a trailing AI', () => {
|
|
112
|
+
const msgs: BaseMessage[] = [
|
|
113
|
+
new HumanMessage('question'),
|
|
114
|
+
new AIMessage('plain text answer'),
|
|
115
|
+
];
|
|
116
|
+
const out = prepareHandoffMessages(msgs);
|
|
117
|
+
expect(out[out.length - 1].getType()).toBe('human');
|
|
118
|
+
expect(out[out.length - 1].content).toBe(
|
|
119
|
+
`${HANDOFF_TAIL_CONTEXT_PREFIX}plain text answer`
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('drops a trailing empty AIMessage instead of creating an empty human', () => {
|
|
124
|
+
const msgs: BaseMessage[] = [
|
|
125
|
+
new HumanMessage('q'),
|
|
126
|
+
new AIMessage(''), // empty, nothing to carry forward
|
|
127
|
+
];
|
|
128
|
+
const out = prepareHandoffMessages(msgs);
|
|
129
|
+
expect(out).toHaveLength(1);
|
|
130
|
+
expect(out[0].getType()).toBe('human');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('prepareIsolatedChildMessages', () => {
|
|
135
|
+
it('returns empty input unchanged', () => {
|
|
136
|
+
expect(prepareIsolatedChildMessages([])).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('keeps only the first human message and a synthetic directive', () => {
|
|
140
|
+
const msgs: BaseMessage[] = [
|
|
141
|
+
new HumanMessage('original request'),
|
|
142
|
+
new AIMessage('upstream produced this result'),
|
|
143
|
+
];
|
|
144
|
+
const out = prepareIsolatedChildMessages(msgs);
|
|
145
|
+
expect(out).toHaveLength(2);
|
|
146
|
+
expect(out[0].getType()).toBe('human');
|
|
147
|
+
expect(out[0].content).toBe('original request');
|
|
148
|
+
expect(out[1].getType()).toBe('human');
|
|
149
|
+
expect(String(out[1].content)).toContain('upstream produced this result');
|
|
150
|
+
expect(String(out[1].content)).toContain('## Your task');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('discards upstream tool_use/tool_result blocks entirely', () => {
|
|
154
|
+
const aiWithTool = new AIMessage({
|
|
155
|
+
content: 'working',
|
|
156
|
+
tool_calls: [{ name: 'search', args: {}, id: 'c1' }],
|
|
157
|
+
});
|
|
158
|
+
const toolResult = new ToolMessage({
|
|
159
|
+
content: 'search result',
|
|
160
|
+
tool_call_id: 'c1',
|
|
161
|
+
});
|
|
162
|
+
const aiFinal = new AIMessage('final summary text');
|
|
163
|
+
const out = prepareIsolatedChildMessages([
|
|
164
|
+
new HumanMessage('user ask'),
|
|
165
|
+
aiWithTool,
|
|
166
|
+
toolResult,
|
|
167
|
+
aiFinal,
|
|
168
|
+
]);
|
|
169
|
+
// Must not contain any ToolMessage or upstream tool_calls.
|
|
170
|
+
expect(out.some((m) => m.getType() === 'tool')).toBe(false);
|
|
171
|
+
// The synthetic directive should reference only the final text output.
|
|
172
|
+
const directive = String(out[1].content);
|
|
173
|
+
expect(directive).toContain('final summary text');
|
|
174
|
+
expect(directive).not.toContain('search result');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('picks the most recent non-empty AI text output', () => {
|
|
178
|
+
const msgs: BaseMessage[] = [
|
|
179
|
+
new HumanMessage('req'),
|
|
180
|
+
new AIMessage('older text'),
|
|
181
|
+
new AIMessage(''),
|
|
182
|
+
new AIMessage('newer text'),
|
|
183
|
+
];
|
|
184
|
+
const out = prepareIsolatedChildMessages(msgs);
|
|
185
|
+
expect(String(out[1].content)).toContain('newer text');
|
|
186
|
+
expect(String(out[1].content)).not.toContain('older text');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('handles AI content in Anthropic array-of-blocks shape', () => {
|
|
190
|
+
const ai = new AIMessage({
|
|
191
|
+
content: [
|
|
192
|
+
{ type: 'tool_use', name: 'x', input: {}, id: 'c1' },
|
|
193
|
+
{ type: 'text', text: 'block text result' },
|
|
194
|
+
] as unknown as string, // the SDK types are strict, cast for fixture
|
|
195
|
+
});
|
|
196
|
+
const out = prepareIsolatedChildMessages([new HumanMessage('q'), ai]);
|
|
197
|
+
expect(out).toHaveLength(2);
|
|
198
|
+
expect(String(out[1].content)).toContain('block text result');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('falls back to raw messages if no human and no upstream text exist', () => {
|
|
202
|
+
const msgs: BaseMessage[] = [new SystemMessage('just system')];
|
|
203
|
+
const out = prepareIsolatedChildMessages(msgs);
|
|
204
|
+
expect(out).toBe(msgs);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('returns just the user message when upstream has no text content', () => {
|
|
208
|
+
const msgs: BaseMessage[] = [
|
|
209
|
+
new HumanMessage('only user'),
|
|
210
|
+
new AIMessage(''),
|
|
211
|
+
];
|
|
212
|
+
const out = prepareIsolatedChildMessages(msgs);
|
|
213
|
+
expect(out).toHaveLength(1);
|
|
214
|
+
expect(out[0].getType()).toBe('human');
|
|
215
|
+
expect(out[0].content).toBe('only user');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the finish-reason detection helpers.
|
|
3
|
+
*
|
|
4
|
+
* Guards the single source of truth that truncation-aware callers
|
|
5
|
+
* (Graph.ts sticky finish reason, host continuation retry) rely on.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
TRUNCATION_FINISH_REASONS,
|
|
9
|
+
isTruncationReason,
|
|
10
|
+
} from '../finishReasons';
|
|
11
|
+
|
|
12
|
+
describe('TRUNCATION_FINISH_REASONS', () => {
|
|
13
|
+
it('includes every provider-specific truncation value we support', () => {
|
|
14
|
+
expect(TRUNCATION_FINISH_REASONS.has('max_tokens')).toBe(true);
|
|
15
|
+
expect(TRUNCATION_FINISH_REASONS.has('length')).toBe(true);
|
|
16
|
+
expect(TRUNCATION_FINISH_REASONS.has('MAX_TOKENS')).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('is case-sensitive — lowercase vertex value is not accepted', () => {
|
|
20
|
+
// VertexAI uses the uppercase enum name; lowercase would be a wire bug
|
|
21
|
+
// we do not want to silently treat as truncation.
|
|
22
|
+
expect(TRUNCATION_FINISH_REASONS.has('max_Tokens')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('isTruncationReason', () => {
|
|
27
|
+
it('returns true for Anthropic/Bedrock max_tokens', () => {
|
|
28
|
+
expect(isTruncationReason('max_tokens')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns true for OpenAI length', () => {
|
|
32
|
+
expect(isTruncationReason('length')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns true for VertexAI MAX_TOKENS', () => {
|
|
36
|
+
expect(isTruncationReason('MAX_TOKENS')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns false for the stop/end_turn happy path', () => {
|
|
40
|
+
expect(isTruncationReason('stop')).toBe(false);
|
|
41
|
+
expect(isTruncationReason('end_turn')).toBe(false);
|
|
42
|
+
expect(isTruncationReason('STOP')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns false for tool_use / tool_calls', () => {
|
|
46
|
+
expect(isTruncationReason('tool_use')).toBe(false);
|
|
47
|
+
expect(isTruncationReason('tool_calls')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns false for undefined, null, and empty string', () => {
|
|
51
|
+
expect(isTruncationReason(undefined)).toBe(false);
|
|
52
|
+
expect(isTruncationReason(null)).toBe(false);
|
|
53
|
+
expect(isTruncationReason('')).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `toolCallNormalization`.
|
|
3
|
+
*
|
|
4
|
+
* Exercises every resolution branch of `normalizeToolCallName` and
|
|
5
|
+
* `normalizeMessageToolCalls` so future edits can't silently regress the
|
|
6
|
+
* fault-tolerance guarantees the downstream ToolNode relies on.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
normalizeToolCallName,
|
|
10
|
+
normalizeMessageToolCalls,
|
|
11
|
+
} from '../toolCallNormalization';
|
|
12
|
+
|
|
13
|
+
const allowed = new Set([
|
|
14
|
+
'outlook_operations',
|
|
15
|
+
'teams_operations',
|
|
16
|
+
'sharepoint_operations',
|
|
17
|
+
'person_lookup',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
describe('normalizeToolCallName', () => {
|
|
21
|
+
describe('exact match fast path', () => {
|
|
22
|
+
it('returns the name unchanged when it matches exactly', () => {
|
|
23
|
+
expect(normalizeToolCallName('outlook_operations', allowed)).toBe(
|
|
24
|
+
'outlook_operations'
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('delimiter normalization', () => {
|
|
30
|
+
it('maps slash delimiters to underscore', () => {
|
|
31
|
+
expect(normalizeToolCallName('outlook/operations', allowed)).toBe(
|
|
32
|
+
'outlook_operations'
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('maps dot delimiters to underscore', () => {
|
|
37
|
+
expect(normalizeToolCallName('outlook.operations', allowed)).toBe(
|
|
38
|
+
'outlook_operations'
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('maps dash delimiters to underscore', () => {
|
|
43
|
+
expect(normalizeToolCallName('outlook-operations', allowed)).toBe(
|
|
44
|
+
'outlook_operations'
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns the trimmed input when delimiter-normalized form is not registered', () => {
|
|
49
|
+
// `outlook_operations_v2` is not in the allowed set, so the resolver
|
|
50
|
+
// leaves the name untouched rather than guessing which variant the
|
|
51
|
+
// model meant.
|
|
52
|
+
expect(normalizeToolCallName('outlook.operations-v2', allowed)).toBe(
|
|
53
|
+
'outlook.operations-v2'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('case folding', () => {
|
|
59
|
+
it('resolves camelCase to registered snake_case', () => {
|
|
60
|
+
expect(normalizeToolCallName('Outlook_Operations', allowed)).toBe(
|
|
61
|
+
'outlook_operations'
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('resolves SCREAMING_CASE', () => {
|
|
66
|
+
expect(normalizeToolCallName('OUTLOOK_OPERATIONS', allowed)).toBe(
|
|
67
|
+
'outlook_operations'
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('structured candidates — prefix stripping', () => {
|
|
73
|
+
it('strips functions. prefix', () => {
|
|
74
|
+
expect(
|
|
75
|
+
normalizeToolCallName('functions.outlook_operations', allowed)
|
|
76
|
+
).toBe('outlook_operations');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('strips tools. prefix', () => {
|
|
80
|
+
expect(normalizeToolCallName('tools.teams_operations', allowed)).toBe(
|
|
81
|
+
'teams_operations'
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('takes suffix segment when dotted path has multiple segments', () => {
|
|
86
|
+
expect(
|
|
87
|
+
normalizeToolCallName('namespace.group.person_lookup', allowed)
|
|
88
|
+
).toBe('person_lookup');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('infer from tool_call id when name is empty', () => {
|
|
93
|
+
it('recovers name from an id containing the tool name', () => {
|
|
94
|
+
expect(
|
|
95
|
+
normalizeToolCallName('', allowed, 'call_outlook_operations_42')
|
|
96
|
+
).toBe('outlook_operations');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('strips counter suffix from id', () => {
|
|
100
|
+
expect(normalizeToolCallName('', allowed, 'outlook_operations_1')).toBe(
|
|
101
|
+
'outlook_operations'
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns empty string when id is also absent', () => {
|
|
106
|
+
expect(normalizeToolCallName('', allowed, undefined)).toBe('');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('fail-safe behavior', () => {
|
|
111
|
+
it('returns the trimmed input when no match is possible', () => {
|
|
112
|
+
expect(normalizeToolCallName('totally_unknown_tool', allowed)).toBe(
|
|
113
|
+
'totally_unknown_tool'
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns the trimmed input when allowed set is empty', () => {
|
|
118
|
+
expect(normalizeToolCallName('outlook_operations', new Set())).toBe(
|
|
119
|
+
'outlook_operations'
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('fails closed when case-insensitive match is ambiguous', () => {
|
|
124
|
+
const ambiguous = new Set(['Tool', 'tool']);
|
|
125
|
+
// Two allowed names fold to the same lowercase — resolver must not
|
|
126
|
+
// guess. It returns the input unchanged (via structured fallthrough).
|
|
127
|
+
const out = normalizeToolCallName('TOOL', ambiguous);
|
|
128
|
+
expect(out).toBe('TOOL');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('normalizeMessageToolCalls', () => {
|
|
134
|
+
it('rewrites LangChain-style tool_calls in place', () => {
|
|
135
|
+
const msg = {
|
|
136
|
+
tool_calls: [
|
|
137
|
+
{ name: 'functions.outlook_operations', id: 'call_1' },
|
|
138
|
+
{ name: 'Teams_Operations', id: 'call_2' },
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
const changed = normalizeMessageToolCalls(msg, allowed);
|
|
142
|
+
expect(changed).toBe(true);
|
|
143
|
+
expect(msg.tool_calls[0].name).toBe('outlook_operations');
|
|
144
|
+
expect(msg.tool_calls[1].name).toBe('teams_operations');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('rewrites Anthropic-style tool_use content blocks in place', () => {
|
|
148
|
+
const msg = {
|
|
149
|
+
content: [
|
|
150
|
+
{ type: 'text', text: 'ok' },
|
|
151
|
+
{ type: 'tool_use', name: 'outlook/operations', id: 'toolu_1' },
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
const changed = normalizeMessageToolCalls(msg, allowed);
|
|
155
|
+
expect(changed).toBe(true);
|
|
156
|
+
const toolBlock = msg.content[1] as { name?: string };
|
|
157
|
+
expect(toolBlock.name).toBe('outlook_operations');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns false when nothing needed rewriting', () => {
|
|
161
|
+
const msg = {
|
|
162
|
+
tool_calls: [{ name: 'outlook_operations', id: 'call_1' }],
|
|
163
|
+
};
|
|
164
|
+
expect(normalizeMessageToolCalls(msg, allowed)).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('is a no-op for non-message objects', () => {
|
|
168
|
+
expect(normalizeMessageToolCalls(null, allowed)).toBe(false);
|
|
169
|
+
expect(normalizeMessageToolCalls(undefined, allowed)).toBe(false);
|
|
170
|
+
expect(normalizeMessageToolCalls('string', allowed)).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('handles empty name by inferring from id', () => {
|
|
174
|
+
const msg = {
|
|
175
|
+
tool_calls: [{ name: '', id: 'call_person_lookup_99' }],
|
|
176
|
+
};
|
|
177
|
+
const changed = normalizeMessageToolCalls(msg, allowed);
|
|
178
|
+
expect(changed).toBe(true);
|
|
179
|
+
expect(msg.tool_calls[0].name).toBe('person_lookup');
|
|
180
|
+
});
|
|
181
|
+
});
|