@illuma-ai/agents 1.1.28 → 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.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 +84 -33
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +47 -8
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +493 -267
- 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 +113 -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/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 -7
- 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.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 +84 -33
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
- 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 +20 -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/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 -8
- 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 +24 -7
- package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
- 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 +10 -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 +12 -4
- 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 +90 -47
- package/src/graphs/HandoffRegistry.ts +48 -17
- package/src/graphs/MultiAgentGraph.ts +588 -327
- package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
- 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/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/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 +1 -1
- 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 +10 -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 -7
- 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,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the agentic flush loop.
|
|
3
|
+
*
|
|
4
|
+
* Scope: pure loop mechanics — model invocation, tool dispatch,
|
|
5
|
+
* ToolMessage feedback, iteration cap, NO_REPLY detection, error capture.
|
|
6
|
+
* Does NOT touch the real pgvector backend or memory_append — every tool
|
|
7
|
+
* is a stub so the test stays hermetic.
|
|
8
|
+
*/
|
|
9
|
+
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
|
10
|
+
import { tool } from '@langchain/core/tools';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import {
|
|
13
|
+
runFlushLoop,
|
|
14
|
+
extractText,
|
|
15
|
+
parseToolResult,
|
|
16
|
+
type InvokableModel,
|
|
17
|
+
} from '../flushLoop';
|
|
18
|
+
import {
|
|
19
|
+
MEMORY_APPEND_TOOL_NAME,
|
|
20
|
+
SILENT_REPLY_TOKEN,
|
|
21
|
+
} from '@/memory/constants';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build a stubbed LangChain tool with a controllable response
|
|
25
|
+
* function, so individual test cases can simulate success / schema
|
|
26
|
+
* error / backend error.
|
|
27
|
+
*/
|
|
28
|
+
function makeAppendStub(
|
|
29
|
+
respond: (args: { path: string; content: string }) => string
|
|
30
|
+
) {
|
|
31
|
+
return tool(
|
|
32
|
+
async (args) => respond(args as { path: string; content: string }),
|
|
33
|
+
{
|
|
34
|
+
name: MEMORY_APPEND_TOOL_NAME,
|
|
35
|
+
description: 'stub memory_append',
|
|
36
|
+
schema: z.object({ path: z.string(), content: z.string() }),
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Scripted model — returns pre-canned AIMessages in order. */
|
|
42
|
+
function scriptedModel(script: AIMessage[]): InvokableModel {
|
|
43
|
+
let i = 0;
|
|
44
|
+
return {
|
|
45
|
+
async invoke() {
|
|
46
|
+
const next = script[i] ?? new AIMessage({ content: 'done' });
|
|
47
|
+
i += 1;
|
|
48
|
+
return next;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('runFlushLoop', () => {
|
|
54
|
+
it('stops immediately if the first response has no tool_calls', async () => {
|
|
55
|
+
const model = scriptedModel([
|
|
56
|
+
new AIMessage({ content: 'nothing to store' }),
|
|
57
|
+
]);
|
|
58
|
+
const result = await runFlushLoop({
|
|
59
|
+
model,
|
|
60
|
+
tools: [],
|
|
61
|
+
initialMessages: [new HumanMessage('flush')],
|
|
62
|
+
});
|
|
63
|
+
expect(result.iterations).toBe(1);
|
|
64
|
+
expect(result.appendsAttempted).toBe(0);
|
|
65
|
+
expect(result.appendsSucceeded).toBe(0);
|
|
66
|
+
expect(result.toolErrors).toEqual([]);
|
|
67
|
+
expect(result.silentReply).toBe(false);
|
|
68
|
+
expect(result.finalText).toBe('nothing to store');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('detects NO_REPLY silent reply', async () => {
|
|
72
|
+
const model = scriptedModel([
|
|
73
|
+
new AIMessage({ content: SILENT_REPLY_TOKEN }),
|
|
74
|
+
]);
|
|
75
|
+
const result = await runFlushLoop({
|
|
76
|
+
model,
|
|
77
|
+
tools: [],
|
|
78
|
+
initialMessages: [new HumanMessage('flush')],
|
|
79
|
+
});
|
|
80
|
+
expect(result.silentReply).toBe(true);
|
|
81
|
+
expect(result.finalText).toBe(SILENT_REPLY_TOKEN);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('executes memory_append tool_calls and counts successes', async () => {
|
|
85
|
+
const appendTool = makeAppendStub(({ path }) =>
|
|
86
|
+
JSON.stringify({ ok: true, path })
|
|
87
|
+
);
|
|
88
|
+
const model = scriptedModel([
|
|
89
|
+
new AIMessage({
|
|
90
|
+
content: '',
|
|
91
|
+
tool_calls: [
|
|
92
|
+
{
|
|
93
|
+
name: MEMORY_APPEND_TOOL_NAME,
|
|
94
|
+
args: { path: 'memory/2026-04-13.md', content: 'user prefers X' },
|
|
95
|
+
id: 'c1',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: MEMORY_APPEND_TOOL_NAME,
|
|
99
|
+
args: {
|
|
100
|
+
path: 'memory/2026-04-13.md',
|
|
101
|
+
content: 'tool schema failed',
|
|
102
|
+
},
|
|
103
|
+
id: 'c2',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
new AIMessage({ content: SILENT_REPLY_TOKEN }),
|
|
108
|
+
]);
|
|
109
|
+
const result = await runFlushLoop({
|
|
110
|
+
model,
|
|
111
|
+
tools: [appendTool],
|
|
112
|
+
initialMessages: [new HumanMessage('flush')],
|
|
113
|
+
});
|
|
114
|
+
expect(result.iterations).toBe(2);
|
|
115
|
+
expect(result.appendsAttempted).toBe(2);
|
|
116
|
+
expect(result.appendsSucceeded).toBe(2);
|
|
117
|
+
expect(result.silentReply).toBe(true);
|
|
118
|
+
expect(result.toolErrors).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('captures tool errors and feeds them back as ToolMessages', async () => {
|
|
122
|
+
const appendTool = makeAppendStub(() =>
|
|
123
|
+
JSON.stringify({ ok: false, error: 'path must start with memory/' })
|
|
124
|
+
);
|
|
125
|
+
const model = scriptedModel([
|
|
126
|
+
new AIMessage({
|
|
127
|
+
content: '',
|
|
128
|
+
tool_calls: [
|
|
129
|
+
{
|
|
130
|
+
name: MEMORY_APPEND_TOOL_NAME,
|
|
131
|
+
args: { path: 'invalid.md', content: 'x' },
|
|
132
|
+
id: 'bad1',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
}),
|
|
136
|
+
new AIMessage({ content: 'gave up' }),
|
|
137
|
+
]);
|
|
138
|
+
const result = await runFlushLoop({
|
|
139
|
+
model,
|
|
140
|
+
tools: [appendTool],
|
|
141
|
+
initialMessages: [new HumanMessage('flush')],
|
|
142
|
+
});
|
|
143
|
+
expect(result.appendsAttempted).toBe(1);
|
|
144
|
+
expect(result.appendsSucceeded).toBe(0);
|
|
145
|
+
expect(result.toolErrors).toHaveLength(1);
|
|
146
|
+
expect(result.toolErrors[0].error).toMatch(/path must start with memory/);
|
|
147
|
+
// The ToolMessage should be in the transcript so the next invoke
|
|
148
|
+
// would see it.
|
|
149
|
+
const hasToolMsg = result.messages.some((m) => m._getType() === 'tool');
|
|
150
|
+
expect(hasToolMsg).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('records tool_not_found when the model calls an unknown tool', async () => {
|
|
154
|
+
const model = scriptedModel([
|
|
155
|
+
new AIMessage({
|
|
156
|
+
content: '',
|
|
157
|
+
tool_calls: [{ name: 'mystery_tool', args: {}, id: 'm1' }],
|
|
158
|
+
}),
|
|
159
|
+
new AIMessage({ content: 'stopped' }),
|
|
160
|
+
]);
|
|
161
|
+
const result = await runFlushLoop({
|
|
162
|
+
model,
|
|
163
|
+
tools: [],
|
|
164
|
+
initialMessages: [new HumanMessage('flush')],
|
|
165
|
+
});
|
|
166
|
+
expect(result.toolErrors).toHaveLength(1);
|
|
167
|
+
expect(result.toolErrors[0].error).toMatch(/tool_not_found/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('stops at maxIterations and flags hitIterationCap', async () => {
|
|
171
|
+
// A model that keeps emitting tool_calls forever.
|
|
172
|
+
const appendTool = makeAppendStub(() =>
|
|
173
|
+
JSON.stringify({ ok: true, path: 'memory/x.md' })
|
|
174
|
+
);
|
|
175
|
+
const model: InvokableModel = {
|
|
176
|
+
async invoke() {
|
|
177
|
+
return new AIMessage({
|
|
178
|
+
content: '',
|
|
179
|
+
tool_calls: [
|
|
180
|
+
{
|
|
181
|
+
name: MEMORY_APPEND_TOOL_NAME,
|
|
182
|
+
args: { path: 'memory/x.md', content: 'loop' },
|
|
183
|
+
id: 'loop',
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
const result = await runFlushLoop({
|
|
190
|
+
model,
|
|
191
|
+
tools: [appendTool],
|
|
192
|
+
initialMessages: [new HumanMessage('flush')],
|
|
193
|
+
maxIterations: 3,
|
|
194
|
+
});
|
|
195
|
+
expect(result.iterations).toBe(3);
|
|
196
|
+
expect(result.hitIterationCap).toBe(true);
|
|
197
|
+
expect(result.appendsAttempted).toBe(3);
|
|
198
|
+
expect(result.appendsSucceeded).toBe(3);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('surfaces thrown tool errors without aborting the loop', async () => {
|
|
202
|
+
const appendTool = makeAppendStub(() => {
|
|
203
|
+
throw new Error('backend down');
|
|
204
|
+
});
|
|
205
|
+
const model = scriptedModel([
|
|
206
|
+
new AIMessage({
|
|
207
|
+
content: '',
|
|
208
|
+
tool_calls: [
|
|
209
|
+
{
|
|
210
|
+
name: MEMORY_APPEND_TOOL_NAME,
|
|
211
|
+
args: { path: 'memory/x.md', content: 'x' },
|
|
212
|
+
id: 't1',
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
}),
|
|
216
|
+
new AIMessage({ content: 'done' }),
|
|
217
|
+
]);
|
|
218
|
+
const result = await runFlushLoop({
|
|
219
|
+
model,
|
|
220
|
+
tools: [appendTool],
|
|
221
|
+
initialMessages: [new HumanMessage('flush')],
|
|
222
|
+
});
|
|
223
|
+
expect(result.appendsAttempted).toBe(1);
|
|
224
|
+
expect(result.appendsSucceeded).toBe(0);
|
|
225
|
+
expect(result.toolErrors[0].error).toMatch(/backend down/);
|
|
226
|
+
expect(result.iterations).toBe(2);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('extractText', () => {
|
|
231
|
+
it('returns string content directly', () => {
|
|
232
|
+
expect(extractText(new AIMessage({ content: 'hi' }))).toBe('hi');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('joins array content blocks', () => {
|
|
236
|
+
const ai = new AIMessage({
|
|
237
|
+
content: [
|
|
238
|
+
{ type: 'text', text: 'hello ' },
|
|
239
|
+
{ type: 'text', text: 'world' },
|
|
240
|
+
] as unknown as string,
|
|
241
|
+
});
|
|
242
|
+
expect(extractText(ai)).toBe('hello world');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('parseToolResult', () => {
|
|
247
|
+
it('parses well-formed JSON with ok field', () => {
|
|
248
|
+
expect(parseToolResult('{"ok":true,"path":"memory/x.md"}')).toEqual({
|
|
249
|
+
ok: true,
|
|
250
|
+
path: 'memory/x.md',
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('parses error JSON', () => {
|
|
255
|
+
expect(parseToolResult('{"ok":false,"error":"bad"}')).toEqual({
|
|
256
|
+
ok: false,
|
|
257
|
+
error: 'bad',
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('treats non-JSON as opaque success', () => {
|
|
262
|
+
expect(parseToolResult('hello')).toEqual({ ok: true });
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { shouldFlushMemory } from '../memoryFlushPhase';
|
|
2
|
+
|
|
3
|
+
describe('shouldFlushMemory', () => {
|
|
4
|
+
// Use upstream-aligned defaults: soft=4000, reserve=20000 means a 200k
|
|
5
|
+
// window fires at 176k. Explicit numbers here keep the test independent
|
|
6
|
+
// from the constants file.
|
|
7
|
+
const base = {
|
|
8
|
+
windowTokens: 200_000,
|
|
9
|
+
reserveFloorTokens: 20_000,
|
|
10
|
+
softThresholdTokens: 4_000,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it('returns false far from the window', () => {
|
|
14
|
+
expect(shouldFlushMemory({ ...base, currentTokens: 100_000 })).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns true exactly at the threshold', () => {
|
|
18
|
+
// window - reserve - soft = 176_000
|
|
19
|
+
expect(shouldFlushMemory({ ...base, currentTokens: 176_000 })).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns true past the threshold', () => {
|
|
23
|
+
expect(shouldFlushMemory({ ...base, currentTokens: 190_000 })).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns false for non-finite inputs', () => {
|
|
27
|
+
expect(shouldFlushMemory({ ...base, currentTokens: Number.NaN })).toBe(
|
|
28
|
+
false
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns false when window is zero', () => {
|
|
33
|
+
expect(shouldFlushMemory({ currentTokens: 100, windowTokens: 0 })).toBe(
|
|
34
|
+
false
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration-lite test for {@link runMemoryFlush}: scripted model +
|
|
3
|
+
* in-memory backend stub. Verifies end-to-end that the flush phase
|
|
4
|
+
* flips, tool_calls execute, the backend sees the writes, and the
|
|
5
|
+
* rich result object is populated.
|
|
6
|
+
*/
|
|
7
|
+
import { AIMessage } from '@langchain/core/messages';
|
|
8
|
+
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
9
|
+
import { runMemoryFlush } from '../memoryFlushPhase';
|
|
10
|
+
import type {
|
|
11
|
+
MemoryAppendInput,
|
|
12
|
+
MemoryBackend,
|
|
13
|
+
MemoryScope,
|
|
14
|
+
} from '@/memory/types';
|
|
15
|
+
import {
|
|
16
|
+
MEMORY_APPEND_TOOL_NAME,
|
|
17
|
+
MEMORY_PHASE_FLUSHING,
|
|
18
|
+
MEMORY_PHASE_NORMAL,
|
|
19
|
+
SILENT_REPLY_TOKEN,
|
|
20
|
+
} from '@/memory/constants';
|
|
21
|
+
|
|
22
|
+
/** Minimal in-memory backend — just records appends. */
|
|
23
|
+
function makeBackend(): MemoryBackend & { writes: MemoryAppendInput[] } {
|
|
24
|
+
const writes: MemoryAppendInput[] = [];
|
|
25
|
+
return {
|
|
26
|
+
kind: 'vector',
|
|
27
|
+
writes,
|
|
28
|
+
async search() {
|
|
29
|
+
return [];
|
|
30
|
+
},
|
|
31
|
+
async get() {
|
|
32
|
+
return null;
|
|
33
|
+
},
|
|
34
|
+
async append(_scope: MemoryScope, input: MemoryAppendInput) {
|
|
35
|
+
writes.push(input);
|
|
36
|
+
},
|
|
37
|
+
async health() {
|
|
38
|
+
return { ok: true, backend: 'vector' };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scripted model masquerading as a BaseChatModel. We only need
|
|
45
|
+
* `invoke` and `bindTools`; the memoryFlushPhase code accesses those
|
|
46
|
+
* through bindToolsIfSupported and runFlushLoop only.
|
|
47
|
+
*/
|
|
48
|
+
function scriptedModel(script: AIMessage[]) {
|
|
49
|
+
let i = 0;
|
|
50
|
+
const model = {
|
|
51
|
+
bindTools() {
|
|
52
|
+
return model;
|
|
53
|
+
},
|
|
54
|
+
async invoke() {
|
|
55
|
+
const next = script[i] ?? new AIMessage({ content: SILENT_REPLY_TOKEN });
|
|
56
|
+
i += 1;
|
|
57
|
+
return next;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return model as unknown as BaseChatModel;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('runMemoryFlush', () => {
|
|
64
|
+
it('flips phase, runs the loop, and writes notes through the backend', async () => {
|
|
65
|
+
const backend = makeBackend();
|
|
66
|
+
const phases: string[] = [];
|
|
67
|
+
const setPhase = (
|
|
68
|
+
p: typeof MEMORY_PHASE_NORMAL | typeof MEMORY_PHASE_FLUSHING
|
|
69
|
+
) => {
|
|
70
|
+
phases.push(p);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const model = scriptedModel([
|
|
74
|
+
new AIMessage({
|
|
75
|
+
content: '',
|
|
76
|
+
tool_calls: [
|
|
77
|
+
{
|
|
78
|
+
name: MEMORY_APPEND_TOOL_NAME,
|
|
79
|
+
args: {
|
|
80
|
+
path: 'memory/user/preferences.md',
|
|
81
|
+
content: 'User is Varun; prefers clean agent separation.',
|
|
82
|
+
},
|
|
83
|
+
id: 'c1',
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
}),
|
|
87
|
+
new AIMessage({ content: SILENT_REPLY_TOKEN }),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const result = await runMemoryFlush({
|
|
91
|
+
model,
|
|
92
|
+
memory: {
|
|
93
|
+
backend,
|
|
94
|
+
scope: { agentId: 'orchestrator', userId: 'user-1' },
|
|
95
|
+
},
|
|
96
|
+
conversationSummary: 'Varun explained his preferences...',
|
|
97
|
+
setPhase,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.ran).toBe(true);
|
|
101
|
+
expect(result.iterations).toBe(2);
|
|
102
|
+
expect(result.appendsAttempted).toBe(1);
|
|
103
|
+
expect(result.appendsSucceeded).toBe(1);
|
|
104
|
+
expect(result.silentReply).toBe(true);
|
|
105
|
+
expect(result.toolErrors).toEqual([]);
|
|
106
|
+
expect(backend.writes).toHaveLength(1);
|
|
107
|
+
expect(backend.writes[0].path).toBe('memory/user/preferences.md');
|
|
108
|
+
expect(phases[0]).toBe(MEMORY_PHASE_FLUSHING);
|
|
109
|
+
expect(phases[phases.length - 1]).toBe(MEMORY_PHASE_NORMAL);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns ran=false when flush is disabled', async () => {
|
|
113
|
+
const backend = makeBackend();
|
|
114
|
+
const result = await runMemoryFlush({
|
|
115
|
+
model: scriptedModel([]),
|
|
116
|
+
memory: {
|
|
117
|
+
backend,
|
|
118
|
+
scope: { agentId: 'a', userId: 'u' },
|
|
119
|
+
flush: { enabled: false },
|
|
120
|
+
},
|
|
121
|
+
conversationSummary: 'x',
|
|
122
|
+
setPhase: () => {},
|
|
123
|
+
});
|
|
124
|
+
expect(result.ran).toBe(false);
|
|
125
|
+
expect(backend.writes).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('restores phase even when the model throws', async () => {
|
|
129
|
+
const backend = makeBackend();
|
|
130
|
+
const phases: string[] = [];
|
|
131
|
+
const brokenModel = {
|
|
132
|
+
bindTools() {
|
|
133
|
+
return brokenModel;
|
|
134
|
+
},
|
|
135
|
+
async invoke() {
|
|
136
|
+
throw new Error('boom');
|
|
137
|
+
},
|
|
138
|
+
} as unknown as BaseChatModel;
|
|
139
|
+
|
|
140
|
+
const result = await runMemoryFlush({
|
|
141
|
+
model: brokenModel,
|
|
142
|
+
memory: { backend, scope: { agentId: 'a', userId: 'u' } },
|
|
143
|
+
conversationSummary: 'x',
|
|
144
|
+
setPhase: (p) => phases.push(p),
|
|
145
|
+
});
|
|
146
|
+
expect(result.ran).toBe(false);
|
|
147
|
+
expect(result.error).toMatch(/boom/);
|
|
148
|
+
expect(phases[phases.length - 1]).toBe(MEMORY_PHASE_NORMAL);
|
|
149
|
+
});
|
|
150
|
+
});
|