@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.
@@ -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