@illuma-ai/agents 1.1.28 → 1.3.1
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 +89 -45
- 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 +117 -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/errors.cjs +113 -0
- package/dist/cjs/utils/errors.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 +89 -45
- 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 +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/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/errors.mjs +109 -0
- package/dist/esm/utils/errors.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/errors.d.ts +37 -0
- package/dist/types/utils/events.d.ts +21 -0
- package/dist/types/utils/finishReasons.d.ts +32 -0
- package/dist/types/utils/index.d.ts +1 -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 +95 -61
- 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__/errors.test.ts +136 -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/errors.ts +115 -0
- package/src/utils/events.ts +37 -7
- package/src/utils/finishReasons.ts +40 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/llm.ts +0 -1
- package/src/utils/logging.ts +45 -8
- package/src/utils/toolCallNormalization.ts +271 -0
|
@@ -3,7 +3,7 @@ import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
|
3
3
|
import type * as _t from '@/types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Browser tool names - keep in sync with
|
|
6
|
+
* Browser tool names - keep in sync with the browser extension
|
|
7
7
|
* These tools execute locally in the browser extension, NOT on the server
|
|
8
8
|
*/
|
|
9
9
|
export const EBrowserTools = {
|
|
@@ -26,7 +26,7 @@ export type BrowserToolName =
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Callback function type for waiting on browser action results
|
|
29
|
-
* This allows the server
|
|
29
|
+
* This allows the host server to provide a callback that waits for the extension
|
|
30
30
|
* to POST results back to the server before returning to the LLM.
|
|
31
31
|
*
|
|
32
32
|
* @param action - The browser action (click, type, navigate, etc.)
|
|
@@ -87,9 +87,7 @@ const BrowserClickSchema = z.object({
|
|
|
87
87
|
label: z
|
|
88
88
|
.string()
|
|
89
89
|
.optional()
|
|
90
|
-
.describe(
|
|
91
|
-
'The fieldLabel or ariaLabel of the element, if available.'
|
|
92
|
-
),
|
|
90
|
+
.describe('The fieldLabel or ariaLabel of the element, if available.'),
|
|
93
91
|
});
|
|
94
92
|
|
|
95
93
|
const BrowserTypeSchema = z.object({
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -188,9 +188,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
188
188
|
// Resolve the effective execution context for this agent.
|
|
189
189
|
// Per-agent overrides take precedence — allows handoff agents to bypass HITL
|
|
190
190
|
// while the primary agent retains interactive approval.
|
|
191
|
-
const effectiveContext =
|
|
192
|
-
this.agentId && agentExecutionContextOverrides?.[this.agentId]
|
|
193
|
-
|
|
191
|
+
const effectiveContext =
|
|
192
|
+
(this.agentId && agentExecutionContextOverrides?.[this.agentId]) ??
|
|
193
|
+
executionContext;
|
|
194
194
|
|
|
195
195
|
// Scheduled executions bypass all approval checks — no user is present
|
|
196
196
|
if (effectiveContext === ExecutionContext.SCHEDULED) {
|
|
@@ -205,9 +205,21 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
205
205
|
return false;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
// Graph-managed tools (handoff/transfer
|
|
209
|
-
// these are internal routing mechanisms, not user-facing tool executions
|
|
210
|
-
|
|
208
|
+
// Graph-managed routing tools (handoff/transfer) bypass HITL approval —
|
|
209
|
+
// these are internal routing mechanisms, not user-facing tool executions.
|
|
210
|
+
//
|
|
211
|
+
// NOTE: `directToolNames` is used for two purposes — (1) marking tools that
|
|
212
|
+
// are loaded as full instances and don't need on-demand ON_TOOL_EXECUTE loading,
|
|
213
|
+
// and (2) bypassing HITL. In event-driven mode ALL built-in tools (including
|
|
214
|
+
// `ask_user`) end up in directToolNames for reason (1), so we cannot use
|
|
215
|
+
// `directToolNames.has(toolName)` as the HITL-bypass test — it would let
|
|
216
|
+
// `ask_user` execute without ever firing interrupt(), defeating the whole tool.
|
|
217
|
+
// Instead, gate the bypass on the actual routing-tool name prefix.
|
|
218
|
+
if (
|
|
219
|
+
this.directToolNames?.has(toolName) &&
|
|
220
|
+
(toolName.startsWith(Constants.LC_TRANSFER_TO_) ||
|
|
221
|
+
toolName.startsWith(Constants.LC_HANDOFF_TO_))
|
|
222
|
+
) {
|
|
211
223
|
return false;
|
|
212
224
|
}
|
|
213
225
|
|
|
@@ -240,10 +252,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
240
252
|
* @param config - The runnable config for event dispatch
|
|
241
253
|
* @returns The approval response from the human
|
|
242
254
|
*/
|
|
243
|
-
private requestApproval(
|
|
255
|
+
private async requestApproval(
|
|
244
256
|
call: ToolCall,
|
|
245
257
|
config: RunnableConfig
|
|
246
|
-
): t.ToolApprovalResponse {
|
|
258
|
+
): Promise<t.ToolApprovalResponse> {
|
|
247
259
|
const approvalRequest: t.ToolApprovalRequest = {
|
|
248
260
|
type: 'tool_approval_required',
|
|
249
261
|
toolCallId: call.id ?? '',
|
|
@@ -253,9 +265,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
253
265
|
description: `Tool "${call.name}" wants to execute with the provided arguments.`,
|
|
254
266
|
};
|
|
255
267
|
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
268
|
+
// MUST await — interrupt() throws GraphInterrupt synchronously which unwinds
|
|
269
|
+
// the call stack. Any un-awaited dispatch Promise is abandoned before the
|
|
270
|
+
// host's ON_TOOL_APPROVAL_REQUIRED handler runs, so the MongoDB row never
|
|
271
|
+
// gets written and the subsequent approve-tool POST 404s with
|
|
272
|
+
// "No pending approval found". Awaiting guarantees the handler has persisted
|
|
273
|
+
// the request before we suspend the graph.
|
|
274
|
+
await safeDispatchCustomEvent(
|
|
259
275
|
GraphEvents.ON_TOOL_APPROVAL_REQUIRED,
|
|
260
276
|
approvalRequest,
|
|
261
277
|
config
|
|
@@ -373,7 +389,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
373
389
|
if (
|
|
374
390
|
this.requiresApproval(call.name, call.args as Record<string, unknown>)
|
|
375
391
|
) {
|
|
376
|
-
const approvalResponse = this.requestApproval(call, config);
|
|
392
|
+
const approvalResponse = await this.requestApproval(call, config);
|
|
377
393
|
if (!approvalResponse.approved) {
|
|
378
394
|
// Human denied the tool call - return a denial message
|
|
379
395
|
return new ToolMessage({
|
|
@@ -825,7 +841,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
825
841
|
if (
|
|
826
842
|
this.requiresApproval(call.name, call.args as Record<string, unknown>)
|
|
827
843
|
) {
|
|
828
|
-
const approvalResponse = this.requestApproval(call, config);
|
|
844
|
+
const approvalResponse = await this.requestApproval(call, config);
|
|
829
845
|
if (!approvalResponse.approved) {
|
|
830
846
|
denialMessages.push(
|
|
831
847
|
new ToolMessage({
|
|
@@ -1152,6 +1168,127 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
1152
1168
|
}
|
|
1153
1169
|
}
|
|
1154
1170
|
|
|
1171
|
+
/**
|
|
1172
|
+
* Dedupe handoffs targeting the same destination.
|
|
1173
|
+
*
|
|
1174
|
+
* Parent LLMs sometimes emit multiple parallel transfer_to_<agent> tool
|
|
1175
|
+
* calls for the same child agent in one model tick (e.g. "send email 1"
|
|
1176
|
+
* and "send email 2" both routed to the Productivity Assistant). LangGraph
|
|
1177
|
+
* would otherwise create two parallel Sends into the same subgraph, which
|
|
1178
|
+
* produces duplicate handoff entries in the UI and leaves extra
|
|
1179
|
+
* tool_call_ids without a clean result when the single child run finishes.
|
|
1180
|
+
*
|
|
1181
|
+
* Merge same-destination handoffs into one Command whose update.messages
|
|
1182
|
+
* carries every original ToolMessage — the primary one has its
|
|
1183
|
+
* Instructions field rebuilt from the combined set, and the rest are kept
|
|
1184
|
+
* verbatim so the parent message history has a ToolMessage for every
|
|
1185
|
+
* tool_call_id (LangChain requires every AI tool_call to be resolved).
|
|
1186
|
+
* The child's _extractTransferContext filters all transfer ToolMessages
|
|
1187
|
+
* out of the child's view, so it only ever sees the merged instructions.
|
|
1188
|
+
*/
|
|
1189
|
+
if (handoffCommands.length > 1) {
|
|
1190
|
+
const byDestination = new Map<string, Command[]>();
|
|
1191
|
+
for (const cmd of handoffCommands) {
|
|
1192
|
+
const goto = cmd.goto;
|
|
1193
|
+
const dest = typeof goto === 'string' ? goto : (goto as string[])[0];
|
|
1194
|
+
const arr = byDestination.get(dest) ?? [];
|
|
1195
|
+
arr.push(cmd);
|
|
1196
|
+
byDestination.set(dest, arr);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const hasDuplicates = Array.from(byDestination.values()).some(
|
|
1200
|
+
(arr) => arr.length > 1
|
|
1201
|
+
);
|
|
1202
|
+
|
|
1203
|
+
if (hasDuplicates) {
|
|
1204
|
+
const TRANSFER_INSTRUCTIONS_PATTERN =
|
|
1205
|
+
/(?:Instructions?|Context):\s*(.+)/is;
|
|
1206
|
+
const mergedHandoffs: Command[] = [];
|
|
1207
|
+
|
|
1208
|
+
for (const [dest, cmds] of byDestination) {
|
|
1209
|
+
if (cmds.length === 1) {
|
|
1210
|
+
mergedHandoffs.push(cmds[0]);
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const allToolMessages: ToolMessage[] = [];
|
|
1215
|
+
const allOtherMessages: BaseMessage[] = [];
|
|
1216
|
+
for (const cmd of cmds) {
|
|
1217
|
+
const upd = cmd.update as { messages?: BaseMessage[] } | undefined;
|
|
1218
|
+
for (const m of upd?.messages ?? []) {
|
|
1219
|
+
if (m.getType() === 'tool') {
|
|
1220
|
+
allToolMessages.push(m as ToolMessage);
|
|
1221
|
+
} else {
|
|
1222
|
+
allOtherMessages.push(m);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (allToolMessages.length === 0) {
|
|
1228
|
+
mergedHandoffs.push(cmds[0]);
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
const primary = allToolMessages[0];
|
|
1233
|
+
const primaryContent =
|
|
1234
|
+
typeof primary.content === 'string'
|
|
1235
|
+
? primary.content
|
|
1236
|
+
: JSON.stringify(primary.content);
|
|
1237
|
+
|
|
1238
|
+
const mergedInstructions: string[] = [];
|
|
1239
|
+
for (const tm of allToolMessages) {
|
|
1240
|
+
const c =
|
|
1241
|
+
typeof tm.content === 'string'
|
|
1242
|
+
? tm.content
|
|
1243
|
+
: JSON.stringify(tm.content);
|
|
1244
|
+
const match = c.match(TRANSFER_INSTRUCTIONS_PATTERN);
|
|
1245
|
+
if (match?.[1]) {
|
|
1246
|
+
const instr = match[1].trim();
|
|
1247
|
+
if (instr && !mergedInstructions.includes(instr)) {
|
|
1248
|
+
mergedInstructions.push(instr);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
let mergedPrimaryContent = primaryContent;
|
|
1254
|
+
if (mergedInstructions.length > 1) {
|
|
1255
|
+
const header = primaryContent
|
|
1256
|
+
.replace(TRANSFER_INSTRUCTIONS_PATTERN, '')
|
|
1257
|
+
.trimEnd();
|
|
1258
|
+
const combinedInstr = mergedInstructions
|
|
1259
|
+
.map((s, i) => `${i + 1}. ${s}`)
|
|
1260
|
+
.join('\n');
|
|
1261
|
+
mergedPrimaryContent =
|
|
1262
|
+
`${header}\nInstructions: ${combinedInstr}`.trim();
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const mergedPrimary = new ToolMessage({
|
|
1266
|
+
content: mergedPrimaryContent,
|
|
1267
|
+
tool_call_id: primary.tool_call_id,
|
|
1268
|
+
name: primary.name,
|
|
1269
|
+
additional_kwargs: { ...primary.additional_kwargs },
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
const mergedMessages: BaseMessage[] = [
|
|
1273
|
+
mergedPrimary,
|
|
1274
|
+
...allToolMessages.slice(1),
|
|
1275
|
+
...allOtherMessages,
|
|
1276
|
+
];
|
|
1277
|
+
|
|
1278
|
+
mergedHandoffs.push(
|
|
1279
|
+
new Command({
|
|
1280
|
+
graph: Command.PARENT,
|
|
1281
|
+
goto: dest,
|
|
1282
|
+
update: { messages: mergedMessages },
|
|
1283
|
+
})
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
handoffCommands.length = 0;
|
|
1288
|
+
handoffCommands.push(...mergedHandoffs);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1155
1292
|
/**
|
|
1156
1293
|
* Handle handoff commands - convert to Send objects for parallel execution
|
|
1157
1294
|
* when multiple handoffs are requested
|
|
@@ -86,7 +86,10 @@ function createTestGraph(
|
|
|
86
86
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
87
|
compiled: any;
|
|
88
88
|
notifications: t.ToolApprovalNotification[];
|
|
89
|
-
config: Partial<RunnableConfig> & {
|
|
89
|
+
config: Partial<RunnableConfig> & {
|
|
90
|
+
version: 'v1' | 'v2';
|
|
91
|
+
configurable: { thread_id: string };
|
|
92
|
+
};
|
|
90
93
|
} {
|
|
91
94
|
const StateAnnotation = Annotation.Root({
|
|
92
95
|
messages: Annotation<BaseMessage[]>({
|
|
@@ -145,7 +148,10 @@ function createTestGraph(
|
|
|
145
148
|
const notifications: t.ToolApprovalNotification[] = [];
|
|
146
149
|
|
|
147
150
|
// Collect notification events (data-only, no resolve/reject)
|
|
148
|
-
const config: Partial<RunnableConfig> & {
|
|
151
|
+
const config: Partial<RunnableConfig> & {
|
|
152
|
+
version: 'v1' | 'v2';
|
|
153
|
+
configurable: { thread_id: string };
|
|
154
|
+
} = {
|
|
149
155
|
version: 'v2',
|
|
150
156
|
configurable: {
|
|
151
157
|
thread_id: `test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
@@ -434,7 +440,12 @@ describe('HITL Tool Approval - Interrupt/Resume Flow', () => {
|
|
|
434
440
|
|
|
435
441
|
// Resume with denial
|
|
436
442
|
const result = await compiled.invoke(
|
|
437
|
-
new Command({
|
|
443
|
+
new Command({
|
|
444
|
+
resume: {
|
|
445
|
+
approved: false,
|
|
446
|
+
feedback: 'Not now',
|
|
447
|
+
} as t.ToolApprovalResponse,
|
|
448
|
+
}),
|
|
438
449
|
config
|
|
439
450
|
);
|
|
440
451
|
|
|
@@ -742,7 +753,10 @@ describe('HITL Tool Approval - Interrupt/Resume Flow', () => {
|
|
|
742
753
|
const checkpointer = new MemorySaver();
|
|
743
754
|
const compiled = workflow.compile({ checkpointer });
|
|
744
755
|
|
|
745
|
-
const config: Partial<RunnableConfig> & {
|
|
756
|
+
const config: Partial<RunnableConfig> & {
|
|
757
|
+
version: 'v1' | 'v2';
|
|
758
|
+
configurable: { thread_id: string };
|
|
759
|
+
} = {
|
|
746
760
|
version: 'v2',
|
|
747
761
|
configurable: {
|
|
748
762
|
thread_id: `test-multi-${Date.now()}`,
|
|
@@ -762,10 +776,7 @@ describe('HITL Tool Approval - Interrupt/Resume Flow', () => {
|
|
|
762
776
|
};
|
|
763
777
|
|
|
764
778
|
// First invoke: hits first interrupt
|
|
765
|
-
await compiled.invoke(
|
|
766
|
-
{ messages: [new HumanMessage('Do both')] },
|
|
767
|
-
config
|
|
768
|
-
);
|
|
779
|
+
await compiled.invoke({ messages: [new HumanMessage('Do both')] }, config);
|
|
769
780
|
|
|
770
781
|
// First interrupt notification received
|
|
771
782
|
expect(notifications.length).toBeGreaterThanOrEqual(1);
|
|
@@ -784,7 +795,9 @@ describe('HITL Tool Approval - Interrupt/Resume Flow', () => {
|
|
|
784
795
|
|
|
785
796
|
// Notifications fire on every re-execution (interrupt() replays the node),
|
|
786
797
|
// so we check unique tool names rather than total count.
|
|
787
|
-
const uniqueTools = [
|
|
798
|
+
const uniqueTools = [
|
|
799
|
+
...new Set(notifications.map((e) => e.toolName)),
|
|
800
|
+
].sort();
|
|
788
801
|
expect(uniqueTools).toEqual(['echo', 'send_email']);
|
|
789
802
|
});
|
|
790
803
|
});
|
|
@@ -63,7 +63,7 @@ describe('HITL Approval Constants', () => {
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
describe('MCP_DELIMITER', () => {
|
|
66
|
-
it('should match
|
|
66
|
+
it('should match the host data-provider mcp_delimiter constant', () => {
|
|
67
67
|
expect(MCP_DELIMITER).toBe('_mcp_');
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared HITL (Human-in-the-Loop) constants and enums.
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for approval-related values consumed by
|
|
5
|
-
* the agents library,
|
|
5
|
+
* the agents library, host backend, and browser extension.
|
|
6
6
|
*
|
|
7
7
|
* @module approval/constants
|
|
8
8
|
*/
|
|
@@ -108,6 +108,6 @@ export enum ActionCategory {
|
|
|
108
108
|
*
|
|
109
109
|
* Example: `send_email_mcp_outlook` → tool = `send_email`, server = `outlook`
|
|
110
110
|
*
|
|
111
|
-
* Must match
|
|
111
|
+
* Must match the host data-provider delimiter constant.
|
|
112
112
|
*/
|
|
113
113
|
export const MCP_DELIMITER = '_mcp_';
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the memory tool family.
|
|
3
|
+
* Exercise the read path, the phase gate on append, and the isolation
|
|
4
|
+
* closure — all through the LangChain tool interface, against a mock backend.
|
|
5
|
+
*/
|
|
6
|
+
import { MockMemoryBackend } from '@/memory/__tests__/mockBackend';
|
|
7
|
+
import { buildMemoryTools } from '../index';
|
|
8
|
+
import {
|
|
9
|
+
MEMORY_APPEND_TOOL_NAME,
|
|
10
|
+
MEMORY_GET_TOOL_NAME,
|
|
11
|
+
MEMORY_PHASE_FLUSHING,
|
|
12
|
+
MEMORY_PHASE_NORMAL,
|
|
13
|
+
MEMORY_SEARCH_TOOL_NAME,
|
|
14
|
+
} from '@/memory/constants';
|
|
15
|
+
import type { MemoryPhase } from '@/memory/types';
|
|
16
|
+
|
|
17
|
+
describe('buildMemoryTools', () => {
|
|
18
|
+
const scope = { agentId: 'sales', userId: 'alice' };
|
|
19
|
+
|
|
20
|
+
async function callTool(t: unknown, args: Record<string, unknown>) {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
return (t as any).invoke(args) as Promise<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
it('exposes memory_search and memory_get in readOnly mode', () => {
|
|
26
|
+
const backend = new MockMemoryBackend();
|
|
27
|
+
const tools = buildMemoryTools({ backend, scope, readOnly: true });
|
|
28
|
+
const names = tools.map((t) => (t as { name: string }).name);
|
|
29
|
+
expect(names).toEqual([MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('attaches memory_append when not readOnly', () => {
|
|
33
|
+
const backend = new MockMemoryBackend();
|
|
34
|
+
let phase: MemoryPhase = MEMORY_PHASE_NORMAL;
|
|
35
|
+
const tools = buildMemoryTools({
|
|
36
|
+
backend,
|
|
37
|
+
scope,
|
|
38
|
+
getPhase: () => phase,
|
|
39
|
+
});
|
|
40
|
+
const names = tools.map((t) => (t as { name: string }).name);
|
|
41
|
+
expect(names).toContain(MEMORY_APPEND_TOOL_NAME);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('memory_search returns hits from the agent workspace only', async () => {
|
|
45
|
+
const backend = new MockMemoryBackend();
|
|
46
|
+
await backend.append(scope, {
|
|
47
|
+
path: 'memory/agent/playbook.md',
|
|
48
|
+
content: 'chose Postgres over Mongo for vector search',
|
|
49
|
+
});
|
|
50
|
+
await backend.append(
|
|
51
|
+
{ agentId: 'support', userId: 'alice' },
|
|
52
|
+
{ path: 'memory/agent/playbook.md', content: 'unrelated support note' }
|
|
53
|
+
);
|
|
54
|
+
const [search] = buildMemoryTools({ backend, scope, readOnly: true });
|
|
55
|
+
const result = JSON.parse(await callTool(search, { query: 'Postgres' }));
|
|
56
|
+
expect(result.results).toHaveLength(1);
|
|
57
|
+
expect(result.results[0].content).toContain('Postgres');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('different user of same agent sees shared workspace', async () => {
|
|
61
|
+
const backend = new MockMemoryBackend();
|
|
62
|
+
// Alice writes in her session.
|
|
63
|
+
await backend.append(
|
|
64
|
+
{ agentId: 'sales', userId: 'alice' },
|
|
65
|
+
{
|
|
66
|
+
path: 'memory/agent/playbook.md',
|
|
67
|
+
content: 'Alice learned pricing tiers',
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
// Bob searches in his session — same agent, shared workspace.
|
|
71
|
+
const [search] = buildMemoryTools({
|
|
72
|
+
backend,
|
|
73
|
+
scope: { agentId: 'sales', userId: 'bob' },
|
|
74
|
+
readOnly: true,
|
|
75
|
+
});
|
|
76
|
+
const result = JSON.parse(await callTool(search, { query: 'pricing' }));
|
|
77
|
+
expect(result.results).toHaveLength(1);
|
|
78
|
+
expect(result.results[0].content).toContain('pricing');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('memory_append rejects outside memory_flushing phase', async () => {
|
|
82
|
+
const backend = new MockMemoryBackend();
|
|
83
|
+
const tools = buildMemoryTools({
|
|
84
|
+
backend,
|
|
85
|
+
scope,
|
|
86
|
+
getPhase: () => MEMORY_PHASE_NORMAL,
|
|
87
|
+
});
|
|
88
|
+
const append = tools.find(
|
|
89
|
+
(t) => (t as { name: string }).name === MEMORY_APPEND_TOOL_NAME
|
|
90
|
+
)!;
|
|
91
|
+
const result = JSON.parse(
|
|
92
|
+
await callTool(append, {
|
|
93
|
+
path: 'memory/agent/playbook.md',
|
|
94
|
+
content: 'hello',
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
expect(result.ok).toBe(false);
|
|
98
|
+
expect(result.error).toMatch(/reflection/);
|
|
99
|
+
expect(backend.allRows()).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('memory_append persists during memory_flushing phase', async () => {
|
|
103
|
+
const backend = new MockMemoryBackend();
|
|
104
|
+
const phase: MemoryPhase = MEMORY_PHASE_FLUSHING;
|
|
105
|
+
const tools = buildMemoryTools({
|
|
106
|
+
backend,
|
|
107
|
+
scope,
|
|
108
|
+
getPhase: () => phase,
|
|
109
|
+
});
|
|
110
|
+
const append = tools.find(
|
|
111
|
+
(t) => (t as { name: string }).name === MEMORY_APPEND_TOOL_NAME
|
|
112
|
+
)!;
|
|
113
|
+
const result = JSON.parse(
|
|
114
|
+
await callTool(append, {
|
|
115
|
+
path: 'memory/agent/playbook.md',
|
|
116
|
+
content: 'I decided to use pgvector.',
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
expect(result.ok).toBe(true);
|
|
120
|
+
expect(backend.allRows()).toHaveLength(1);
|
|
121
|
+
// Agent-tier row — scoping user_id is NULL, latest writer recorded
|
|
122
|
+
// as provenance via lastUserId.
|
|
123
|
+
expect(backend.allRows()[0].userId).toBeNull();
|
|
124
|
+
expect(backend.allRows()[0].lastUserId).toBe('alice');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('memory_append merges multiple notes into one row per path', async () => {
|
|
128
|
+
const backend = new MockMemoryBackend();
|
|
129
|
+
const tools = buildMemoryTools({
|
|
130
|
+
backend,
|
|
131
|
+
scope,
|
|
132
|
+
getPhase: () => MEMORY_PHASE_FLUSHING,
|
|
133
|
+
});
|
|
134
|
+
const append = tools.find(
|
|
135
|
+
(t) => (t as { name: string }).name === MEMORY_APPEND_TOOL_NAME
|
|
136
|
+
)!;
|
|
137
|
+
await callTool(append, {
|
|
138
|
+
path: 'memory/agent/playbook.md',
|
|
139
|
+
content: 'first note',
|
|
140
|
+
});
|
|
141
|
+
await callTool(append, {
|
|
142
|
+
path: 'memory/agent/playbook.md',
|
|
143
|
+
content: 'second note',
|
|
144
|
+
});
|
|
145
|
+
const rows = backend.allRows();
|
|
146
|
+
expect(rows).toHaveLength(1);
|
|
147
|
+
expect(rows[0].content).toContain('first note');
|
|
148
|
+
expect(rows[0].content).toContain('second note');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('memory_append rejects paths outside memory/', async () => {
|
|
152
|
+
const backend = new MockMemoryBackend();
|
|
153
|
+
const tools = buildMemoryTools({
|
|
154
|
+
backend,
|
|
155
|
+
scope,
|
|
156
|
+
getPhase: () => MEMORY_PHASE_FLUSHING,
|
|
157
|
+
});
|
|
158
|
+
const append = tools.find(
|
|
159
|
+
(t) => (t as { name: string }).name === MEMORY_APPEND_TOOL_NAME
|
|
160
|
+
)!;
|
|
161
|
+
const result = JSON.parse(
|
|
162
|
+
await callTool(append, {
|
|
163
|
+
path: 'secret.md',
|
|
164
|
+
content: 'escape',
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
expect(result.ok).toBe(false);
|
|
168
|
+
expect(result.error).toMatch(/memory\//);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('memory_append rejects non-whitelisted paths even with memory/ prefix', async () => {
|
|
172
|
+
const backend = new MockMemoryBackend();
|
|
173
|
+
const tools = buildMemoryTools({
|
|
174
|
+
backend,
|
|
175
|
+
scope,
|
|
176
|
+
getPhase: () => MEMORY_PHASE_FLUSHING,
|
|
177
|
+
});
|
|
178
|
+
const append = tools.find(
|
|
179
|
+
(t) => (t as { name: string }).name === MEMORY_APPEND_TOOL_NAME
|
|
180
|
+
)!;
|
|
181
|
+
const result = JSON.parse(
|
|
182
|
+
await callTool(append, {
|
|
183
|
+
path: 'memory/random-notes.md',
|
|
184
|
+
content: 'escape',
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
expect(result.ok).toBe(false);
|
|
188
|
+
expect(result.error).toMatch(/whitelist/i);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('tool args cannot inject agent_id into scope', async () => {
|
|
192
|
+
const backend = new MockMemoryBackend();
|
|
193
|
+
await backend.append(
|
|
194
|
+
{ agentId: 'victim', userId: 'alice' },
|
|
195
|
+
{ path: 'memory/agent/playbook.md', content: 'secret data' }
|
|
196
|
+
);
|
|
197
|
+
const [search] = buildMemoryTools({ backend, scope, readOnly: true });
|
|
198
|
+
// Try to pass a forged agent_id — Zod strips it, closure wins.
|
|
199
|
+
const result = JSON.parse(
|
|
200
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
201
|
+
await callTool(search, { query: 'secret', agent_id: 'victim' } as any)
|
|
202
|
+
);
|
|
203
|
+
expect(result.results).toHaveLength(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry for the memory tool family.
|
|
3
|
+
*
|
|
4
|
+
* `buildMemoryTools` is the one function host calls. It returns an array
|
|
5
|
+
* of LangChain tools with scope captured in a closure — the LLM cannot
|
|
6
|
+
* override the `(agentId, userId)` pair through any tool argument.
|
|
7
|
+
*
|
|
8
|
+
* Phase 2: accepts independent `readEnabled` / `writeEnabled` flags that
|
|
9
|
+
* mirror host's existing `agent.memory_read_enabled` /
|
|
10
|
+
* `agent.memory_write_enabled` fields. Either flag can be set without the
|
|
11
|
+
* other: a read-only agent can consult memory without mutating it; a
|
|
12
|
+
* write-only agent can reflect without reading.
|
|
13
|
+
*/
|
|
14
|
+
import type { StructuredToolInterface } from '@langchain/core/tools';
|
|
15
|
+
import type { MemoryConfig, MemoryPhase } from '@/memory/types';
|
|
16
|
+
import { createMemorySearchTool } from './memorySearchTool';
|
|
17
|
+
import { createMemoryGetTool } from './memoryGetTool';
|
|
18
|
+
import { createMemoryAppendTool } from './memoryAppendTool';
|
|
19
|
+
import type { MemoryToolBinding } from './shared';
|
|
20
|
+
|
|
21
|
+
export * from './shared';
|
|
22
|
+
export { createMemorySearchTool } from './memorySearchTool';
|
|
23
|
+
export { createMemoryGetTool } from './memoryGetTool';
|
|
24
|
+
export { createMemoryAppendTool } from './memoryAppendTool';
|
|
25
|
+
|
|
26
|
+
export interface BuildMemoryToolsOptions extends MemoryConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Attach `memory_search` + `memory_get`. Maps to
|
|
29
|
+
* `agent.memory_read_enabled` on the host Agent document.
|
|
30
|
+
*/
|
|
31
|
+
readEnabled?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Attach `memory_append` (still phase-gated to `memory_flushing`).
|
|
34
|
+
* Maps to `agent.memory_write_enabled` on the host Agent document.
|
|
35
|
+
*/
|
|
36
|
+
writeEnabled?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* @deprecated — legacy Phase 1 flag. When set, equivalent to
|
|
39
|
+
* `{ readEnabled: true, writeEnabled: false }`. Kept so existing tests
|
|
40
|
+
* don't break during the Phase 1→2 transition. New callers should pass
|
|
41
|
+
* `readEnabled`/`writeEnabled` explicitly.
|
|
42
|
+
*/
|
|
43
|
+
readOnly?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildMemoryTools(
|
|
47
|
+
options: BuildMemoryToolsOptions
|
|
48
|
+
): StructuredToolInterface[] {
|
|
49
|
+
// Back-compat for the Phase 1 readOnly flag — maps to read-only mode.
|
|
50
|
+
let readEnabled = options.readEnabled;
|
|
51
|
+
let writeEnabled = options.writeEnabled;
|
|
52
|
+
if (options.readOnly) {
|
|
53
|
+
readEnabled = true;
|
|
54
|
+
writeEnabled = false;
|
|
55
|
+
}
|
|
56
|
+
// Default: if neither flag supplied, attach both (back-compat with the
|
|
57
|
+
// pre-Phase-2 single-flag caller).
|
|
58
|
+
if (readEnabled === undefined && writeEnabled === undefined) {
|
|
59
|
+
readEnabled = true;
|
|
60
|
+
writeEnabled = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const binding: MemoryToolBinding = {
|
|
64
|
+
backend: options.backend,
|
|
65
|
+
scope: options.scope,
|
|
66
|
+
maxInjectedChars: options.search?.maxInjectedChars,
|
|
67
|
+
searchOptions: {
|
|
68
|
+
mmr: options.search?.mmr,
|
|
69
|
+
temporalDecay: options.search?.temporalDecay,
|
|
70
|
+
citations: options.search?.citations,
|
|
71
|
+
},
|
|
72
|
+
recallTracker: options.recallTracker,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const tools: StructuredToolInterface[] = [];
|
|
76
|
+
|
|
77
|
+
if (readEnabled) {
|
|
78
|
+
tools.push(
|
|
79
|
+
createMemorySearchTool(binding) as unknown as StructuredToolInterface,
|
|
80
|
+
createMemoryGetTool(binding) as unknown as StructuredToolInterface
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (writeEnabled) {
|
|
85
|
+
const getPhase: () => MemoryPhase =
|
|
86
|
+
options.getPhase ?? ((): MemoryPhase => 'normal');
|
|
87
|
+
tools.push(
|
|
88
|
+
createMemoryAppendTool({
|
|
89
|
+
...binding,
|
|
90
|
+
getPhase,
|
|
91
|
+
}) as unknown as StructuredToolInterface
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return tools;
|
|
96
|
+
}
|