@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
package/src/stream.ts
CHANGED
|
@@ -741,7 +741,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
741
741
|
const messageDelta = data as t.MessageDeltaEvent;
|
|
742
742
|
const runStep = stepMap.get(messageDelta.id);
|
|
743
743
|
if (!runStep) {
|
|
744
|
-
|
|
744
|
+
// Expected in handoff subgraphs — reasoning/message deltas can arrive before ON_RUN_STEP
|
|
745
745
|
return;
|
|
746
746
|
}
|
|
747
747
|
|
|
@@ -765,7 +765,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
765
765
|
const reasoningDelta = data as t.ReasoningDeltaEvent;
|
|
766
766
|
const runStep = stepMap.get(reasoningDelta.id);
|
|
767
767
|
if (!runStep) {
|
|
768
|
-
|
|
768
|
+
// Expected in handoff subgraphs — reasoning deltas arrive before ON_RUN_STEP
|
|
769
769
|
return;
|
|
770
770
|
}
|
|
771
771
|
|
|
@@ -786,7 +786,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
786
786
|
|
|
787
787
|
const runStep = stepMap.get(runStepDelta.id);
|
|
788
788
|
if (!runStep) {
|
|
789
|
-
|
|
789
|
+
// Expected in handoff subgraphs — step deltas can arrive before ON_RUN_STEP
|
|
790
790
|
return;
|
|
791
791
|
}
|
|
792
792
|
|
|
@@ -838,9 +838,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
838
838
|
|
|
839
839
|
const runStep = stepMap.get(stepId);
|
|
840
840
|
if (!runStep) {
|
|
841
|
-
|
|
842
|
-
'No run step or runId found for completed tool call event'
|
|
843
|
-
);
|
|
841
|
+
// Expected in handoff subgraphs — completion can arrive for untracked steps
|
|
844
842
|
return;
|
|
845
843
|
}
|
|
846
844
|
|
package/src/tools/AskUser.ts
CHANGED
|
@@ -7,7 +7,12 @@ export const AskUserToolName = Constants.ASK_USER;
|
|
|
7
7
|
export const AskUserDescription =
|
|
8
8
|
'Ask the user clarification questions with structured options before taking action. ' +
|
|
9
9
|
'For a single key question, use question+options. For multiple related questions, use steps[] (max 3 steps). ' +
|
|
10
|
-
'Each step can be single-select or multi-select. After receiving answers, proceed immediately.'
|
|
10
|
+
'Each step can be single-select or multi-select. After receiving answers, proceed immediately. ' +
|
|
11
|
+
'BEFORE calling this tool, ALWAYS emit one short sentence of plain-text output (one line, <= 20 words) ' +
|
|
12
|
+
'explaining why you need more information — e.g. "I need a few details to help with that.". ' +
|
|
13
|
+
'NEVER include an option like "I\'ll give it myself", "Other", "Something else", "Custom", "None of the above", ' +
|
|
14
|
+
'or any equivalent free-text escape hatch — the UI already provides a dedicated "Something else" button ' +
|
|
15
|
+
'for free-text responses. Only list concrete, mutually-exclusive choices.';
|
|
11
16
|
|
|
12
17
|
const AskUserOptionSchema = z.object({
|
|
13
18
|
label: z.string().describe('Short display label'),
|
|
@@ -68,7 +73,7 @@ const AskUserSchema = z
|
|
|
68
73
|
|
|
69
74
|
/**
|
|
70
75
|
* Represents a single step selection from the multi-step wizard UI.
|
|
71
|
-
* Exported for use by both
|
|
76
|
+
* Exported for use by both the host web client and browser extension.
|
|
72
77
|
*/
|
|
73
78
|
export type StepSelection = {
|
|
74
79
|
values: string[];
|
|
@@ -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
|
});
|
|
@@ -14,10 +14,10 @@ describe('HITL Approval Constants', () => {
|
|
|
14
14
|
expect(ExecutionContext.SCHEDULED).toBe('scheduled');
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
it('should only have
|
|
17
|
+
it('should only have 3 values (interactive, scheduled, handoff)', () => {
|
|
18
18
|
const values = Object.values(ExecutionContext);
|
|
19
|
-
expect(values).toHaveLength(
|
|
20
|
-
expect(values).toEqual(['interactive', 'scheduled']);
|
|
19
|
+
expect(values).toHaveLength(3);
|
|
20
|
+
expect(values).toEqual(['interactive', 'scheduled', 'handoff']);
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
23
|
|
|
@@ -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
|
+
});
|