diff --git a/src/_utils/__tests__/pagination.test.ts b/src/_utils/__tests__/pagination.test.ts new file mode 100644 index 0000000..8839c77 --- /dev/null +++ b/src/_utils/__tests__/pagination.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import { paginateAll } from '../../_utils/pagination.js' + +describe('paginateAll', () => { + it('should return all items from a single page', async () => { + const pages = [{ items: [1, 2, 3], nextToken: undefined as string | undefined }] + let callCount = 0 + + const result = await paginateAll( + () => Promise.resolve(pages[callCount++]!), + (page) => page.items, + (page) => page.nextToken, + ) + + expect(result).toEqual([1, 2, 3]) + expect(callCount).toBe(1) + }) + + it('should accumulate items across multiple pages', async () => { + const pages = [ + { items: [1, 2], nextToken: 'tok1' as string | undefined }, + { items: [3, 4], nextToken: 'tok2' as string | undefined }, + { items: [5], nextToken: undefined as string | undefined }, + ] + let callCount = 0 + const tokensReceived: (string | undefined)[] = [] + + const result = await paginateAll( + (nextToken) => { + tokensReceived.push(nextToken) + return Promise.resolve(pages[callCount++]!) + }, + (page) => page.items, + (page) => page.nextToken, + ) + + expect(result).toEqual([1, 2, 3, 4, 5]) + expect(tokensReceived).toEqual([undefined, 'tok1', 'tok2']) + }) + + it('should return empty array when page has no items', async () => { + const result = await paginateAll( + () => Promise.resolve({ items: undefined as number[] | undefined, nextToken: undefined }), + (page) => page.items, + (page) => page.nextToken, + ) + + expect(result).toEqual([]) + }) +}) diff --git a/src/_utils/__tests__/polling.test.ts b/src/_utils/__tests__/polling.test.ts new file mode 100644 index 0000000..1127796 --- /dev/null +++ b/src/_utils/__tests__/polling.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest' +import { pollUntil } from '../../_utils/polling.js' + +describe('pollUntil', () => { + it('should return true immediately when condition is met', async () => { + const result = await pollUntil( + () => Promise.resolve(true), + { maxWaitSeconds: 5, pollIntervalMs: 10 }, + ) + expect(result).toBe(true) + }) + + it('should poll until condition becomes true', async () => { + let calls = 0 + const result = await pollUntil( + () => Promise.resolve(++calls >= 3), + { maxWaitSeconds: 5, pollIntervalMs: 10 }, + ) + expect(result).toBe(true) + expect(calls).toBe(3) + }) + + it('should return false on timeout when no timeoutErrorMessage', async () => { + const result = await pollUntil( + () => Promise.resolve(false), + { maxWaitSeconds: 0.05, pollIntervalMs: 10 }, + ) + expect(result).toBe(false) + }) + + it('should throw on timeout when timeoutErrorMessage is set', async () => { + await expect( + pollUntil( + () => Promise.resolve(false), + { maxWaitSeconds: 0.05, pollIntervalMs: 10, timeoutErrorMessage: 'timed out' }, + ), + ).rejects.toThrow('timed out') + }) + + it('should swallow errors matching shouldSwallowError predicate', async () => { + let calls = 0 + const result = await pollUntil( + () => { + calls++ + if (calls < 2) throw new Error('transient') + return Promise.resolve(true) + }, + { maxWaitSeconds: 5, pollIntervalMs: 10, shouldSwallowError: () => true }, + ) + expect(result).toBe(true) + expect(calls).toBe(2) + }) + + it('should propagate errors not matched by shouldSwallowError', async () => { + await expect( + pollUntil( + () => { throw new Error('fatal') }, + { maxWaitSeconds: 5, pollIntervalMs: 10, shouldSwallowError: (err) => (err as Error).message !== 'fatal' }, + ), + ).rejects.toThrow('fatal') + }) + + it('should propagate all errors when shouldSwallowError is not provided', async () => { + await expect( + pollUntil( + () => { throw new Error('fatal') }, + { maxWaitSeconds: 5, pollIntervalMs: 10 }, + ), + ).rejects.toThrow('fatal') + }) +}) diff --git a/src/_utils/pagination.ts b/src/_utils/pagination.ts new file mode 100644 index 0000000..5b4205b --- /dev/null +++ b/src/_utils/pagination.ts @@ -0,0 +1,34 @@ +export const DEFAULT_PAGE_SIZE = 100 + +/** + * Paginate any SDK method that follows the nextToken request/response pattern. + * + * @param fetchPage - Fetches a single page, given an optional nextToken + * @param getItems - Extracts the item array from each page response + * @param getNextToken - Extracts the nextToken from each page response + * @returns All items accumulated across pages + * + * @example + * ```typescript + * const allEvents = await paginateAll( + * (nextToken) => client.listEvents({ memoryId, actorId, sessionId, nextToken }), + * (page) => page.events, + * (page) => page.nextToken, + * ); + * ``` + */ +export async function paginateAll( + fetchPage: (nextToken?: string) => Promise, + getItems: (output: TOutput) => TItem[] | undefined, + getNextToken: (output: TOutput) => string | undefined, +): Promise { + const items: TItem[] = [] + let nextToken: string | undefined + do { + const page = await fetchPage(nextToken) + const pageItems = getItems(page) + if (pageItems) items.push(...pageItems) + nextToken = getNextToken(page) + } while (nextToken) + return items +} diff --git a/src/_utils/polling.ts b/src/_utils/polling.ts new file mode 100644 index 0000000..11cbecc --- /dev/null +++ b/src/_utils/polling.ts @@ -0,0 +1,29 @@ +/** + * Poll a condition function until it returns `true` or the timeout expires. + * + * @param condition - Async function that returns `true` when done. Throwing is treated as "not done yet" when `swallowErrors` is true. + * @param opts - Polling options + * @returns `true` if the condition was met, `false` if timed out (only when `timeoutErrorMessage` is not set) + * @throws Error with `timeoutErrorMessage` if provided and timeout expires + */ +export async function pollUntil( + condition: () => Promise, + opts: { + maxWaitSeconds: number + pollIntervalMs: number + timeoutErrorMessage?: string + shouldSwallowError?: (err: unknown) => boolean + }, +): Promise { + const deadline = Date.now() + opts.maxWaitSeconds * 1000 + while (Date.now() < deadline) { + try { + if (await condition()) return true + } catch (err) { + if (!opts.shouldSwallowError?.(err)) throw err + } + await new Promise((resolve) => globalThis.setTimeout(resolve, opts.pollIntervalMs)) + } + if (opts.timeoutErrorMessage) throw new Error(opts.timeoutErrorMessage) + return false +} diff --git a/src/memory/__tests__/client.test.ts b/src/memory/__tests__/client.test.ts new file mode 100644 index 0000000..3ed1ebd --- /dev/null +++ b/src/memory/__tests__/client.test.ts @@ -0,0 +1,172 @@ +import { describe, it, expect } from 'vitest' +import type { BedrockAgentCore } from '@aws-sdk/client-bedrock-agentcore' +import type { BedrockAgentCoreControl } from '@aws-sdk/client-bedrock-agentcore-control' +import { MemoryClient } from '../client.js' +import { DATA_PLANE_METHODS, CONTROL_PLANE_METHODS } from '../types.js' + +function fakeControlPlane(overrides: Record unknown>): Record { + return new Proxy({} as Record, { + get: (_, method) => overrides[method as string] ?? (() => Promise.resolve({})), + }) +} + +function fakeDataPlane(overrides: Record unknown>): Record { + return new Proxy({} as Record, { + get: (_, method) => overrides[method as string] ?? (() => Promise.resolve({})), + }) +} + +describe('MemoryClient', () => { + describe('passthrough', () => { + const client = new MemoryClient({ region: 'us-west-2' }) + + it('exposes every data plane method as a function', () => { + for (const method of DATA_PLANE_METHODS) { + expect(typeof client[method]).toBe('function') + } + }) + + it('exposes every control plane method as a function', () => { + for (const method of CONTROL_PLANE_METHODS) { + expect(typeof client[method]).toBe('function') + } + }) + + it('does not expose arbitrary properties', () => { + expect((client as unknown as Record)['nonExistentMethod']).toBeUndefined() + }) + + it('constructs without config using defaults', () => { + expect(new MemoryClient()).toBeDefined() + }) + }) + + describe('memory() scoping', () => { + const client = new MemoryClient({ region: 'us-west-2' }) + const mem = client.memory('mem-1') + + it('exposes scoped methods as functions', () => { + expect(typeof mem.createEvent).toBe('function') + expect(typeof mem.retrieveMemoryRecords).toBe('function') + expect(typeof mem.listEvents).toBe('function') + expect(typeof mem.listActors).toBe('function') + }) + }) + + describe('createOrGetMemory()', () => { + it('returns existing memory when create throws already-exists', async () => { + const client = new MemoryClient({ + controlPlaneClient: fakeControlPlane({ + createMemory: () => { + throw Object.assign(new Error('already exists'), { name: 'ValidationException' }) + }, + getMemory: () => Promise.resolve({ memory: { id: 'mem-1' }, $metadata: {} }), + }) as unknown as BedrockAgentCoreControl, + }) + + const result = await client.createOrGetMemory({ name: 'test', eventExpiryDuration: 30 }) + expect(result.memory).toMatchObject({ id: 'mem-1' }) + }) + + it('propagates non-conflict errors', async () => { + const client = new MemoryClient({ + controlPlaneClient: fakeControlPlane({ + createMemory: () => { + throw Object.assign(new Error('throttled'), { name: 'ThrottlingException' }) + }, + }) as unknown as BedrockAgentCoreControl, + }) + + await expect(client.createOrGetMemory({ name: 'test', eventExpiryDuration: 30 })).rejects.toThrow('throttled') + }) + }) + + describe('deleteMemoryAndWait()', () => { + it('resolves when resource is not found after delete', async () => { + const client = new MemoryClient({ + controlPlaneClient: fakeControlPlane({ + deleteMemory: () => Promise.resolve({}), + getMemory: () => { + throw Object.assign(new Error(), { name: 'ResourceNotFoundException' }) + }, + }) as unknown as BedrockAgentCoreControl, + }) + + await expect( + client.deleteMemoryAndWait('mem-1', { maxWaitSeconds: 1, pollIntervalMs: 10 }) + ).resolves.toBeUndefined() + }) + }) + + describe('getLastKTurns()', () => { + it('groups messages into turns at USER boundaries', async () => { + const client = new MemoryClient({ + dataPlaneClient: fakeDataPlane({ + listEvents: () => + Promise.resolve({ + events: [ + { eventId: 'e1', payload: [{ conversational: { role: 'USER', content: { text: 'hi' } } }] }, + { eventId: 'e2', payload: [{ conversational: { role: 'ASSISTANT', content: { text: 'hello' } } }] }, + { eventId: 'e3', payload: [{ conversational: { role: 'USER', content: { text: 'bye' } } }] }, + { eventId: 'e4', payload: [{ conversational: { role: 'ASSISTANT', content: { text: 'goodbye' } } }] }, + ], + }), + }) as unknown as BedrockAgentCore, + }) + + const turns = await client.memory('mem-1').getLastKTurns({ actorId: 'a1', sessionId: 's1', k: 1 }) + expect(turns).toMatchObject([[{ role: 'USER' }, { role: 'ASSISTANT' }]]) + }) + + it('returns all turns when k exceeds total', async () => { + const client = new MemoryClient({ + dataPlaneClient: fakeDataPlane({ + listEvents: () => + Promise.resolve({ + events: [ + { eventId: 'e1', payload: [{ conversational: { role: 'USER', content: { text: 'hi' } } }] }, + { eventId: 'e2', payload: [{ conversational: { role: 'ASSISTANT', content: { text: 'hello' } } }] }, + ], + }), + }) as unknown as BedrockAgentCore, + }) + + const turns = await client.memory('mem-1').getLastKTurns({ actorId: 'a1', sessionId: 's1', k: 5 }) + expect(turns).toMatchObject([[{ role: 'USER' }, { role: 'ASSISTANT' }]]) + }) + + it('returns empty array for empty session', async () => { + const client = new MemoryClient({ + dataPlaneClient: fakeDataPlane({ + listEvents: () => Promise.resolve({ events: [] }), + }) as unknown as BedrockAgentCore, + }) + + const turns = await client.memory('mem-1').getLastKTurns({ actorId: 'a1', sessionId: 's1', k: 5 }) + expect(turns).toEqual([]) + }) + }) + + describe('listBranches()', () => { + it('aggregates branch info from events', async () => { + const client = new MemoryClient({ + dataPlaneClient: fakeDataPlane({ + listEvents: () => + Promise.resolve({ + events: [ + { eventId: 'e1' }, + { eventId: 'e2' }, + { eventId: 'e3', branch: { name: 'alt', rootEventId: 'e1' } }, + ], + }), + }) as unknown as BedrockAgentCore, + }) + + const branches = await client.memory('mem-1').listBranches({ actorId: 'a1', sessionId: 's1' }) + expect(branches).toMatchObject([ + { name: 'main', eventCount: 2 }, + { name: 'alt', eventCount: 1, rootEventId: 'e1' }, + ]) + }) + }) +}) diff --git a/src/memory/client.ts b/src/memory/client.ts new file mode 100644 index 0000000..d4c5d34 --- /dev/null +++ b/src/memory/client.ts @@ -0,0 +1,284 @@ +import { + BedrockAgentCore, + type Conversational, +} from '@aws-sdk/client-bedrock-agentcore' +import { + BedrockAgentCoreControl, + type CreateMemoryCommandInput, + type CreateMemoryCommandOutput, + type GetMemoryCommandInput, +} from '@aws-sdk/client-bedrock-agentcore-control' +import type { + MemoryClientConfig, + ScopedMemory, + WaitOptions, + WaitForMemoriesParams, + GetLastKTurnsParams, + BranchInfo, + DataPlaneMethods, + ControlPlaneMethods, +} from './types.js' +import { DATA_PLANE_METHODS, CONTROL_PLANE_METHODS } from './types.js' +import { paginateAll, DEFAULT_PAGE_SIZE } from '../_utils/pagination.js' +import { pollUntil } from '../_utils/polling.js' + +const DEFAULT_REGION = 'us-west-2' +const DATA_PLANE_SET: Set = new Set(DATA_PLANE_METHODS) +const CONTROL_PLANE_SET: Set = new Set(CONTROL_PLANE_METHODS) + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +interface MemoryPassthroughClient extends DataPlaneMethods, ControlPlaneMethods {} + +/** + * Passthrough layer that routes method calls to the appropriate AWS SDK client. + * Uses a Proxy to forward calls listed in DATA_PLANE_METHODS and CONTROL_PLANE_METHODS. + * See: https://www.typescriptlang.org/docs/handbook/declaration-merging.html + */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging, no-redeclare +class MemoryPassthroughClient { + protected readonly dataPlane: BedrockAgentCore + protected readonly controlPlane: BedrockAgentCoreControl + + constructor(config?: MemoryClientConfig) { + const region = config?.region ?? process.env.AWS_REGION ?? DEFAULT_REGION + const clientConfig = { + region, + ...(config?.credentialsProvider && { credentials: config.credentialsProvider }), + } + this.dataPlane = config?.dataPlaneClient ?? new BedrockAgentCore(clientConfig) + this.controlPlane = config?.controlPlaneClient ?? new BedrockAgentCoreControl(clientConfig) + + return new Proxy(this, { + get(target, prop, receiver): unknown { + if (typeof prop === 'string') { + if (DATA_PLANE_SET.has(prop)) { + const method = (target.dataPlane as unknown as Record unknown>)[prop]! + return method.bind(target.dataPlane) + } + if (CONTROL_PLANE_SET.has(prop)) { + const method = (target.controlPlane as unknown as Record unknown>)[prop]! + return method.bind(target.controlPlane) + } + } + return Reflect.get(target, prop, receiver) + }, + }) + } +} + +/** + * Client for AWS Bedrock AgentCore Memory. + * + * Passthrough methods from both AWS SDK clients are available directly on this + * class (e.g. `client.createEvent(...)`, `client.createMemory(...)`). + * + * Use `.memory(memoryId)` for memory-scoped operations. + * + * @example + * ```typescript + * const client = new MemoryClient({ region: 'us-west-2' }); + * + * // Passthrough — delegates to the right AWS SDK client + * await client.createMemory({ name: 'my-mem', eventExpiryDuration: 30, memoryStrategies: [] }); + * + * // Memory-scoped — memoryId auto-filled + * const mem = client.memory('mem-123'); + * await mem.listActors(); + * await mem.createEvent({ actorId: 'u1', sessionId: 's1', payload: [], eventTimestamp: new Date() }); + * ``` + */ +class MemoryClient extends MemoryPassthroughClient { + constructor(config?: MemoryClientConfig) { + super(config) + } + + async createMemoryAndWait( + input: CreateMemoryCommandInput, + opts?: WaitOptions, + ): Promise { + const result = await this.controlPlane.createMemory(input) + const memoryId = result.memory!.id! + await this.controlPlane.waitUntilMemoryCreated( + { memoryId } satisfies GetMemoryCommandInput, + { maxWaitTime: opts?.maxWaitSeconds ?? 300, minDelay: opts?.pollIntervalMs ? opts.pollIntervalMs / 1000 : 10 }, + ) + return result + } + + async createOrGetMemory(input: CreateMemoryCommandInput): Promise { + try { + return await this.controlPlane.createMemory(input) + } catch (err: unknown) { + if (hasName(err, 'ValidationException') && String(err).includes('already exists')) { + const resp = await this.controlPlane.getMemory({ memoryId: input.name }) + return { memory: resp.memory, $metadata: resp.$metadata } + } + throw err + } + } + + async deleteMemoryAndWait(memoryId: string, opts?: WaitOptions): Promise { + await this.controlPlane.deleteMemory({ memoryId }) + await pollUntil( + async () => { + try { + await this.controlPlane.getMemory({ memoryId }) + return false + } catch (err: unknown) { + if (hasName(err, 'ResourceNotFoundException')) return true + throw err + } + }, + { + maxWaitSeconds: opts?.maxWaitSeconds ?? 300, + pollIntervalMs: opts?.pollIntervalMs ?? 10_000, + timeoutErrorMessage: `Memory ${memoryId} was not deleted within ${opts?.maxWaitSeconds ?? 300}s`, + }, + ) + } + + async waitForMemories(params: WaitForMemoriesParams): Promise { + return pollUntil( + async () => { + const resp = await this.dataPlane.retrieveMemoryRecords({ + memoryId: params.memoryId, + namespace: params.namespace, + searchCriteria: { searchQuery: params.testQuery ?? 'test' }, + }) + return (resp.memoryRecordSummaries?.length ?? 0) > 0 + }, + { + maxWaitSeconds: params.maxWaitSeconds ?? 180, + pollIntervalMs: params.pollIntervalMs ?? 15_000, + shouldSwallowError: () => true, + }, + ) + } + + async getLastKTurns(params: GetLastKTurnsParams): Promise { + const listParams: Record = { + memoryId: params.memoryId, + actorId: params.actorId, + sessionId: params.sessionId, + includePayloads: true, + maxResults: DEFAULT_PAGE_SIZE, + } + + if (params.branchName && params.branchName !== 'main') { + listParams.filter = { + branch: { name: params.branchName, includeParentBranches: params.includeParentBranches ?? false }, + } + } + + const turns: Conversational[][] = [] + let currentTurn: Conversational[] = [] + let nextToken: string | undefined + + while (turns.length < params.k) { + const response = await this.dataPlane.listEvents({ ...listParams, nextToken } as Parameters[0]) + const events = response.events ?? [] + if (events.length === 0) break + + for (const event of events) { + for (const payloadItem of event.payload ?? []) { + if (payloadItem.conversational) { + if (payloadItem.conversational.role === 'USER' && currentTurn.length > 0) { + turns.push(currentTurn) + currentTurn = [] + } + currentTurn.push(payloadItem.conversational) + } + } + if (turns.length >= params.k) break + } + + nextToken = response.nextToken + if (!nextToken) break + } + + if (currentTurn.length > 0 && turns.length < params.k) { + turns.push(currentTurn) + } + + return turns.slice(0, params.k) + } + + async listBranches(params: { + memoryId: string + actorId: string + sessionId: string + }): Promise { + const allEvents = await paginateAll( + (nextToken) => this.dataPlane.listEvents({ memoryId: params.memoryId, actorId: params.actorId, sessionId: params.sessionId, includePayloads: false, nextToken }), + (page) => page.events, + (page) => page.nextToken, + ) + + const branches = new Map() + for (const event of allEvents) { + const name = event.branch?.name ?? 'main' + const existing = branches.get(name) + if (existing) { + existing.eventCount++ + } else { + branches.set(name, { + name, + rootEventId: event.branch?.rootEventId, + eventCount: 1, + }) + } + } + return Array.from(branches.values()) + } + + memory(memoryId: string): ScopedMemory { + const dp = this.dataPlane + const getLastK = this.getLastKTurns.bind(this) + const getBranches = this.listBranches.bind(this) + + const scoped: ScopedMemory = { + createEvent: (input) => + dp.createEvent({ ...input, memoryId }), + + listEvents: (input) => + dp.listEvents({ ...input, memoryId }), + + listAllEvents: (input) => + paginateAll( + (nextToken) => dp.listEvents({ ...input, memoryId, nextToken }), + (page) => page.events, + (page) => page.nextToken, + ), + + getEvent: (input) => + dp.getEvent({ ...input, memoryId }), + + deleteEvent: (input) => + dp.deleteEvent({ ...input, memoryId }), + + retrieveMemoryRecords: (input) => + dp.retrieveMemoryRecords({ ...input, memoryId }), + + listActors: () => + dp.listActors({ memoryId }), + + listSessions: (input) => + dp.listSessions({ memoryId, actorId: input.actorId }), + + getLastKTurns: (params) => + getLastK({ ...params, memoryId }), + + listBranches: (params) => + getBranches({ ...params, memoryId }), + } + + return scoped + } + +} + +function hasName(err: unknown, name: string): boolean { + return typeof err === 'object' && err !== null && 'name' in err && (err as { name: unknown }).name === name +} + +export { MemoryClient } diff --git a/src/memory/index.ts b/src/memory/index.ts new file mode 100644 index 0000000..74f07b9 --- /dev/null +++ b/src/memory/index.ts @@ -0,0 +1,10 @@ +export { MemoryClient } from './client.js' +export type { + MemoryClientConfig, + ScopedMemory, + BranchInfo, + WaitOptions, + WaitForMemoriesParams, + GetLastKTurnsParams, + MemoryEvent, +} from './types.js' diff --git a/src/memory/types.ts b/src/memory/types.ts new file mode 100644 index 0000000..ae32b47 --- /dev/null +++ b/src/memory/types.ts @@ -0,0 +1,100 @@ +import type { AwsCredentialIdentityProvider } from '@aws-sdk/types' +import type { + CreateEventCommandInput, + CreateEventCommandOutput, + ListEventsCommandInput, + ListEventsCommandOutput, + GetEventCommandInput, + GetEventCommandOutput, + DeleteEventCommandInput, + DeleteEventCommandOutput, + RetrieveMemoryRecordsCommandInput, + RetrieveMemoryRecordsCommandOutput, + ListActorsCommandOutput, + ListSessionsCommandOutput, + Event as MemoryEvent, + Conversational, + BedrockAgentCore, +} from '@aws-sdk/client-bedrock-agentcore' +import type { BedrockAgentCoreControl } from '@aws-sdk/client-bedrock-agentcore-control' + +export const DATA_PLANE_METHODS = [ + 'createEvent', + 'listEvents', + 'getEvent', + 'deleteEvent', + 'retrieveMemoryRecords', + 'getMemoryRecord', + 'deleteMemoryRecord', + 'listMemoryRecords', + 'listActors', + 'listSessions', + 'batchCreateMemoryRecords', + 'batchDeleteMemoryRecords', + 'batchUpdateMemoryRecords', + 'listMemoryExtractionJobs', + 'startMemoryExtractionJob', +] as const + +export const CONTROL_PLANE_METHODS = [ + 'createMemory', + 'getMemory', + 'listMemories', + 'updateMemory', + 'deleteMemory', +] as const + +export type DataPlaneMethods = Pick +export type ControlPlaneMethods = Pick + +export interface MemoryClientConfig { + region?: string + credentialsProvider?: AwsCredentialIdentityProvider + dataPlaneClient?: BedrockAgentCore + controlPlaneClient?: BedrockAgentCoreControl +} + +export interface WaitOptions { + maxWaitSeconds?: number + pollIntervalMs?: number +} + +type MemoryScoped = 'memoryId' + +export interface ScopedMemory { + createEvent(input: Omit): Promise + listEvents(input: Omit): Promise + listAllEvents(input: Omit): Promise + getEvent(input: Omit): Promise + deleteEvent(input: Omit): Promise + retrieveMemoryRecords(input: Omit): Promise + listActors(): Promise + listSessions(input: { actorId: string }): Promise + getLastKTurns(params: Omit): Promise + listBranches(params: { actorId: string; sessionId: string }): Promise +} + +export interface BranchInfo { + name: string + rootEventId?: string | undefined + eventCount: number +} + +export interface WaitForMemoriesParams { + memoryId: string + namespace: string + testQuery?: string + maxWaitSeconds?: number + pollIntervalMs?: number +} + +export interface GetLastKTurnsParams { + memoryId: string + actorId: string + sessionId: string + k: number + branchName?: string + includeParentBranches?: boolean +} + +export type { MemoryEvent }