@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,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentic tool-execution loop for the memory-flush reflection turn.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from {@link runMemoryFlush} so the loop primitive is
|
|
5
|
+
* testable in isolation and reusable from any reflection-style phase
|
|
6
|
+
* that wants "invoke → execute tool_calls → feed results back → repeat".
|
|
7
|
+
*
|
|
8
|
+
* Design:
|
|
9
|
+
* - Bounded iterations — caller passes `maxIterations` (default
|
|
10
|
+
* {@link DEFAULT_MAX_FLUSH_ITERATIONS}). The loop exits as soon as
|
|
11
|
+
* the model stops emitting `tool_calls`, hits the cap, or the reply
|
|
12
|
+
* matches {@link SILENT_REPLY_TOKEN}.
|
|
13
|
+
* - Rich result — returns iteration count, attempted/successful
|
|
14
|
+
* appends, per-tool errors, silent-reply flag, and raw final text.
|
|
15
|
+
* The caller logs this; nothing is swallowed.
|
|
16
|
+
* - Self-contained tool execution — does not depend on LangGraph's
|
|
17
|
+
* ToolNode or any graph runtime. The only contract is the
|
|
18
|
+
* LangChain v0.3 `StructuredToolInterface.invoke(args)` shape.
|
|
19
|
+
* - Tool errors are captured as {@link ToolMessage}s with the raw
|
|
20
|
+
* JSON error, so the model can read them and self-correct (e.g.
|
|
21
|
+
* retry with a different path if the schema rejected its first
|
|
22
|
+
* attempt).
|
|
23
|
+
*/
|
|
24
|
+
import {
|
|
25
|
+
AIMessage,
|
|
26
|
+
type BaseMessage,
|
|
27
|
+
ToolMessage,
|
|
28
|
+
} from '@langchain/core/messages';
|
|
29
|
+
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_MAX_FLUSH_ITERATIONS,
|
|
32
|
+
MEMORY_APPEND_TOOL_NAME,
|
|
33
|
+
SILENT_REPLY_TOKEN,
|
|
34
|
+
} from '@/memory/constants';
|
|
35
|
+
|
|
36
|
+
/** Minimal model contract the loop needs. */
|
|
37
|
+
export interface InvokableModel {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
39
|
+
invoke(messages: BaseMessage[], options?: any): Promise<AIMessage>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FlushLoopParams {
|
|
43
|
+
model: InvokableModel;
|
|
44
|
+
tools: StructuredToolInterface[];
|
|
45
|
+
/** Initial messages — typically [SystemMessage, HumanMessage]. */
|
|
46
|
+
initialMessages: BaseMessage[];
|
|
47
|
+
/** Upper bound on model.invoke() calls. Default 8. */
|
|
48
|
+
maxIterations?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Optional debug hook — called once per iteration with `{ i, ai, toolCalls }`.
|
|
51
|
+
* Use for INFO-level tracing during rollout; pass `undefined` in prod.
|
|
52
|
+
*/
|
|
53
|
+
onIteration?: (event: {
|
|
54
|
+
i: number;
|
|
55
|
+
ai: AIMessage;
|
|
56
|
+
toolCalls: ToolCallLike[];
|
|
57
|
+
}) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Shape LangChain emits on AIMessage.tool_calls (duck-typed). */
|
|
61
|
+
export interface ToolCallLike {
|
|
62
|
+
name: string;
|
|
63
|
+
args: Record<string, unknown>;
|
|
64
|
+
id?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ToolErrorRecord {
|
|
68
|
+
iteration: number;
|
|
69
|
+
toolName: string;
|
|
70
|
+
toolCallId?: string;
|
|
71
|
+
error: string;
|
|
72
|
+
/** Raw args the model passed — useful for diagnosing schema drift. */
|
|
73
|
+
args?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface FlushLoopResult {
|
|
77
|
+
/** Number of model.invoke() calls actually made. */
|
|
78
|
+
iterations: number;
|
|
79
|
+
/** Every `memory_append` tool_call the model emitted across all iterations. */
|
|
80
|
+
appendsAttempted: number;
|
|
81
|
+
/** How many of those returned `{ ok: true, ... }`. */
|
|
82
|
+
appendsSucceeded: number;
|
|
83
|
+
/** Tool errors the model saw (and may have reacted to). */
|
|
84
|
+
toolErrors: ToolErrorRecord[];
|
|
85
|
+
/** True if the final AIMessage text matched {@link SILENT_REPLY_TOKEN}. */
|
|
86
|
+
silentReply: boolean;
|
|
87
|
+
/** Whether the loop stopped because it hit `maxIterations`. */
|
|
88
|
+
hitIterationCap: boolean;
|
|
89
|
+
/** Final AIMessage content as plain text (best-effort). */
|
|
90
|
+
finalText: string;
|
|
91
|
+
/** Full message transcript — useful for debugging / tests. */
|
|
92
|
+
messages: BaseMessage[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract a flat text string from any AIMessage content shape
|
|
97
|
+
* (string, array of content blocks, etc).
|
|
98
|
+
*/
|
|
99
|
+
export function extractText(message: AIMessage): string {
|
|
100
|
+
const c = message.content;
|
|
101
|
+
if (typeof c === 'string') return c;
|
|
102
|
+
if (!Array.isArray(c)) return '';
|
|
103
|
+
return c
|
|
104
|
+
.map((block) => {
|
|
105
|
+
if (typeof block === 'string') return block;
|
|
106
|
+
if (block && typeof block === 'object' && 'text' in block) {
|
|
107
|
+
return String((block as { text?: unknown }).text ?? '');
|
|
108
|
+
}
|
|
109
|
+
return '';
|
|
110
|
+
})
|
|
111
|
+
.join('')
|
|
112
|
+
.trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parse a tool result string. Memory tools return JSON with
|
|
117
|
+
* `{ ok: boolean, error?: string, path?: string }`. Non-JSON payloads
|
|
118
|
+
* are treated as opaque success strings.
|
|
119
|
+
*/
|
|
120
|
+
export function parseToolResult(raw: string): {
|
|
121
|
+
ok: boolean;
|
|
122
|
+
error?: string;
|
|
123
|
+
path?: string;
|
|
124
|
+
} {
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(raw);
|
|
127
|
+
if (parsed && typeof parsed === 'object' && 'ok' in parsed) {
|
|
128
|
+
return {
|
|
129
|
+
ok: Boolean(parsed.ok),
|
|
130
|
+
error: typeof parsed.error === 'string' ? parsed.error : undefined,
|
|
131
|
+
path: typeof parsed.path === 'string' ? parsed.path : undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { ok: true };
|
|
135
|
+
} catch {
|
|
136
|
+
return { ok: true };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Bind a tool array to a model if `bindTools` exists on the model.
|
|
142
|
+
* Exported so the caller (runMemoryFlush) can do it once and pass the
|
|
143
|
+
* bound model in, keeping this loop free of LangChain model-specific
|
|
144
|
+
* extensions.
|
|
145
|
+
*/
|
|
146
|
+
export function bindToolsIfSupported(
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
model: any,
|
|
149
|
+
tools: StructuredToolInterface[]
|
|
150
|
+
): InvokableModel {
|
|
151
|
+
if (typeof model?.bindTools === 'function') {
|
|
152
|
+
return model.bindTools(tools) as InvokableModel;
|
|
153
|
+
}
|
|
154
|
+
return model as InvokableModel;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Run the agentic reflection loop until the model stops emitting
|
|
159
|
+
* tool_calls, the iteration cap is hit, or the final reply matches
|
|
160
|
+
* {@link SILENT_REPLY_TOKEN}.
|
|
161
|
+
*/
|
|
162
|
+
export async function runFlushLoop(
|
|
163
|
+
params: FlushLoopParams
|
|
164
|
+
): Promise<FlushLoopResult> {
|
|
165
|
+
const {
|
|
166
|
+
model,
|
|
167
|
+
tools,
|
|
168
|
+
initialMessages,
|
|
169
|
+
maxIterations = DEFAULT_MAX_FLUSH_ITERATIONS,
|
|
170
|
+
onIteration,
|
|
171
|
+
} = params;
|
|
172
|
+
|
|
173
|
+
const toolMap = new Map<string, StructuredToolInterface>();
|
|
174
|
+
for (const t of tools) {
|
|
175
|
+
toolMap.set(t.name, t);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const messages: BaseMessage[] = [...initialMessages];
|
|
179
|
+
const toolErrors: ToolErrorRecord[] = [];
|
|
180
|
+
let appendsAttempted = 0;
|
|
181
|
+
let appendsSucceeded = 0;
|
|
182
|
+
let iterations = 0;
|
|
183
|
+
let finalText = '';
|
|
184
|
+
let hitIterationCap = false;
|
|
185
|
+
// Tracks whether the most recent iteration ended with unresolved
|
|
186
|
+
// tool_calls. If we exit the while via the cap (not via the natural
|
|
187
|
+
// `break`), this stays true and we flag hitIterationCap.
|
|
188
|
+
let lastIterationHadPendingCalls = false;
|
|
189
|
+
|
|
190
|
+
while (iterations < maxIterations) {
|
|
191
|
+
iterations += 1;
|
|
192
|
+
const ai = await model.invoke(messages);
|
|
193
|
+
messages.push(ai);
|
|
194
|
+
|
|
195
|
+
const toolCalls = (ai.tool_calls ?? []) as ToolCallLike[];
|
|
196
|
+
onIteration?.({ i: iterations, ai, toolCalls });
|
|
197
|
+
|
|
198
|
+
if (toolCalls.length === 0) {
|
|
199
|
+
finalText = extractText(ai);
|
|
200
|
+
lastIterationHadPendingCalls = false;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
lastIterationHadPendingCalls = true;
|
|
204
|
+
|
|
205
|
+
for (const call of toolCalls) {
|
|
206
|
+
if (call.name === MEMORY_APPEND_TOOL_NAME) {
|
|
207
|
+
appendsAttempted += 1;
|
|
208
|
+
}
|
|
209
|
+
const tool = toolMap.get(call.name);
|
|
210
|
+
if (!tool) {
|
|
211
|
+
const errStr = `tool_not_found: ${call.name}`;
|
|
212
|
+
toolErrors.push({
|
|
213
|
+
iteration: iterations,
|
|
214
|
+
toolName: call.name,
|
|
215
|
+
toolCallId: call.id,
|
|
216
|
+
error: errStr,
|
|
217
|
+
args: call.args,
|
|
218
|
+
});
|
|
219
|
+
messages.push(
|
|
220
|
+
new ToolMessage({
|
|
221
|
+
content: JSON.stringify({ ok: false, error: errStr }),
|
|
222
|
+
tool_call_id: call.id ?? '',
|
|
223
|
+
name: call.name,
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let rawResult: string;
|
|
230
|
+
try {
|
|
231
|
+
// LangChain tools accept the parsed args object.
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
233
|
+
const res = await (tool as any).invoke(call.args);
|
|
234
|
+
rawResult = typeof res === 'string' ? res : JSON.stringify(res);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
const errStr = err instanceof Error ? err.message : String(err);
|
|
237
|
+
toolErrors.push({
|
|
238
|
+
iteration: iterations,
|
|
239
|
+
toolName: call.name,
|
|
240
|
+
toolCallId: call.id,
|
|
241
|
+
error: errStr,
|
|
242
|
+
args: call.args,
|
|
243
|
+
});
|
|
244
|
+
messages.push(
|
|
245
|
+
new ToolMessage({
|
|
246
|
+
content: JSON.stringify({ ok: false, error: errStr }),
|
|
247
|
+
tool_call_id: call.id ?? '',
|
|
248
|
+
name: call.name,
|
|
249
|
+
})
|
|
250
|
+
);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const parsed = parseToolResult(rawResult);
|
|
255
|
+
if (call.name === MEMORY_APPEND_TOOL_NAME) {
|
|
256
|
+
if (parsed.ok) {
|
|
257
|
+
appendsSucceeded += 1;
|
|
258
|
+
} else if (parsed.error) {
|
|
259
|
+
toolErrors.push({
|
|
260
|
+
iteration: iterations,
|
|
261
|
+
toolName: call.name,
|
|
262
|
+
toolCallId: call.id,
|
|
263
|
+
error: parsed.error,
|
|
264
|
+
args: call.args,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
messages.push(
|
|
270
|
+
new ToolMessage({
|
|
271
|
+
content: rawResult,
|
|
272
|
+
tool_call_id: call.id ?? '',
|
|
273
|
+
name: call.name,
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (iterations >= maxIterations && lastIterationHadPendingCalls) {
|
|
280
|
+
hitIterationCap = true;
|
|
281
|
+
// Surface the last AIMessage text (if any) so callers can still log it.
|
|
282
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
283
|
+
const m = messages[i];
|
|
284
|
+
if (m instanceof AIMessage) {
|
|
285
|
+
finalText = extractText(m);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const silentReply = finalText.trim() === SILENT_REPLY_TOKEN;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
iterations,
|
|
295
|
+
appendsAttempted,
|
|
296
|
+
appendsSucceeded,
|
|
297
|
+
toolErrors,
|
|
298
|
+
silentReply,
|
|
299
|
+
hitIterationCap,
|
|
300
|
+
finalText,
|
|
301
|
+
messages,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory flush phase — trigger logic + reflection invocation.
|
|
3
|
+
*
|
|
4
|
+
* Ported from upstream's post-turn flush handler. The agent is re-invoked
|
|
5
|
+
* with a reflection system prompt and the `memory_append` tool unlocked;
|
|
6
|
+
* it writes notes to its future self, then the graph returns to normal.
|
|
7
|
+
*
|
|
8
|
+
* This module is INTENTIONALLY decoupled from the specific graph runtime —
|
|
9
|
+
* it exposes pure functions (`shouldFlushMemory`) plus a runner that takes
|
|
10
|
+
* the model as a parameter, so the same logic works from `createAgentNode`,
|
|
11
|
+
* `MultiAgentGraph`, or a future graph backend that wants to override
|
|
12
|
+
* flush behaviour.
|
|
13
|
+
*
|
|
14
|
+
* The agentic tool-execution loop (invoke → run tool_calls → feed results
|
|
15
|
+
* back → repeat until stop) lives in {@link ./flushLoop}. This module wires
|
|
16
|
+
* it up: build tools, resolve prompts, flip the phase, call the loop,
|
|
17
|
+
* shape the rich result.
|
|
18
|
+
*/
|
|
19
|
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
20
|
+
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_FLUSH_RESERVE_FLOOR_TOKENS,
|
|
23
|
+
DEFAULT_FLUSH_SOFT_THRESHOLD_TOKENS,
|
|
24
|
+
DEFAULT_MAX_FLUSH_ITERATIONS,
|
|
25
|
+
MEMORY_PHASE_FLUSHING,
|
|
26
|
+
MEMORY_PHASE_NORMAL,
|
|
27
|
+
} from '@/memory/constants';
|
|
28
|
+
import type { MemoryConfig } from '@/memory/types';
|
|
29
|
+
import { buildMemoryTools } from '@/tools/memory';
|
|
30
|
+
import { resolveFlushPrompts } from '@/prompts/memoryFlushPrompt';
|
|
31
|
+
import {
|
|
32
|
+
bindToolsIfSupported,
|
|
33
|
+
runFlushLoop,
|
|
34
|
+
type FlushLoopResult,
|
|
35
|
+
type ToolErrorRecord,
|
|
36
|
+
} from './flushLoop';
|
|
37
|
+
|
|
38
|
+
export interface ShouldFlushInput {
|
|
39
|
+
currentTokens: number;
|
|
40
|
+
windowTokens: number;
|
|
41
|
+
reserveFloorTokens?: number;
|
|
42
|
+
softThresholdTokens?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pure trigger function: fires when the current context is within
|
|
47
|
+
* `softThreshold + reserveFloor` tokens of the model window. Matches
|
|
48
|
+
* upstream's formula.
|
|
49
|
+
*/
|
|
50
|
+
export function shouldFlushMemory(input: ShouldFlushInput): boolean {
|
|
51
|
+
if (
|
|
52
|
+
!Number.isFinite(input.currentTokens) ||
|
|
53
|
+
!Number.isFinite(input.windowTokens)
|
|
54
|
+
) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (input.windowTokens <= 0) return false;
|
|
58
|
+
const reserve =
|
|
59
|
+
input.reserveFloorTokens ?? DEFAULT_FLUSH_RESERVE_FLOOR_TOKENS;
|
|
60
|
+
const soft = input.softThresholdTokens ?? DEFAULT_FLUSH_SOFT_THRESHOLD_TOKENS;
|
|
61
|
+
return input.currentTokens >= input.windowTokens - reserve - soft;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RunFlushParams {
|
|
65
|
+
model: BaseChatModel;
|
|
66
|
+
memory: MemoryConfig;
|
|
67
|
+
/** A compact summary of the conversation — last N turns, not raw history. */
|
|
68
|
+
conversationSummary: string;
|
|
69
|
+
/**
|
|
70
|
+
* Accessor the graph runtime uses to expose the current phase to the
|
|
71
|
+
* append tool. The runner sets this to `memory_flushing` for the duration
|
|
72
|
+
* of the reflection turn and restores it on exit.
|
|
73
|
+
*/
|
|
74
|
+
setPhase: (
|
|
75
|
+
phase: typeof MEMORY_PHASE_NORMAL | typeof MEMORY_PHASE_FLUSHING
|
|
76
|
+
) => void;
|
|
77
|
+
/**
|
|
78
|
+
* @deprecated No longer used — the 8-path canonical-document model
|
|
79
|
+
* does not date-stamp files. Kept in the params shape for one release
|
|
80
|
+
* so host's callers don't break on the type signature.
|
|
81
|
+
*/
|
|
82
|
+
timezone?: string;
|
|
83
|
+
/**
|
|
84
|
+
* @deprecated Same as `timezone` — unused in the canonical-document model.
|
|
85
|
+
*/
|
|
86
|
+
nowMs?: number;
|
|
87
|
+
/** Override for the agentic-loop iteration cap. */
|
|
88
|
+
maxIterations?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Optional per-iteration callback for structured debug logging.
|
|
91
|
+
* Caller is expected to demote this to debug once the rollout is stable.
|
|
92
|
+
*/
|
|
93
|
+
onIteration?: (event: {
|
|
94
|
+
i: number;
|
|
95
|
+
toolCallCount: number;
|
|
96
|
+
toolNames: string[];
|
|
97
|
+
}) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface RunFlushResult {
|
|
101
|
+
/** Did the flush actually run (false if disabled via config). */
|
|
102
|
+
ran: boolean;
|
|
103
|
+
/** Model.invoke() iterations actually performed. */
|
|
104
|
+
iterations?: number;
|
|
105
|
+
/** Total `memory_append` calls the model emitted. */
|
|
106
|
+
appendsAttempted?: number;
|
|
107
|
+
/** Subset that returned `{ ok: true }` from the backend. */
|
|
108
|
+
appendsSucceeded?: number;
|
|
109
|
+
/** Tool errors the model saw during the flush — surfaced for logging. */
|
|
110
|
+
toolErrors?: ToolErrorRecord[];
|
|
111
|
+
/** Final text reply equalled SILENT_REPLY_TOKEN (`NO_REPLY`). */
|
|
112
|
+
silentReply?: boolean;
|
|
113
|
+
/** Loop hit its iteration cap with tool_calls still pending. */
|
|
114
|
+
hitIterationCap?: boolean;
|
|
115
|
+
/** Final text reply from the last AIMessage. */
|
|
116
|
+
finalText?: string;
|
|
117
|
+
/** Error message if the flush threw. */
|
|
118
|
+
error?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Run the reflection turn. The model is re-invoked with just the flush
|
|
123
|
+
* prompt + compact summary + the append tool unlocked. We intentionally
|
|
124
|
+
* drop the full history — the prompt tells the agent to write notes based
|
|
125
|
+
* on what it learned, not to continue the conversation.
|
|
126
|
+
*
|
|
127
|
+
* The loop keeps invoking the model until it stops emitting tool_calls,
|
|
128
|
+
* hits the iteration cap, or replies with {@link SILENT_REPLY_TOKEN}. Each
|
|
129
|
+
* `memory_append` tool_call is executed against the pgvector backend and
|
|
130
|
+
* the result (success path or error) is fed back as a ToolMessage so the
|
|
131
|
+
* model can self-correct (e.g. retry with a valid path).
|
|
132
|
+
*/
|
|
133
|
+
export async function runMemoryFlush(
|
|
134
|
+
params: RunFlushParams
|
|
135
|
+
): Promise<RunFlushResult> {
|
|
136
|
+
const {
|
|
137
|
+
model,
|
|
138
|
+
memory,
|
|
139
|
+
conversationSummary,
|
|
140
|
+
setPhase,
|
|
141
|
+
maxIterations,
|
|
142
|
+
onIteration,
|
|
143
|
+
} = params;
|
|
144
|
+
if (memory.flush?.enabled === false) {
|
|
145
|
+
return { ran: false };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setPhase(MEMORY_PHASE_FLUSHING);
|
|
149
|
+
try {
|
|
150
|
+
const tools = buildMemoryTools({
|
|
151
|
+
...memory,
|
|
152
|
+
readEnabled: false,
|
|
153
|
+
writeEnabled: true,
|
|
154
|
+
getPhase: () => MEMORY_PHASE_FLUSHING,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Bind tools to the caller-provided model. If the model already came
|
|
158
|
+
// bound (some graph wrappers do), bindToolsIfSupported is a no-op.
|
|
159
|
+
const bound = bindToolsIfSupported(model, tools);
|
|
160
|
+
|
|
161
|
+
// Resolve flush prompts with the scope-aware rubric. For an
|
|
162
|
+
// isolated agent (no userId in scope), the user-tier paths are
|
|
163
|
+
// filtered out of the rubric so the LLM never sees them — it
|
|
164
|
+
// physically cannot route a write to `memory/user/*` because the
|
|
165
|
+
// prompt never lists those paths.
|
|
166
|
+
const { systemPrompt, prompt } = resolveFlushPrompts({
|
|
167
|
+
scope: memory.scope,
|
|
168
|
+
});
|
|
169
|
+
const initialMessages = [
|
|
170
|
+
new SystemMessage(systemPrompt),
|
|
171
|
+
new HumanMessage(
|
|
172
|
+
`${prompt}\n\nConversation context:\n\n${conversationSummary}`
|
|
173
|
+
),
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const loop: FlushLoopResult = await runFlushLoop({
|
|
177
|
+
model: bound,
|
|
178
|
+
tools,
|
|
179
|
+
initialMessages,
|
|
180
|
+
maxIterations: maxIterations ?? DEFAULT_MAX_FLUSH_ITERATIONS,
|
|
181
|
+
onIteration: onIteration
|
|
182
|
+
? (ev): void =>
|
|
183
|
+
onIteration({
|
|
184
|
+
i: ev.i,
|
|
185
|
+
toolCallCount: ev.toolCalls.length,
|
|
186
|
+
toolNames: ev.toolCalls.map((c) => c.name),
|
|
187
|
+
})
|
|
188
|
+
: undefined,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
ran: true,
|
|
193
|
+
iterations: loop.iterations,
|
|
194
|
+
appendsAttempted: loop.appendsAttempted,
|
|
195
|
+
appendsSucceeded: loop.appendsSucceeded,
|
|
196
|
+
toolErrors: loop.toolErrors,
|
|
197
|
+
silentReply: loop.silentReply,
|
|
198
|
+
hitIterationCap: loop.hitIterationCap,
|
|
199
|
+
finalText: loop.finalText,
|
|
200
|
+
};
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return {
|
|
203
|
+
ran: false,
|
|
204
|
+
error: err instanceof Error ? err.message : String(err),
|
|
205
|
+
};
|
|
206
|
+
} finally {
|
|
207
|
+
setPhase(MEMORY_PHASE_NORMAL);
|
|
208
|
+
}
|
|
209
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export * from './splitStream';
|
|
|
5
5
|
export * from './events';
|
|
6
6
|
export * from './messages';
|
|
7
7
|
|
|
8
|
-
/* Observability types re-exported for
|
|
8
|
+
/* Observability types re-exported for host consumption */
|
|
9
9
|
export type {
|
|
10
10
|
GuardrailTraceData,
|
|
11
11
|
GuardrailOutcome,
|
|
@@ -25,6 +25,35 @@ export * from './tools/AskUser';
|
|
|
25
25
|
export * from './tools/schema';
|
|
26
26
|
export * from './tools/handlers';
|
|
27
27
|
export * from './tools/search';
|
|
28
|
+
export * from './tools/memory';
|
|
29
|
+
|
|
30
|
+
/* Memory (storage + factory) */
|
|
31
|
+
export * from './memory';
|
|
32
|
+
|
|
33
|
+
/* Prompts */
|
|
34
|
+
export { MEMORY_FLUSH_SYSTEM_PROMPT } from './prompts/memoryFlushPrompt';
|
|
35
|
+
|
|
36
|
+
/* Phases */
|
|
37
|
+
export {
|
|
38
|
+
shouldFlushMemory,
|
|
39
|
+
runMemoryFlush,
|
|
40
|
+
} from './graphs/phases/memoryFlushPhase';
|
|
41
|
+
export type {
|
|
42
|
+
RunFlushParams,
|
|
43
|
+
RunFlushResult,
|
|
44
|
+
} from './graphs/phases/memoryFlushPhase';
|
|
45
|
+
export {
|
|
46
|
+
runFlushLoop,
|
|
47
|
+
bindToolsIfSupported,
|
|
48
|
+
extractText as extractFlushText,
|
|
49
|
+
parseToolResult as parseFlushToolResult,
|
|
50
|
+
} from './graphs/phases/flushLoop';
|
|
51
|
+
export type {
|
|
52
|
+
FlushLoopParams,
|
|
53
|
+
FlushLoopResult,
|
|
54
|
+
ToolCallLike,
|
|
55
|
+
ToolErrorRecord,
|
|
56
|
+
} from './graphs/phases/flushLoop';
|
|
28
57
|
|
|
29
58
|
/* Schemas */
|
|
30
59
|
export * from './schemas';
|
package/src/llm/bedrock/index.ts
CHANGED
|
@@ -172,14 +172,13 @@ export class IllumaBedrockConverse extends ChatBedrockConverse {
|
|
|
172
172
|
* Some tools (e.g., MCP-sourced or dynamically created) may have empty or
|
|
173
173
|
* missing descriptions. Patch them here to avoid Bedrock validation errors.
|
|
174
174
|
*/
|
|
175
|
-
if (
|
|
176
|
-
params.toolConfig?.tools &&
|
|
177
|
-
Array.isArray(params.toolConfig.tools)
|
|
178
|
-
) {
|
|
175
|
+
if (params.toolConfig?.tools && Array.isArray(params.toolConfig.tools)) {
|
|
179
176
|
for (const t of params.toolConfig.tools) {
|
|
180
177
|
const spec = (t as { toolSpec?: { description?: string } }).toolSpec;
|
|
181
178
|
if (spec && (!spec.description || spec.description === '')) {
|
|
182
|
-
spec.description =
|
|
179
|
+
spec.description =
|
|
180
|
+
spec.description ||
|
|
181
|
+
`Tool: ${(spec as { name?: string }).name ?? 'unknown'}`;
|
|
183
182
|
}
|
|
184
183
|
}
|
|
185
184
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decorateCitations,
|
|
3
|
+
resolveMemoryCitationsMode,
|
|
4
|
+
shouldIncludeCitations,
|
|
5
|
+
type CitationCandidate,
|
|
6
|
+
} from '../citations';
|
|
7
|
+
|
|
8
|
+
describe('resolveMemoryCitationsMode', () => {
|
|
9
|
+
it('accepts on/off/auto', () => {
|
|
10
|
+
expect(resolveMemoryCitationsMode('on')).toBe('on');
|
|
11
|
+
expect(resolveMemoryCitationsMode('off')).toBe('off');
|
|
12
|
+
expect(resolveMemoryCitationsMode('auto')).toBe('auto');
|
|
13
|
+
});
|
|
14
|
+
it('falls back to auto for anything else', () => {
|
|
15
|
+
expect(resolveMemoryCitationsMode(undefined)).toBe('auto');
|
|
16
|
+
expect(resolveMemoryCitationsMode('nonsense')).toBe('auto');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('shouldIncludeCitations', () => {
|
|
21
|
+
it('on → true, off → false, auto → true in direct chat', () => {
|
|
22
|
+
expect(shouldIncludeCitations('on')).toBe(true);
|
|
23
|
+
expect(shouldIncludeCitations('off')).toBe(false);
|
|
24
|
+
expect(shouldIncludeCitations('auto')).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('decorateCitations', () => {
|
|
29
|
+
it('strips citation when include=false', () => {
|
|
30
|
+
const hits = [
|
|
31
|
+
{ path: 'memory/x.md', content: 'hello', citation: 'memory/x.md#L1' },
|
|
32
|
+
];
|
|
33
|
+
const out = decorateCitations(hits, false);
|
|
34
|
+
expect(out[0].citation).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('formats single-line citation as #L1', () => {
|
|
38
|
+
const hits: CitationCandidate[] = [
|
|
39
|
+
{ path: 'memory/x.md', content: 'hello' },
|
|
40
|
+
];
|
|
41
|
+
const out = decorateCitations(hits, true);
|
|
42
|
+
expect(out[0].citation).toBe('memory/x.md#L1');
|
|
43
|
+
expect(out[0].content).toContain('Source: memory/x.md#L1');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('formats multi-line citation as #L1-LN', () => {
|
|
47
|
+
const hits: CitationCandidate[] = [
|
|
48
|
+
{ path: 'memory/x.md', content: 'line1\nline2\nline3' },
|
|
49
|
+
];
|
|
50
|
+
const out = decorateCitations(hits, true);
|
|
51
|
+
expect(out[0].citation).toBe('memory/x.md#L1-L3');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('preserves caller-provided line range', () => {
|
|
55
|
+
const hits: CitationCandidate[] = [
|
|
56
|
+
{ path: 'memory/x.md', content: 'ignored', startLine: 5, endLine: 12 },
|
|
57
|
+
];
|
|
58
|
+
const out = decorateCitations(hits, true);
|
|
59
|
+
expect(out[0].citation).toBe('memory/x.md#L5-L12');
|
|
60
|
+
});
|
|
61
|
+
});
|