@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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memory_append` — the reflection-phase-only write tool.
|
|
3
|
+
*
|
|
4
|
+
* New (not present in upstream verbatim). Upstream enforces append-only via
|
|
5
|
+
* `wrapToolMemoryFlushAppendOnlyWrite`; we combine the tool and the phase
|
|
6
|
+
* gate in one place because the agents library has a simpler graph state.
|
|
7
|
+
*
|
|
8
|
+
* Behaviour:
|
|
9
|
+
* - Outside `memory_flushing` phase: returns an error object instead of
|
|
10
|
+
* calling the backend. The LLM sees this and stops trying.
|
|
11
|
+
* - Inside `memory_flushing`: persists the note via {@link MemoryBackend.append}.
|
|
12
|
+
* - Hard-caps total appends per flush (protects against runaway writes).
|
|
13
|
+
*/
|
|
14
|
+
import { tool } from '@langchain/core/tools';
|
|
15
|
+
import {
|
|
16
|
+
DEFAULT_MAX_APPENDS_PER_FLUSH,
|
|
17
|
+
MEMORY_APPEND_DESCRIPTION,
|
|
18
|
+
MEMORY_APPEND_TOOL_NAME,
|
|
19
|
+
MEMORY_PHASE_FLUSHING,
|
|
20
|
+
} from '@/memory/constants';
|
|
21
|
+
import type { MemoryPhase } from '@/memory/types';
|
|
22
|
+
import {
|
|
23
|
+
MemoryAppendSchema,
|
|
24
|
+
toAppendInput,
|
|
25
|
+
type MemoryToolBinding,
|
|
26
|
+
} from './shared';
|
|
27
|
+
|
|
28
|
+
export interface MemoryAppendBinding extends MemoryToolBinding {
|
|
29
|
+
/**
|
|
30
|
+
* Reads the current phase at call-time. Required — without it the append
|
|
31
|
+
* tool rejects every call. Supplied by the graph runtime.
|
|
32
|
+
*/
|
|
33
|
+
getPhase: () => MemoryPhase;
|
|
34
|
+
/** Hard cap on appends per flush phase. Default 20. */
|
|
35
|
+
maxAppendsPerFlush?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface AppendResult {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
path?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
45
|
+
export function createMemoryAppendTool(binding: MemoryAppendBinding) {
|
|
46
|
+
const maxAppends =
|
|
47
|
+
binding.maxAppendsPerFlush ?? DEFAULT_MAX_APPENDS_PER_FLUSH;
|
|
48
|
+
let appendsInCurrentFlush = 0;
|
|
49
|
+
let lastSeenPhase: MemoryPhase = 'normal';
|
|
50
|
+
|
|
51
|
+
return tool(
|
|
52
|
+
async (args): Promise<string> => {
|
|
53
|
+
const phase = binding.getPhase();
|
|
54
|
+
|
|
55
|
+
// Reset counter on entry to a new flush.
|
|
56
|
+
if (
|
|
57
|
+
phase === MEMORY_PHASE_FLUSHING &&
|
|
58
|
+
lastSeenPhase !== MEMORY_PHASE_FLUSHING
|
|
59
|
+
) {
|
|
60
|
+
appendsInCurrentFlush = 0;
|
|
61
|
+
}
|
|
62
|
+
lastSeenPhase = phase;
|
|
63
|
+
|
|
64
|
+
if (phase !== MEMORY_PHASE_FLUSHING) {
|
|
65
|
+
const result: AppendResult = {
|
|
66
|
+
ok: false,
|
|
67
|
+
error:
|
|
68
|
+
'memory_append can only be called during the reflection (memory_flushing) phase',
|
|
69
|
+
};
|
|
70
|
+
return JSON.stringify(result);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (appendsInCurrentFlush >= maxAppends) {
|
|
74
|
+
const result: AppendResult = {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: `memory_append hit the per-flush cap of ${maxAppends} notes`,
|
|
77
|
+
};
|
|
78
|
+
return JSON.stringify(result);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const input = toAppendInput(args);
|
|
83
|
+
await binding.backend.append(binding.scope, input);
|
|
84
|
+
appendsInCurrentFlush += 1;
|
|
85
|
+
const result: AppendResult = { ok: true, path: input.path };
|
|
86
|
+
return JSON.stringify(result);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const result: AppendResult = {
|
|
89
|
+
ok: false,
|
|
90
|
+
error: err instanceof Error ? err.message : String(err),
|
|
91
|
+
};
|
|
92
|
+
return JSON.stringify(result);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: MEMORY_APPEND_TOOL_NAME,
|
|
97
|
+
description: MEMORY_APPEND_DESCRIPTION,
|
|
98
|
+
schema: MemoryAppendSchema,
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memory_get` — safe snippet read.
|
|
3
|
+
*
|
|
4
|
+
* Port of upstream `createMemoryGetTool` at
|
|
5
|
+
* `upstream reference`.
|
|
6
|
+
*/
|
|
7
|
+
import { tool } from '@langchain/core/tools';
|
|
8
|
+
import {
|
|
9
|
+
MEMORY_GET_DESCRIPTION,
|
|
10
|
+
MEMORY_GET_TOOL_NAME,
|
|
11
|
+
} from '@/memory/constants';
|
|
12
|
+
import {
|
|
13
|
+
MemoryGetSchema,
|
|
14
|
+
type MemoryGetToolResult,
|
|
15
|
+
type MemoryToolBinding,
|
|
16
|
+
} from './shared';
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
19
|
+
export function createMemoryGetTool(binding: MemoryToolBinding) {
|
|
20
|
+
return tool(
|
|
21
|
+
async (args): Promise<string> => {
|
|
22
|
+
try {
|
|
23
|
+
const result = await binding.backend.get(binding.scope, {
|
|
24
|
+
path: args.path,
|
|
25
|
+
from: args.from,
|
|
26
|
+
lines: args.lines,
|
|
27
|
+
});
|
|
28
|
+
if (!result) {
|
|
29
|
+
const payload: MemoryGetToolResult = { path: args.path, text: '' };
|
|
30
|
+
return JSON.stringify(payload);
|
|
31
|
+
}
|
|
32
|
+
const payload: MemoryGetToolResult = {
|
|
33
|
+
path: result.path,
|
|
34
|
+
text: result.text,
|
|
35
|
+
};
|
|
36
|
+
return JSON.stringify(payload);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const payload: MemoryGetToolResult = {
|
|
39
|
+
path: args.path,
|
|
40
|
+
text: '',
|
|
41
|
+
disabled: true,
|
|
42
|
+
error: err instanceof Error ? err.message : String(err),
|
|
43
|
+
};
|
|
44
|
+
return JSON.stringify(payload);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: MEMORY_GET_TOOL_NAME,
|
|
49
|
+
description: MEMORY_GET_DESCRIPTION,
|
|
50
|
+
schema: MemoryGetSchema,
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memory_search` — the mandatory-recall tool.
|
|
3
|
+
*
|
|
4
|
+
* Port of upstream `createMemorySearchTool` at
|
|
5
|
+
* `upstream reference`. The tool description
|
|
6
|
+
* is verbatim "Mandatory recall step..." from upstream — this is the load-
|
|
7
|
+
* bearing line that turns "the agent may recall" into "the agent will recall".
|
|
8
|
+
*/
|
|
9
|
+
import { tool } from '@langchain/core/tools';
|
|
10
|
+
import {
|
|
11
|
+
MEMORY_SEARCH_DESCRIPTION,
|
|
12
|
+
MEMORY_SEARCH_TOOL_NAME,
|
|
13
|
+
} from '@/memory/constants';
|
|
14
|
+
import {
|
|
15
|
+
buildMemorySearchUnavailableResult,
|
|
16
|
+
clampResultsByInjectedChars,
|
|
17
|
+
MemorySearchSchema,
|
|
18
|
+
type MemorySearchToolResult,
|
|
19
|
+
type MemoryToolBinding,
|
|
20
|
+
} from './shared';
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
23
|
+
export function createMemorySearchTool(binding: MemoryToolBinding) {
|
|
24
|
+
return tool(
|
|
25
|
+
async (args): Promise<string> => {
|
|
26
|
+
try {
|
|
27
|
+
const entries = await binding.backend.search(
|
|
28
|
+
binding.scope,
|
|
29
|
+
args.query,
|
|
30
|
+
{
|
|
31
|
+
maxResults: args.maxResults,
|
|
32
|
+
minScore: args.minScore,
|
|
33
|
+
mmr: binding.searchOptions?.mmr,
|
|
34
|
+
temporalDecay: binding.searchOptions?.temporalDecay,
|
|
35
|
+
citations: binding.searchOptions?.citations,
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
const clamped = clampResultsByInjectedChars(
|
|
39
|
+
entries,
|
|
40
|
+
binding.maxInjectedChars
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// [phase2-recall-tracking] debug: fire-and-forget best-effort record
|
|
44
|
+
if (binding.recallTracker && clamped.length > 0) {
|
|
45
|
+
void binding.recallTracker
|
|
46
|
+
.record({
|
|
47
|
+
agentId: binding.scope.agentId,
|
|
48
|
+
query: args.query,
|
|
49
|
+
hits: clamped.map((e) => ({
|
|
50
|
+
id: e.id,
|
|
51
|
+
path: e.path,
|
|
52
|
+
score: e.score,
|
|
53
|
+
})),
|
|
54
|
+
})
|
|
55
|
+
.catch(() => undefined);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const payload: MemorySearchToolResult = {
|
|
59
|
+
results: clamped.map((entry) => ({
|
|
60
|
+
id: entry.id,
|
|
61
|
+
path: entry.path,
|
|
62
|
+
content: entry.content,
|
|
63
|
+
score: entry.score,
|
|
64
|
+
createdAt: entry.createdAt,
|
|
65
|
+
citation: entry.citation,
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
return JSON.stringify(payload);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
return JSON.stringify(buildMemorySearchUnavailableResult(message));
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: MEMORY_SEARCH_TOOL_NAME,
|
|
76
|
+
description: MEMORY_SEARCH_DESCRIPTION,
|
|
77
|
+
schema: MemorySearchSchema,
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Zod schemas + helpers for the memory tool family.
|
|
3
|
+
*
|
|
4
|
+
* The tool schemas deliberately do NOT expose `agent_id` or `user_id`. Scope
|
|
5
|
+
* is captured in a closure at tool construction time by {@link buildMemoryTools},
|
|
6
|
+
* so even a hallucinated tool call with extra fields cannot influence it.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_MAX_INJECTED_CHARS,
|
|
11
|
+
MEMORY_PATH_PREFIX,
|
|
12
|
+
} from '@/memory/constants';
|
|
13
|
+
import type {
|
|
14
|
+
MemoryAppendInput,
|
|
15
|
+
MemoryBackend,
|
|
16
|
+
MemoryEntry,
|
|
17
|
+
MemoryScope,
|
|
18
|
+
} from '@/memory/types';
|
|
19
|
+
|
|
20
|
+
export const MemorySearchSchema = z.object({
|
|
21
|
+
query: z.string().describe('Natural-language query to search memory for'),
|
|
22
|
+
maxResults: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(50)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Maximum number of results to return (default 10)'),
|
|
29
|
+
minScore: z
|
|
30
|
+
.number()
|
|
31
|
+
.min(0)
|
|
32
|
+
.max(1)
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('Minimum hybrid score (0..1) below which results are dropped'),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const MemoryGetSchema = z.object({
|
|
38
|
+
path: z
|
|
39
|
+
.string()
|
|
40
|
+
.describe(
|
|
41
|
+
'Memory path, e.g. "memory/decisions.md". Must start with "memory/"'
|
|
42
|
+
),
|
|
43
|
+
from: z
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.min(1)
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Starting line number (1-indexed)'),
|
|
49
|
+
lines: z
|
|
50
|
+
.number()
|
|
51
|
+
.int()
|
|
52
|
+
.min(1)
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Number of lines to read starting at `from`'),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const MemoryAppendSchema = z.object({
|
|
58
|
+
path: z
|
|
59
|
+
.string()
|
|
60
|
+
.describe(
|
|
61
|
+
'Memory path to append to (e.g. "memory/decisions.md"). Must start with "memory/"'
|
|
62
|
+
),
|
|
63
|
+
content: z
|
|
64
|
+
.string()
|
|
65
|
+
.min(1)
|
|
66
|
+
.describe(
|
|
67
|
+
"Note content in the agent's own voice. Markdown allowed. Each call appends an immutable entry."
|
|
68
|
+
),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export type MemorySearchArgs = z.infer<typeof MemorySearchSchema>;
|
|
72
|
+
export type MemoryGetArgs = z.infer<typeof MemoryGetSchema>;
|
|
73
|
+
export type MemoryAppendArgs = z.infer<typeof MemoryAppendSchema>;
|
|
74
|
+
|
|
75
|
+
/** Shape returned by `memory_search` — serialised as the tool result string. */
|
|
76
|
+
export interface MemorySearchToolResult {
|
|
77
|
+
results: Array<
|
|
78
|
+
Pick<MemoryEntry, 'id' | 'path' | 'content' | 'score' | 'createdAt'> & {
|
|
79
|
+
citation?: string;
|
|
80
|
+
}
|
|
81
|
+
>;
|
|
82
|
+
disabled?: boolean;
|
|
83
|
+
unavailable?: boolean;
|
|
84
|
+
error?: string;
|
|
85
|
+
warning?: string;
|
|
86
|
+
action?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Shape returned by `memory_get`. */
|
|
90
|
+
export interface MemoryGetToolResult {
|
|
91
|
+
path: string;
|
|
92
|
+
text: string;
|
|
93
|
+
disabled?: boolean;
|
|
94
|
+
error?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildMemorySearchUnavailableResult(
|
|
98
|
+
error: string | undefined
|
|
99
|
+
): MemorySearchToolResult {
|
|
100
|
+
const reason =
|
|
101
|
+
(error ?? 'memory search unavailable').trim() ||
|
|
102
|
+
'memory search unavailable';
|
|
103
|
+
const isQuota = /insufficient_quota|quota|429/i.test(reason);
|
|
104
|
+
return {
|
|
105
|
+
results: [],
|
|
106
|
+
disabled: true,
|
|
107
|
+
unavailable: true,
|
|
108
|
+
error: reason,
|
|
109
|
+
warning: isQuota
|
|
110
|
+
? 'Memory search is unavailable because the embedding provider quota is exhausted.'
|
|
111
|
+
: 'Memory search is unavailable due to an embedding/provider error.',
|
|
112
|
+
action: isQuota
|
|
113
|
+
? 'Top up or switch embedding provider, then retry memory_search.'
|
|
114
|
+
: 'Check embedding provider configuration and retry memory_search.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clamp a ranked result list to a total character budget.
|
|
120
|
+
* Ported from upstream `tools.citations.ts::clampResultsByInjectedChars`.
|
|
121
|
+
*/
|
|
122
|
+
export function clampResultsByInjectedChars<T extends { content: string }>(
|
|
123
|
+
results: T[],
|
|
124
|
+
maxInjectedChars: number = DEFAULT_MAX_INJECTED_CHARS
|
|
125
|
+
): T[] {
|
|
126
|
+
const out: T[] = [];
|
|
127
|
+
let total = 0;
|
|
128
|
+
for (const result of results) {
|
|
129
|
+
const size = result.content.length ?? 0;
|
|
130
|
+
if (total + size > maxInjectedChars && out.length > 0) break;
|
|
131
|
+
out.push(result);
|
|
132
|
+
total += size;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface MemoryToolBinding {
|
|
138
|
+
backend: MemoryBackend;
|
|
139
|
+
scope: MemoryScope;
|
|
140
|
+
maxInjectedChars?: number;
|
|
141
|
+
/** Phase 2 — per-call search options propagated into backend.search. */
|
|
142
|
+
searchOptions?: {
|
|
143
|
+
mmr?: { enabled?: boolean; lambda?: number };
|
|
144
|
+
temporalDecay?: { enabled?: boolean; halfLifeDays?: number };
|
|
145
|
+
citations?: 'on' | 'off' | 'auto';
|
|
146
|
+
};
|
|
147
|
+
/** Phase 2 — optional best-effort recall recorder. */
|
|
148
|
+
recallTracker?: {
|
|
149
|
+
record(params: {
|
|
150
|
+
agentId: string;
|
|
151
|
+
query: string;
|
|
152
|
+
hits: Array<{ id: string; path: string; score: number }>;
|
|
153
|
+
}): Promise<void>;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function assertAppendAllowed(path: string): void {
|
|
158
|
+
if (!path || !path.startsWith(MEMORY_PATH_PREFIX)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`memory_append path must start with "${MEMORY_PATH_PREFIX}"`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Normalise an append call into the backend input shape. */
|
|
166
|
+
export function toAppendInput(args: MemoryAppendArgs): MemoryAppendInput {
|
|
167
|
+
assertAppendAllowed(args.path);
|
|
168
|
+
return { path: args.path, content: args.content };
|
|
169
|
+
}
|
|
@@ -12,7 +12,12 @@ describe('createSearchAPI', () => {
|
|
|
12
12
|
const mockSerperResponse = {
|
|
13
13
|
data: {
|
|
14
14
|
organic: [
|
|
15
|
-
{
|
|
15
|
+
{
|
|
16
|
+
position: 1,
|
|
17
|
+
title: 'Test',
|
|
18
|
+
link: 'https://example.com',
|
|
19
|
+
snippet: 'test',
|
|
20
|
+
},
|
|
16
21
|
],
|
|
17
22
|
topStories: [],
|
|
18
23
|
images: [],
|
package/src/types/graph.ts
CHANGED
|
@@ -421,6 +421,13 @@ export type GraphEdge = {
|
|
|
421
421
|
* Defaults to DEFAULT_HANDOFF_MAX_RESULT_CHARS (32768 chars, ~8192 tokens).
|
|
422
422
|
*/
|
|
423
423
|
maxResultChars?: number;
|
|
424
|
+
/**
|
|
425
|
+
* For handoff edges: When true, the child agent receives the full parent
|
|
426
|
+
* conversation history plus the orchestrator's instructions appended.
|
|
427
|
+
* When false (default), the child only receives the orchestrator's scoped
|
|
428
|
+
* instructions — isolated from the parent conversation.
|
|
429
|
+
*/
|
|
430
|
+
passthrough?: boolean;
|
|
424
431
|
/**
|
|
425
432
|
* Approval gate configuration for sequence edges.
|
|
426
433
|
* When set, inserts an approval gate node between source and destination.
|
|
@@ -702,7 +709,7 @@ export interface AgentInputs {
|
|
|
702
709
|
discoveredTools?: string[];
|
|
703
710
|
/**
|
|
704
711
|
* Optional callback for summarizing messages that were pruned from context.
|
|
705
|
-
* When provided, discarded messages are summarized by the caller
|
|
712
|
+
* When provided, discarded messages are summarized by the host caller
|
|
706
713
|
* using a cheap LLM call, and the summary is prepended to the context.
|
|
707
714
|
*/
|
|
708
715
|
summarizeCallback?: (
|
|
@@ -712,7 +719,7 @@ export interface AgentInputs {
|
|
|
712
719
|
* Pre-existing summary text loaded from persistent storage (MongoDB/Redis).
|
|
713
720
|
* When provided, this summary is injected into the initial message context
|
|
714
721
|
* so the agent has prior conversation history even on new turns.
|
|
715
|
-
* Set by
|
|
722
|
+
* Set by the host's summary store when resuming a conversation.
|
|
716
723
|
*/
|
|
717
724
|
persistedSummary?: string;
|
|
718
725
|
/**
|
|
@@ -733,7 +740,7 @@ export interface AgentInputs {
|
|
|
733
740
|
* - file_search (RAG semantic search over embedded files)
|
|
734
741
|
* - content_tool read (by contentId for exact file retrieval)
|
|
735
742
|
*
|
|
736
|
-
* Built by the orchestrator
|
|
743
|
+
* Built by the host orchestrator from message_file_map
|
|
737
744
|
* and metadata.context_files across all conversation messages.
|
|
738
745
|
*/
|
|
739
746
|
fileManifest?: FileManifestEntry[];
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for child-agent context preparation.
|
|
3
|
+
*
|
|
4
|
+
* These strategies are the primary defense against schema confusion and
|
|
5
|
+
* Bedrock/VertexAI message-shape rejections, so their behavior must be
|
|
6
|
+
* locked down with fixtures that mirror real provider output shapes.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
AIMessage,
|
|
10
|
+
HumanMessage,
|
|
11
|
+
SystemMessage,
|
|
12
|
+
ToolMessage,
|
|
13
|
+
} from '@langchain/core/messages';
|
|
14
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
15
|
+
import {
|
|
16
|
+
HANDOFF_TAIL_CONTEXT_PREFIX,
|
|
17
|
+
buildIsolatedChildPrompt,
|
|
18
|
+
prepareHandoffMessages,
|
|
19
|
+
prepareIsolatedChildMessages,
|
|
20
|
+
} from '../childAgentContext';
|
|
21
|
+
|
|
22
|
+
describe('buildIsolatedChildPrompt', () => {
|
|
23
|
+
it('wraps upstream text in the prior-step / your-task sections', () => {
|
|
24
|
+
const out = buildIsolatedChildPrompt('RESEARCH FINDINGS');
|
|
25
|
+
expect(out).toContain('## Prior step output');
|
|
26
|
+
expect(out).toContain('RESEARCH FINDINGS');
|
|
27
|
+
expect(out).toContain('## Your task');
|
|
28
|
+
expect(out).toMatch(/You MUST now/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('instructs the model to call tools directly without asking for clarification', () => {
|
|
32
|
+
const out = buildIsolatedChildPrompt('x');
|
|
33
|
+
expect(out).toMatch(/call it directly/);
|
|
34
|
+
expect(out).toMatch(/do not ask for clarification/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('prepareHandoffMessages', () => {
|
|
39
|
+
it('returns an empty array unchanged', () => {
|
|
40
|
+
expect(prepareHandoffMessages([])).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('preserves system and human messages verbatim', () => {
|
|
44
|
+
const msgs: BaseMessage[] = [
|
|
45
|
+
new SystemMessage('sys'),
|
|
46
|
+
new HumanMessage('hello'),
|
|
47
|
+
];
|
|
48
|
+
const out = prepareHandoffMessages(msgs);
|
|
49
|
+
expect(out).toHaveLength(2);
|
|
50
|
+
expect(out[0].getType()).toBe('system');
|
|
51
|
+
expect(out[1].getType()).toBe('human');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('drops orphaned tool_use blocks (handoff tool has no matching result)', () => {
|
|
55
|
+
const ai = new AIMessage({
|
|
56
|
+
content: 'transferring',
|
|
57
|
+
tool_calls: [{ name: 'handoff_to_writer', args: {}, id: 'call_1' }],
|
|
58
|
+
});
|
|
59
|
+
const out = prepareHandoffMessages([new HumanMessage('go'), ai]);
|
|
60
|
+
// The assistant tail becomes a HumanMessage with the context prefix
|
|
61
|
+
// because the orphaned tool_use was stripped, leaving only text.
|
|
62
|
+
expect(out).toHaveLength(2);
|
|
63
|
+
expect(out[1].getType()).toBe('human');
|
|
64
|
+
expect(out[1].content).toContain('transferring');
|
|
65
|
+
expect(out[1].content).toContain(HANDOFF_TAIL_CONTEXT_PREFIX);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('compacts paired tool_use/tool_result into a text summary', () => {
|
|
69
|
+
const ai = new AIMessage({
|
|
70
|
+
content: '',
|
|
71
|
+
tool_calls: [{ name: 'search', args: { q: 'x' }, id: 'call_a' }],
|
|
72
|
+
});
|
|
73
|
+
const toolResult = new ToolMessage({
|
|
74
|
+
content: 'hit: 42',
|
|
75
|
+
tool_call_id: 'call_a',
|
|
76
|
+
});
|
|
77
|
+
const out = prepareHandoffMessages([
|
|
78
|
+
new HumanMessage('find x'),
|
|
79
|
+
ai,
|
|
80
|
+
toolResult,
|
|
81
|
+
new HumanMessage('what did you find?'),
|
|
82
|
+
]);
|
|
83
|
+
// The paired tool_use+result should have become a text summary on the
|
|
84
|
+
// rewritten AI message; the original ToolMessage should be gone.
|
|
85
|
+
expect(out.some((m) => m.getType() === 'tool')).toBe(false);
|
|
86
|
+
const rewrittenAI = out.find((m) => m.getType() === 'ai');
|
|
87
|
+
expect(rewrittenAI).toBeDefined();
|
|
88
|
+
expect(String(rewrittenAI?.content)).toContain('[Tool "search"');
|
|
89
|
+
expect(String(rewrittenAI?.content)).toContain('hit: 42');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('truncates long tool_result content at 500 chars in the summary', () => {
|
|
93
|
+
const long = 'x'.repeat(1200);
|
|
94
|
+
const ai = new AIMessage({
|
|
95
|
+
content: '',
|
|
96
|
+
tool_calls: [{ name: 'dump', args: {}, id: 'call_big' }],
|
|
97
|
+
});
|
|
98
|
+
const tool = new ToolMessage({ content: long, tool_call_id: 'call_big' });
|
|
99
|
+
const out = prepareHandoffMessages([new HumanMessage('hi'), ai, tool]);
|
|
100
|
+
// After the paired compaction the tail is an AIMessage with no text, so
|
|
101
|
+
// it gets dropped (empty) — find the one that carries the summary by
|
|
102
|
+
// scanning for the "[Tool" marker in any message.
|
|
103
|
+
const blob = out.map((m) => String(m.content)).join(' ');
|
|
104
|
+
expect(blob).toContain('[Tool "dump"');
|
|
105
|
+
// The truncated slice is 500 chars of x, not the full 1200.
|
|
106
|
+
const match = blob.match(/x+/);
|
|
107
|
+
expect(match).toBeTruthy();
|
|
108
|
+
expect((match?.[0].length ?? 0) <= 500).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('ensures the tail message is a HumanMessage by rewriting a trailing AI', () => {
|
|
112
|
+
const msgs: BaseMessage[] = [
|
|
113
|
+
new HumanMessage('question'),
|
|
114
|
+
new AIMessage('plain text answer'),
|
|
115
|
+
];
|
|
116
|
+
const out = prepareHandoffMessages(msgs);
|
|
117
|
+
expect(out[out.length - 1].getType()).toBe('human');
|
|
118
|
+
expect(out[out.length - 1].content).toBe(
|
|
119
|
+
`${HANDOFF_TAIL_CONTEXT_PREFIX}plain text answer`
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('drops a trailing empty AIMessage instead of creating an empty human', () => {
|
|
124
|
+
const msgs: BaseMessage[] = [
|
|
125
|
+
new HumanMessage('q'),
|
|
126
|
+
new AIMessage(''), // empty, nothing to carry forward
|
|
127
|
+
];
|
|
128
|
+
const out = prepareHandoffMessages(msgs);
|
|
129
|
+
expect(out).toHaveLength(1);
|
|
130
|
+
expect(out[0].getType()).toBe('human');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('prepareIsolatedChildMessages', () => {
|
|
135
|
+
it('returns empty input unchanged', () => {
|
|
136
|
+
expect(prepareIsolatedChildMessages([])).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('keeps only the first human message and a synthetic directive', () => {
|
|
140
|
+
const msgs: BaseMessage[] = [
|
|
141
|
+
new HumanMessage('original request'),
|
|
142
|
+
new AIMessage('upstream produced this result'),
|
|
143
|
+
];
|
|
144
|
+
const out = prepareIsolatedChildMessages(msgs);
|
|
145
|
+
expect(out).toHaveLength(2);
|
|
146
|
+
expect(out[0].getType()).toBe('human');
|
|
147
|
+
expect(out[0].content).toBe('original request');
|
|
148
|
+
expect(out[1].getType()).toBe('human');
|
|
149
|
+
expect(String(out[1].content)).toContain('upstream produced this result');
|
|
150
|
+
expect(String(out[1].content)).toContain('## Your task');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('discards upstream tool_use/tool_result blocks entirely', () => {
|
|
154
|
+
const aiWithTool = new AIMessage({
|
|
155
|
+
content: 'working',
|
|
156
|
+
tool_calls: [{ name: 'search', args: {}, id: 'c1' }],
|
|
157
|
+
});
|
|
158
|
+
const toolResult = new ToolMessage({
|
|
159
|
+
content: 'search result',
|
|
160
|
+
tool_call_id: 'c1',
|
|
161
|
+
});
|
|
162
|
+
const aiFinal = new AIMessage('final summary text');
|
|
163
|
+
const out = prepareIsolatedChildMessages([
|
|
164
|
+
new HumanMessage('user ask'),
|
|
165
|
+
aiWithTool,
|
|
166
|
+
toolResult,
|
|
167
|
+
aiFinal,
|
|
168
|
+
]);
|
|
169
|
+
// Must not contain any ToolMessage or upstream tool_calls.
|
|
170
|
+
expect(out.some((m) => m.getType() === 'tool')).toBe(false);
|
|
171
|
+
// The synthetic directive should reference only the final text output.
|
|
172
|
+
const directive = String(out[1].content);
|
|
173
|
+
expect(directive).toContain('final summary text');
|
|
174
|
+
expect(directive).not.toContain('search result');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('picks the most recent non-empty AI text output', () => {
|
|
178
|
+
const msgs: BaseMessage[] = [
|
|
179
|
+
new HumanMessage('req'),
|
|
180
|
+
new AIMessage('older text'),
|
|
181
|
+
new AIMessage(''),
|
|
182
|
+
new AIMessage('newer text'),
|
|
183
|
+
];
|
|
184
|
+
const out = prepareIsolatedChildMessages(msgs);
|
|
185
|
+
expect(String(out[1].content)).toContain('newer text');
|
|
186
|
+
expect(String(out[1].content)).not.toContain('older text');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('handles AI content in Anthropic array-of-blocks shape', () => {
|
|
190
|
+
const ai = new AIMessage({
|
|
191
|
+
content: [
|
|
192
|
+
{ type: 'tool_use', name: 'x', input: {}, id: 'c1' },
|
|
193
|
+
{ type: 'text', text: 'block text result' },
|
|
194
|
+
] as unknown as string, // the SDK types are strict, cast for fixture
|
|
195
|
+
});
|
|
196
|
+
const out = prepareIsolatedChildMessages([new HumanMessage('q'), ai]);
|
|
197
|
+
expect(out).toHaveLength(2);
|
|
198
|
+
expect(String(out[1].content)).toContain('block text result');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('falls back to raw messages if no human and no upstream text exist', () => {
|
|
202
|
+
const msgs: BaseMessage[] = [new SystemMessage('just system')];
|
|
203
|
+
const out = prepareIsolatedChildMessages(msgs);
|
|
204
|
+
expect(out).toBe(msgs);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('returns just the user message when upstream has no text content', () => {
|
|
208
|
+
const msgs: BaseMessage[] = [
|
|
209
|
+
new HumanMessage('only user'),
|
|
210
|
+
new AIMessage(''),
|
|
211
|
+
];
|
|
212
|
+
const out = prepareIsolatedChildMessages(msgs);
|
|
213
|
+
expect(out).toHaveLength(1);
|
|
214
|
+
expect(out[0].getType()).toBe('human');
|
|
215
|
+
expect(out[0].content).toBe('only user');
|
|
216
|
+
});
|
|
217
|
+
});
|