@neo4j-labs/agent-memory 0.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/CHANGELOG.md +79 -0
- package/LICENSE +190 -0
- package/README.md +154 -0
- package/dist/chunk-ASQMU7YC.js +58 -0
- package/dist/chunk-ASQMU7YC.js.map +1 -0
- package/dist/chunk-TGBKROHO.js +226 -0
- package/dist/chunk-TGBKROHO.js.map +1 -0
- package/dist/client-DSqbWQoa.d.ts +551 -0
- package/dist/index-qfRrdQNP.d.ts +42 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +1313 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/langchain.d.ts +56 -0
- package/dist/integrations/langchain.js +69 -0
- package/dist/integrations/langchain.js.map +1 -0
- package/dist/integrations/mastra.d.ts +56 -0
- package/dist/integrations/mastra.js +65 -0
- package/dist/integrations/mastra.js.map +1 -0
- package/dist/integrations/strands.d.ts +239 -0
- package/dist/integrations/strands.js +413 -0
- package/dist/integrations/strands.js.map +1 -0
- package/dist/mcp/index.d.ts +53 -0
- package/dist/mcp/index.js +256 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/middleware/vercel-ai.d.ts +86 -0
- package/dist/middleware/vercel-ai.js +107 -0
- package/dist/middleware/vercel-ai.js.map +1 -0
- package/dist/testing.d.ts +37 -0
- package/dist/testing.js +4 -0
- package/dist/testing.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { SessionManager, ConversationManager, ConversationManagerReduceOptions, LocalAgent, SnapshotStorage, SnapshotLocation, Snapshot, SnapshotManifest } from '@strands-agents/sdk';
|
|
2
|
+
import { M as MemoryClient } from '../client-DSqbWQoa.js';
|
|
3
|
+
import '../index-qfRrdQNP.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Strands Agents SDK integration — three orthogonal surfaces, exposed
|
|
7
|
+
* through a single subpath.
|
|
8
|
+
*
|
|
9
|
+
* 1. {@link Neo4jSessionStorage} — implements `SnapshotStorage` so
|
|
10
|
+
* Strands' `SessionManager` persists session state into a NAMS
|
|
11
|
+
* conversation. Hybrid mapping: messages from each snapshot land as
|
|
12
|
+
* real `Message` graph nodes via `addMessage`; the rest of the
|
|
13
|
+
* framework's per-snapshot state is stashed losslessly in synthetic
|
|
14
|
+
* Strands marker messages on that same conversation.
|
|
15
|
+
*
|
|
16
|
+
* 2. {@link Neo4jConversationManager} — a `ConversationManager`
|
|
17
|
+
* subclass that delegates `reduce()` to an inner manager
|
|
18
|
+
* (defaults to `SlidingWindowConversationManager`) AND registers
|
|
19
|
+
* a `BeforeInvocationEvent` hook that prepends three-tier context
|
|
20
|
+
* (reflections + observations from `getContext()`) to every model
|
|
21
|
+
* call. Layered, not replacing — recent-history trimming still
|
|
22
|
+
* behaves the way the inner manager defines.
|
|
23
|
+
*
|
|
24
|
+
* 3. {@link registerReasoningHooks} — wires Strands hook events to
|
|
25
|
+
* our reasoning subclient. Each invocation opens a `ReasoningStep`;
|
|
26
|
+
* each tool call records against that step.
|
|
27
|
+
*
|
|
28
|
+
* {@link connectMemoryToAgent} bundles all three for the common case.
|
|
29
|
+
*
|
|
30
|
+
* Strands lives in `devDependencies` only — every import below is a
|
|
31
|
+
* type-only import, erased at compile time. The published
|
|
32
|
+
* `dist/integrations/strands.js` has no runtime reference to
|
|
33
|
+
* `@strands-agents/sdk`, so users without Strands installed pay zero
|
|
34
|
+
* bundle cost.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { Agent } from "@strands-agents/sdk";
|
|
39
|
+
* import { MemoryClient } from "@neo4j-labs/agent-memory";
|
|
40
|
+
* import { connectMemoryToAgent } from "@neo4j-labs/agent-memory/integrations/strands";
|
|
41
|
+
*
|
|
42
|
+
* const memory = new MemoryClient();
|
|
43
|
+
* const conv = await memory.shortTerm.createConversation({ userId: "alice" });
|
|
44
|
+
*
|
|
45
|
+
* const agent = new Agent({
|
|
46
|
+
* ...await connectMemoryToAgent(memory, { conversationId: conv.id }),
|
|
47
|
+
* model,
|
|
48
|
+
* tools: [...],
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* await agent.invoke("Tell me about graph databases.");
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/** Options shared by every public entrypoint in this module. */
|
|
56
|
+
interface StrandsIntegrationOptions {
|
|
57
|
+
/**
|
|
58
|
+
* NAMS Conversation id to wire to. Required by the convenience factory and
|
|
59
|
+
* by individual exports that need correlation across invocations.
|
|
60
|
+
*/
|
|
61
|
+
conversationId: string;
|
|
62
|
+
/** Include reflections from `getContext()` in prompt injection. Default: true. */
|
|
63
|
+
includeReflections?: boolean;
|
|
64
|
+
/** Include observations from `getContext()` in prompt injection. Default: true. */
|
|
65
|
+
includeObservations?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** Options for {@link Neo4jConversationManager}. */
|
|
68
|
+
interface Neo4jConversationManagerOptions extends Pick<StrandsIntegrationOptions, "conversationId" | "includeReflections" | "includeObservations"> {
|
|
69
|
+
/**
|
|
70
|
+
* Inner `ConversationManager` to delegate `reduce()` to. When omitted,
|
|
71
|
+
* defaults to `SlidingWindowConversationManager` (constructed lazily so
|
|
72
|
+
* Strands' module is only loaded if the manager is actually used).
|
|
73
|
+
*/
|
|
74
|
+
inner?: ConversationManager;
|
|
75
|
+
}
|
|
76
|
+
/** Options for {@link registerReasoningHooks}. */
|
|
77
|
+
interface ReasoningHooksOptions {
|
|
78
|
+
/** NAMS Conversation id to attribute reasoning steps and tool calls to. */
|
|
79
|
+
conversationId: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns true if a message is one of our synthetic state/manifest
|
|
83
|
+
* markers. Exported so consumers walking the conversation can filter
|
|
84
|
+
* them out of UI rendering. See `SYNTHETIC_MESSAGE_PREFIXES` for the
|
|
85
|
+
* canonical prefix list.
|
|
86
|
+
*
|
|
87
|
+
* Recognizes ANY role — the storage role used by the integration is
|
|
88
|
+
* `"user"`, but older saves may have used `"system"`. We match on the
|
|
89
|
+
* content prefix alone for resilience.
|
|
90
|
+
*/
|
|
91
|
+
declare function isSyntheticStrandsMessage(message: {
|
|
92
|
+
role: string;
|
|
93
|
+
content: string;
|
|
94
|
+
}): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Canonical content prefixes used for synthetic messages. Consumers
|
|
97
|
+
* (chat UIs, message-list renderers, Cypher queries) can filter on
|
|
98
|
+
* these to skip the Strands-internal state messages.
|
|
99
|
+
*/
|
|
100
|
+
declare const SYNTHETIC_MESSAGE_PREFIXES: readonly ["__strands_state__:", "__strands_manifest__:"];
|
|
101
|
+
/**
|
|
102
|
+
* Implements Strands' `SnapshotStorage` against a NAMS `MemoryClient`.
|
|
103
|
+
*
|
|
104
|
+
* One Strands session = one NAMS Conversation (keyed by `location.sessionId`).
|
|
105
|
+
* Snapshots are versions within that conversation:
|
|
106
|
+
*
|
|
107
|
+
* - Real conversation messages from `snapshot.data.messages` land as real
|
|
108
|
+
* `Message` graph nodes via `addMessage` (so entity extraction, search,
|
|
109
|
+
* and the graph view all work on them).
|
|
110
|
+
* - Non-message snapshot state (Strands' `data` minus `messages`, plus
|
|
111
|
+
* `appData`, plus the manifest) is persisted as synthetic `role: "user"`
|
|
112
|
+
* messages whose content carries both a marker prefix and a
|
|
113
|
+
* base64-encoded JSON blob. NAMS exposes `POST /conversations/{id}/messages`
|
|
114
|
+
* as the only documented conversation-scoped write, so this approach
|
|
115
|
+
* stays within the documented API surface.
|
|
116
|
+
*
|
|
117
|
+
* Consumers walking the message list (chat UIs, Cypher queries) MUST
|
|
118
|
+
* filter synthetic messages with {@link isSyntheticStrandsMessage}.
|
|
119
|
+
* Strands itself never sees them: {@link Neo4jSessionStorage.loadSnapshot}
|
|
120
|
+
* strips them from the reconstructed Snapshot before handing back to
|
|
121
|
+
* `SessionManager`.
|
|
122
|
+
*
|
|
123
|
+
* Auth errors propagate — Strands needs to know if the backing store is
|
|
124
|
+
* unreachable. Transient errors propagate too; Strands' own retry
|
|
125
|
+
* semantics (in `SessionManager`) apply.
|
|
126
|
+
*/
|
|
127
|
+
declare class Neo4jSessionStorage implements SnapshotStorage {
|
|
128
|
+
private readonly memory;
|
|
129
|
+
constructor(memory: MemoryClient);
|
|
130
|
+
saveSnapshot(params: {
|
|
131
|
+
location: SnapshotLocation;
|
|
132
|
+
snapshotId: string;
|
|
133
|
+
isLatest: boolean;
|
|
134
|
+
snapshot: Snapshot;
|
|
135
|
+
}): Promise<void>;
|
|
136
|
+
loadSnapshot(params: {
|
|
137
|
+
location: SnapshotLocation;
|
|
138
|
+
snapshotId?: string;
|
|
139
|
+
}): Promise<Snapshot | null>;
|
|
140
|
+
listSnapshotIds(params: {
|
|
141
|
+
location: SnapshotLocation;
|
|
142
|
+
limit?: number;
|
|
143
|
+
startAfter?: string;
|
|
144
|
+
}): Promise<string[]>;
|
|
145
|
+
deleteSession(params: {
|
|
146
|
+
sessionId: string;
|
|
147
|
+
}): Promise<void>;
|
|
148
|
+
loadManifest(params: {
|
|
149
|
+
location: SnapshotLocation;
|
|
150
|
+
}): Promise<SnapshotManifest>;
|
|
151
|
+
saveManifest(params: {
|
|
152
|
+
location: SnapshotLocation;
|
|
153
|
+
manifest: SnapshotManifest;
|
|
154
|
+
}): Promise<void>;
|
|
155
|
+
/**
|
|
156
|
+
* Scan a conversation's message list and parse any state markers into
|
|
157
|
+
* blobs, in original order. Matches on the content prefix alone for
|
|
158
|
+
* resilience against role normalization on the service side.
|
|
159
|
+
*/
|
|
160
|
+
private readStateBlobs;
|
|
161
|
+
/** Same idea, for manifest markers. */
|
|
162
|
+
private readManifestBlobs;
|
|
163
|
+
/**
|
|
164
|
+
* Pull the message list out of `snapshot.data.messages` (the canonical
|
|
165
|
+
* Strands layout), find ones not yet present on the conversation
|
|
166
|
+
* (excluding our synthetic markers), and persist them via `addMessage`.
|
|
167
|
+
* Returns the number of new messages written.
|
|
168
|
+
*/
|
|
169
|
+
private extractAndPersistMessages;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Layered ConversationManager: context-injection hook + inner manager.
|
|
173
|
+
*
|
|
174
|
+
* The inner manager (defaults to `SlidingWindowConversationManager`) owns
|
|
175
|
+
* trimming and summarization. This manager registers a
|
|
176
|
+
* `BeforeInvocationEvent` hook that prepends reflections + observations from
|
|
177
|
+
* `getContext()` as system messages, BEFORE the inner manager's reduce
|
|
178
|
+
* logic runs.
|
|
179
|
+
*
|
|
180
|
+
* Lazily constructs an inner manager on first `initAgent` invocation so
|
|
181
|
+
* importing this module doesn't load Strands' runtime unless the manager
|
|
182
|
+
* is actually used.
|
|
183
|
+
*/
|
|
184
|
+
declare class Neo4jConversationManager {
|
|
185
|
+
private readonly memory;
|
|
186
|
+
private readonly options;
|
|
187
|
+
readonly name = "neo4j:context-injection";
|
|
188
|
+
/**
|
|
189
|
+
* Mirrored from Strands' `ConversationManager` to satisfy duck-typing
|
|
190
|
+
* at compile time. We never set it — context injection has no notion
|
|
191
|
+
* of a compression threshold.
|
|
192
|
+
*/
|
|
193
|
+
protected readonly _compressionThreshold: number | undefined;
|
|
194
|
+
private inner;
|
|
195
|
+
constructor(memory: MemoryClient, options: Neo4jConversationManagerOptions);
|
|
196
|
+
reduce(opts: ConversationManagerReduceOptions): Promise<boolean>;
|
|
197
|
+
initAgent(agent: LocalAgent): Promise<void>;
|
|
198
|
+
private ensureInner;
|
|
199
|
+
private injectContext;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Wire reasoning capture onto a Strands `HookRegistry`.
|
|
203
|
+
*
|
|
204
|
+
* - `BeforeInvocationEvent` → `reasoning.recordStep` (opens a step; stashes
|
|
205
|
+
* step id on `event.invocationState`).
|
|
206
|
+
* - `AfterInvocationEvent` → re-records the step with a `result` field
|
|
207
|
+
* (best-effort; we don't have a public `updateStep` API yet, so the
|
|
208
|
+
* second write supplements rather than mutates).
|
|
209
|
+
* - `BeforeToolCallEvent` → `reasoning.recordToolCall` with status
|
|
210
|
+
* `pending`. Strands tool-call id → our tool-call id map stashed on
|
|
211
|
+
* `invocationState`.
|
|
212
|
+
* - `AfterToolCallEvent` → updates the recorded tool call's status.
|
|
213
|
+
*
|
|
214
|
+
* All capture is best-effort: every reasoning write is wrapped in try/catch
|
|
215
|
+
* and silently swallowed on failure. Reasoning capture must never break the
|
|
216
|
+
* agent run.
|
|
217
|
+
*/
|
|
218
|
+
declare function registerReasoningHooks(memory: MemoryClient, agent: LocalAgent, options: ReasoningHooksOptions): Promise<void>;
|
|
219
|
+
/** Result of {@link connectMemoryToAgent} — spread directly into `new Agent({ ... })`. */
|
|
220
|
+
interface ConnectMemoryToAgentResult {
|
|
221
|
+
sessionManager: SessionManager;
|
|
222
|
+
/**
|
|
223
|
+
* Typed as `StrandsConversationManager` (the abstract base) so callers
|
|
224
|
+
* can spread the result straight into `new Agent({ ... })` without
|
|
225
|
+
* casts. At runtime this is a {@link Neo4jConversationManager}.
|
|
226
|
+
*/
|
|
227
|
+
conversationManager: ConversationManager;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* One-shot helper that wires the SessionStorage, the ConversationManager, and
|
|
231
|
+
* (lazily) the reasoning hooks against a NAMS `MemoryClient`. Spread the
|
|
232
|
+
* return value into `new Agent({ ... })`.
|
|
233
|
+
*
|
|
234
|
+
* Reasoning hooks attach themselves automatically when the conversation
|
|
235
|
+
* manager's `initAgent` runs — no separate registration step required.
|
|
236
|
+
*/
|
|
237
|
+
declare function connectMemoryToAgent(memory: MemoryClient, options: StrandsIntegrationOptions): Promise<ConnectMemoryToAgentResult>;
|
|
238
|
+
|
|
239
|
+
export { type ConnectMemoryToAgentResult, Neo4jConversationManager, type Neo4jConversationManagerOptions, Neo4jSessionStorage, type ReasoningHooksOptions, SYNTHETIC_MESSAGE_PREFIXES, type StrandsIntegrationOptions, connectMemoryToAgent, isSyntheticStrandsMessage, registerReasoningHooks };
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// src/integrations/strands.ts
|
|
2
|
+
var _strandsModule = null;
|
|
3
|
+
async function loadStrands() {
|
|
4
|
+
if (!_strandsModule) {
|
|
5
|
+
_strandsModule = await import('@strands-agents/sdk');
|
|
6
|
+
}
|
|
7
|
+
return _strandsModule;
|
|
8
|
+
}
|
|
9
|
+
var STATE_PREFIX = "__strands_state__:";
|
|
10
|
+
var MANIFEST_PREFIX = "__strands_manifest__:";
|
|
11
|
+
var SYNTHETIC_ROLE = "user";
|
|
12
|
+
function encodeBlob(blob) {
|
|
13
|
+
return base64Encode(JSON.stringify(blob));
|
|
14
|
+
}
|
|
15
|
+
function decodeBlob(content, prefix) {
|
|
16
|
+
if (!content.startsWith(prefix)) return null;
|
|
17
|
+
const payload = content.slice(prefix.length);
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(base64Decode(payload));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function base64Encode(s) {
|
|
25
|
+
if (typeof Buffer !== "undefined") {
|
|
26
|
+
return Buffer.from(s, "utf8").toString("base64");
|
|
27
|
+
}
|
|
28
|
+
const g = globalThis;
|
|
29
|
+
if (typeof g.btoa === "function") {
|
|
30
|
+
const bytes = new TextEncoder().encode(s);
|
|
31
|
+
let bin = "";
|
|
32
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
33
|
+
return g.btoa(bin);
|
|
34
|
+
}
|
|
35
|
+
throw new Error("No base64 encoder available in this runtime");
|
|
36
|
+
}
|
|
37
|
+
function base64Decode(b64) {
|
|
38
|
+
if (typeof Buffer !== "undefined") {
|
|
39
|
+
return Buffer.from(b64, "base64").toString("utf8");
|
|
40
|
+
}
|
|
41
|
+
const g = globalThis;
|
|
42
|
+
if (typeof g.atob === "function") {
|
|
43
|
+
const bin = g.atob(b64);
|
|
44
|
+
const bytes = new Uint8Array(bin.length);
|
|
45
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
46
|
+
return new TextDecoder().decode(bytes);
|
|
47
|
+
}
|
|
48
|
+
throw new Error("No base64 decoder available in this runtime");
|
|
49
|
+
}
|
|
50
|
+
function isSyntheticStrandsMessage(message) {
|
|
51
|
+
return message.content.startsWith(STATE_PREFIX) || message.content.startsWith(MANIFEST_PREFIX);
|
|
52
|
+
}
|
|
53
|
+
var SYNTHETIC_MESSAGE_PREFIXES = [STATE_PREFIX, MANIFEST_PREFIX];
|
|
54
|
+
var Neo4jSessionStorage = class {
|
|
55
|
+
constructor(memory) {
|
|
56
|
+
this.memory = memory;
|
|
57
|
+
}
|
|
58
|
+
async saveSnapshot(params) {
|
|
59
|
+
const { location, snapshotId, isLatest, snapshot } = params;
|
|
60
|
+
const conversationId = location.sessionId;
|
|
61
|
+
const existingConversation = await this.memory.shortTerm.getConversation(conversationId);
|
|
62
|
+
await this.extractAndPersistMessages(conversationId, snapshot, existingConversation.messages);
|
|
63
|
+
const strippedSnapshot = stripMessagesFromSnapshot(snapshot);
|
|
64
|
+
const blob = {
|
|
65
|
+
snapshotId,
|
|
66
|
+
isLatest,
|
|
67
|
+
snapshot: strippedSnapshot,
|
|
68
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
69
|
+
};
|
|
70
|
+
const previous = findLastStateBlobForSnapshotId(
|
|
71
|
+
this.readStateBlobs(existingConversation.messages),
|
|
72
|
+
snapshotId
|
|
73
|
+
);
|
|
74
|
+
if (previous && sameStateBlob(previous, blob)) return;
|
|
75
|
+
await this.memory.shortTerm.addMessage(
|
|
76
|
+
conversationId,
|
|
77
|
+
SYNTHETIC_ROLE,
|
|
78
|
+
`${STATE_PREFIX}${encodeBlob(blob)}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
async loadSnapshot(params) {
|
|
82
|
+
const conversationId = params.location.sessionId;
|
|
83
|
+
const conv = await this.memory.shortTerm.getConversation(conversationId);
|
|
84
|
+
const stateBlobs = this.readStateBlobs(conv.messages);
|
|
85
|
+
if (stateBlobs.length === 0) return null;
|
|
86
|
+
let blob;
|
|
87
|
+
if (params.snapshotId) {
|
|
88
|
+
blob = findLastStateBlobForSnapshotId(stateBlobs, params.snapshotId);
|
|
89
|
+
} else {
|
|
90
|
+
blob = [...stateBlobs].reverse().find((b) => b.isLatest) ?? stateBlobs[stateBlobs.length - 1];
|
|
91
|
+
}
|
|
92
|
+
if (!blob) return null;
|
|
93
|
+
const realMessages = conv.messages.filter((m) => !isSyntheticStrandsMessage(m)).map(toStrandsMessage);
|
|
94
|
+
return mergeMessagesIntoSnapshot(blob.snapshot, realMessages);
|
|
95
|
+
}
|
|
96
|
+
async listSnapshotIds(params) {
|
|
97
|
+
const conv = await this.memory.shortTerm.getConversation(params.location.sessionId);
|
|
98
|
+
const stateBlobs = this.readStateBlobs(conv.messages);
|
|
99
|
+
const seen = /* @__PURE__ */ new Set();
|
|
100
|
+
const ids = [];
|
|
101
|
+
for (const blob of stateBlobs) {
|
|
102
|
+
if (seen.has(blob.snapshotId)) continue;
|
|
103
|
+
seen.add(blob.snapshotId);
|
|
104
|
+
ids.push(blob.snapshotId);
|
|
105
|
+
}
|
|
106
|
+
let start = 0;
|
|
107
|
+
if (params.startAfter) {
|
|
108
|
+
const idx = ids.indexOf(params.startAfter);
|
|
109
|
+
start = idx >= 0 ? idx + 1 : 0;
|
|
110
|
+
}
|
|
111
|
+
return ids.slice(start, params.limit ? start + params.limit : void 0);
|
|
112
|
+
}
|
|
113
|
+
async deleteSession(params) {
|
|
114
|
+
await this.memory.shortTerm.deleteConversation(params.sessionId);
|
|
115
|
+
}
|
|
116
|
+
async loadManifest(params) {
|
|
117
|
+
const conv = await this.memory.shortTerm.getConversation(params.location.sessionId);
|
|
118
|
+
const blobs = this.readManifestBlobs(conv.messages);
|
|
119
|
+
const matching = blobs.filter((b) => b.scopeId === params.location.scopeId);
|
|
120
|
+
return matching[matching.length - 1]?.manifest ?? defaultManifest();
|
|
121
|
+
}
|
|
122
|
+
async saveManifest(params) {
|
|
123
|
+
const blob = {
|
|
124
|
+
scopeId: params.location.scopeId,
|
|
125
|
+
manifest: params.manifest,
|
|
126
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
127
|
+
};
|
|
128
|
+
await this.memory.shortTerm.addMessage(
|
|
129
|
+
params.location.sessionId,
|
|
130
|
+
SYNTHETIC_ROLE,
|
|
131
|
+
`${MANIFEST_PREFIX}${encodeBlob(blob)}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
// --- Internals ------------------------------------------------------------
|
|
135
|
+
/**
|
|
136
|
+
* Scan a conversation's message list and parse any state markers into
|
|
137
|
+
* blobs, in original order. Matches on the content prefix alone for
|
|
138
|
+
* resilience against role normalization on the service side.
|
|
139
|
+
*/
|
|
140
|
+
readStateBlobs(messages) {
|
|
141
|
+
const blobs = [];
|
|
142
|
+
for (const msg of messages) {
|
|
143
|
+
const blob = decodeBlob(msg.content, STATE_PREFIX);
|
|
144
|
+
if (blob) blobs.push(blob);
|
|
145
|
+
}
|
|
146
|
+
return blobs;
|
|
147
|
+
}
|
|
148
|
+
/** Same idea, for manifest markers. */
|
|
149
|
+
readManifestBlobs(messages) {
|
|
150
|
+
const blobs = [];
|
|
151
|
+
for (const msg of messages) {
|
|
152
|
+
const blob = decodeBlob(msg.content, MANIFEST_PREFIX);
|
|
153
|
+
if (blob) blobs.push(blob);
|
|
154
|
+
}
|
|
155
|
+
return blobs;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Pull the message list out of `snapshot.data.messages` (the canonical
|
|
159
|
+
* Strands layout), find ones not yet present on the conversation
|
|
160
|
+
* (excluding our synthetic markers), and persist them via `addMessage`.
|
|
161
|
+
* Returns the number of new messages written.
|
|
162
|
+
*/
|
|
163
|
+
async extractAndPersistMessages(conversationId, snapshot, existingMessages) {
|
|
164
|
+
const messages = pickStrandsMessages(snapshot);
|
|
165
|
+
if (messages.length === 0) return 0;
|
|
166
|
+
const seen = new Set(
|
|
167
|
+
(existingMessages ?? (await this.memory.shortTerm.getConversation(conversationId)).messages).filter((m) => !isSyntheticStrandsMessage(m)).map((m) => `${m.role}::${m.content}`)
|
|
168
|
+
);
|
|
169
|
+
let writes = 0;
|
|
170
|
+
for (const msg of messages) {
|
|
171
|
+
const text = strandsMessageToText(msg);
|
|
172
|
+
const key = `${msg.role}::${text}`;
|
|
173
|
+
if (seen.has(key)) continue;
|
|
174
|
+
seen.add(key);
|
|
175
|
+
await this.memory.shortTerm.addMessage(conversationId, msg.role, text);
|
|
176
|
+
writes++;
|
|
177
|
+
}
|
|
178
|
+
return writes;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var Neo4jConversationManager = class {
|
|
182
|
+
constructor(memory, options) {
|
|
183
|
+
this.memory = memory;
|
|
184
|
+
this.options = options;
|
|
185
|
+
}
|
|
186
|
+
name = "neo4j:context-injection";
|
|
187
|
+
/**
|
|
188
|
+
* Mirrored from Strands' `ConversationManager` to satisfy duck-typing
|
|
189
|
+
* at compile time. We never set it — context injection has no notion
|
|
190
|
+
* of a compression threshold.
|
|
191
|
+
*/
|
|
192
|
+
_compressionThreshold = void 0;
|
|
193
|
+
// We can't extend Strands' abstract class via a static `extends` clause
|
|
194
|
+
// because Strands is a dynamic import — the base class identity isn't
|
|
195
|
+
// known at module-load time. Instead we *delegate* to a lazily-built
|
|
196
|
+
// inner manager and implement the abstract surface explicitly. Strands
|
|
197
|
+
// duck-types on shape, not on instanceof, so this works.
|
|
198
|
+
inner = null;
|
|
199
|
+
async reduce(opts) {
|
|
200
|
+
const inner = await this.ensureInner();
|
|
201
|
+
return inner.reduce(opts);
|
|
202
|
+
}
|
|
203
|
+
async initAgent(agent) {
|
|
204
|
+
const inner = await this.ensureInner();
|
|
205
|
+
inner.initAgent(agent);
|
|
206
|
+
const strands = await loadStrands();
|
|
207
|
+
agent.addHook(
|
|
208
|
+
strands.BeforeInvocationEvent,
|
|
209
|
+
async (event) => {
|
|
210
|
+
await this.injectContext(event);
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
async ensureInner() {
|
|
215
|
+
if (this.inner) return this.inner;
|
|
216
|
+
if (this.options.inner) {
|
|
217
|
+
this.inner = this.options.inner;
|
|
218
|
+
return this.inner;
|
|
219
|
+
}
|
|
220
|
+
const strands = await loadStrands();
|
|
221
|
+
const Ctor = strands.SlidingWindowConversationManager;
|
|
222
|
+
this.inner = new Ctor();
|
|
223
|
+
return this.inner;
|
|
224
|
+
}
|
|
225
|
+
async injectContext(event) {
|
|
226
|
+
try {
|
|
227
|
+
const ctx = await this.memory.shortTerm.getContext(this.options.conversationId);
|
|
228
|
+
const prepend = [];
|
|
229
|
+
const includeReflections = this.options.includeReflections ?? true;
|
|
230
|
+
const includeObservations = this.options.includeObservations ?? true;
|
|
231
|
+
if (includeReflections && ctx.reflections.length > 0) {
|
|
232
|
+
for (const r of ctx.reflections) {
|
|
233
|
+
prepend.push(contextInjectionMessage(`[reflection] ${r.content}`));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (includeObservations && ctx.observations.length > 0) {
|
|
237
|
+
for (const o of ctx.observations) {
|
|
238
|
+
prepend.push(contextInjectionMessage(`[observation] ${o.content}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (prepend.length === 0) return;
|
|
242
|
+
const agentLike = event.agent;
|
|
243
|
+
agentLike.messages = [...prepend, ...agentLike.messages];
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
var INVOCATION_STEP_ID_KEY = "__neo4jReasoningStepId";
|
|
249
|
+
var TOOL_CALL_MAP_KEY = "__neo4jReasoningToolCalls";
|
|
250
|
+
async function registerReasoningHooks(memory, agent, options) {
|
|
251
|
+
return registerReasoningHooksOnAgent(memory, agent, options);
|
|
252
|
+
}
|
|
253
|
+
async function registerReasoningHooksOnAgent(memory, agent, options) {
|
|
254
|
+
const strands = await loadStrands();
|
|
255
|
+
const conversationId = options.conversationId;
|
|
256
|
+
agent.addHook(strands.BeforeInvocationEvent, async (event) => {
|
|
257
|
+
try {
|
|
258
|
+
const step = await memory.reasoning.recordStep({
|
|
259
|
+
conversationId,
|
|
260
|
+
reasoning: "agent invocation started",
|
|
261
|
+
actionTaken: "invoke_agent"
|
|
262
|
+
});
|
|
263
|
+
event.invocationState[INVOCATION_STEP_ID_KEY] = step.id;
|
|
264
|
+
event.invocationState[TOOL_CALL_MAP_KEY] = /* @__PURE__ */ new Map();
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
agent.addHook(strands.AfterInvocationEvent, async (event) => {
|
|
269
|
+
try {
|
|
270
|
+
const stepId = event.invocationState[INVOCATION_STEP_ID_KEY];
|
|
271
|
+
if (typeof stepId !== "string") return;
|
|
272
|
+
await memory.reasoning.recordStep({
|
|
273
|
+
conversationId,
|
|
274
|
+
reasoning: `agent invocation completed (step ${stepId})`,
|
|
275
|
+
actionTaken: "invocation_complete",
|
|
276
|
+
result: "ok"
|
|
277
|
+
});
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
agent.addHook(strands.BeforeToolCallEvent, async (event) => {
|
|
282
|
+
try {
|
|
283
|
+
const stepId = event.invocationState[INVOCATION_STEP_ID_KEY];
|
|
284
|
+
if (typeof stepId !== "string") return;
|
|
285
|
+
const toolCall = await memory.reasoning.recordToolCall(
|
|
286
|
+
stepId,
|
|
287
|
+
event.toolUse.name,
|
|
288
|
+
event.toolUse.input,
|
|
289
|
+
{ status: "pending" }
|
|
290
|
+
);
|
|
291
|
+
const map = event.invocationState[TOOL_CALL_MAP_KEY];
|
|
292
|
+
if (map instanceof Map) {
|
|
293
|
+
map.set(event.toolUse.toolUseId, toolCall.id);
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
agent.addHook(strands.AfterToolCallEvent, async (event) => {
|
|
299
|
+
try {
|
|
300
|
+
const stepId = event.invocationState[INVOCATION_STEP_ID_KEY];
|
|
301
|
+
if (typeof stepId !== "string") return;
|
|
302
|
+
await memory.reasoning.recordToolCall(
|
|
303
|
+
stepId,
|
|
304
|
+
event.toolUse.name,
|
|
305
|
+
event.toolUse.input,
|
|
306
|
+
{
|
|
307
|
+
status: event.error ? "failure" : "success",
|
|
308
|
+
error: event.error?.message
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async function connectMemoryToAgent(memory, options) {
|
|
316
|
+
const strands = await loadStrands();
|
|
317
|
+
const sessionManager = new strands.SessionManager({
|
|
318
|
+
sessionId: options.conversationId,
|
|
319
|
+
storage: { snapshot: new Neo4jSessionStorage(memory) }
|
|
320
|
+
});
|
|
321
|
+
const baseManager = new Neo4jConversationManager(memory, options);
|
|
322
|
+
const originalInit = baseManager.initAgent.bind(baseManager);
|
|
323
|
+
baseManager.initAgent = async (agent) => {
|
|
324
|
+
await originalInit(agent);
|
|
325
|
+
await registerReasoningHooksOnAgent(memory, agent, {
|
|
326
|
+
conversationId: options.conversationId
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
return {
|
|
330
|
+
sessionManager,
|
|
331
|
+
conversationManager: baseManager
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function defaultManifest() {
|
|
335
|
+
return {
|
|
336
|
+
schemaVersion: "1.0",
|
|
337
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function pickStrandsMessages(snapshot) {
|
|
341
|
+
const data = snapshot.data;
|
|
342
|
+
if (!data || !Array.isArray(data.messages)) return [];
|
|
343
|
+
return data.messages;
|
|
344
|
+
}
|
|
345
|
+
function stripMessagesFromSnapshot(snapshot) {
|
|
346
|
+
const nextData = { ...snapshot.data ?? {} };
|
|
347
|
+
delete nextData.messages;
|
|
348
|
+
return { ...snapshot, data: nextData };
|
|
349
|
+
}
|
|
350
|
+
function mergeMessagesIntoSnapshot(blob, messages) {
|
|
351
|
+
return {
|
|
352
|
+
...blob,
|
|
353
|
+
data: { ...blob.data ?? {}, messages }
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function strandsMessageToText(msg) {
|
|
357
|
+
const blocks = msg.content ?? [];
|
|
358
|
+
if (!Array.isArray(blocks)) return "";
|
|
359
|
+
const parts = [];
|
|
360
|
+
for (const b of blocks) {
|
|
361
|
+
if (b && typeof b === "object") {
|
|
362
|
+
const block = b;
|
|
363
|
+
if (typeof block.text === "string") {
|
|
364
|
+
parts.push(block.text);
|
|
365
|
+
} else if (block.type) {
|
|
366
|
+
parts.push(`[${block.type}]`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return parts.join("\n");
|
|
371
|
+
}
|
|
372
|
+
function toStrandsMessage(m) {
|
|
373
|
+
return {
|
|
374
|
+
role: m.role,
|
|
375
|
+
content: [{ text: m.content }]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function contextInjectionMessage(text) {
|
|
379
|
+
return {
|
|
380
|
+
role: "system",
|
|
381
|
+
content: [{ text }]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function sameStateBlob(a, b) {
|
|
385
|
+
return a.snapshotId === b.snapshotId && a.isLatest === b.isLatest && jsonLikeEqual(a.snapshot, b.snapshot);
|
|
386
|
+
}
|
|
387
|
+
function findLastStateBlobForSnapshotId(blobs, snapshotId) {
|
|
388
|
+
for (let i = blobs.length - 1; i >= 0; i--) {
|
|
389
|
+
if (blobs[i]?.snapshotId === snapshotId) return blobs[i];
|
|
390
|
+
}
|
|
391
|
+
return void 0;
|
|
392
|
+
}
|
|
393
|
+
function jsonLikeEqual(a, b) {
|
|
394
|
+
if (Object.is(a, b)) return true;
|
|
395
|
+
if (typeof a !== typeof b) return false;
|
|
396
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
397
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
398
|
+
return a.every((value, index) => jsonLikeEqual(value, b[index]));
|
|
399
|
+
}
|
|
400
|
+
if (a && b && typeof a === "object" && typeof b === "object") {
|
|
401
|
+
const aRecord = a;
|
|
402
|
+
const bRecord = b;
|
|
403
|
+
const aKeys = Object.keys(aRecord);
|
|
404
|
+
const bKeys = Object.keys(bRecord);
|
|
405
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
406
|
+
return aKeys.every((key) => key in bRecord && jsonLikeEqual(aRecord[key], bRecord[key]));
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export { Neo4jConversationManager, Neo4jSessionStorage, SYNTHETIC_MESSAGE_PREFIXES, connectMemoryToAgent, isSyntheticStrandsMessage, registerReasoningHooks };
|
|
412
|
+
//# sourceMappingURL=strands.js.map
|
|
413
|
+
//# sourceMappingURL=strands.js.map
|