-
Notifications
You must be signed in to change notification settings - Fork 427
feat(eve): memory - add portable file memory #1703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AndrewBarba
wants to merge
1
commit into
barba/first-class-memory
from
barba/user-memory-vercel-blob
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "eve": patch | ||
| --- | ||
|
|
||
| Add a scope-neutral `fileMemory()` provider with indexed save and forget tools, a configurable 100-memory default limit, a portable versioned-document backend, process-local development storage, and private Vercel Blob persistence selected automatically on Vercel. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { loadDeclaration } from "../_shared.mjs"; | ||
|
|
||
| /** Vendored server-side Vercel Blob slice used by the file-memory backend. */ | ||
| export default { | ||
| packageName: "@vercel/blob", | ||
| compiledPath: "@vercel/blob", | ||
| bundling: "standalone", | ||
| entries: [ | ||
| { | ||
| entry: "dist/index.js", | ||
| outputPath: "index", | ||
| declaration: await loadDeclaration("@vercel/blob.d.ts"), | ||
| }, | ||
| ], | ||
| }; |
45 changes: 45 additions & 0 deletions
45
packages/eve/scripts/vendor-compiled/declarations/@vercel/blob.d.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| export interface BlobCommandOptions { | ||
| readonly abortSignal?: AbortSignal; | ||
| readonly oidcToken?: string; | ||
| readonly storeId?: string; | ||
| readonly token?: string; | ||
| } | ||
|
|
||
| export interface GetCommandOptions extends BlobCommandOptions { | ||
| readonly access: "private" | "public"; | ||
| readonly useCache?: boolean; | ||
| } | ||
|
|
||
| export interface GetBlobResult { | ||
| readonly blob: { | ||
| readonly etag: string; | ||
| }; | ||
| readonly statusCode: 200 | 304; | ||
| readonly stream: ReadableStream<Uint8Array> | null; | ||
| } | ||
|
|
||
| export interface PutCommandOptions extends BlobCommandOptions { | ||
| readonly access: "private" | "public"; | ||
| readonly addRandomSuffix?: boolean; | ||
| readonly allowOverwrite?: boolean; | ||
| readonly cacheControlMaxAge?: number; | ||
| readonly contentType?: string; | ||
| readonly ifMatch?: string; | ||
| } | ||
|
|
||
| export interface PutBlobResult { | ||
| readonly etag: string; | ||
| } | ||
|
|
||
| export declare class BlobPreconditionFailedError extends Error {} | ||
|
|
||
| export declare function get( | ||
| pathname: string, | ||
| options: GetCommandOptions, | ||
| ): Promise<GetBlobResult | null>; | ||
|
|
||
| export declare function put( | ||
| pathname: string, | ||
| body: string, | ||
| options: PutCommandOptions, | ||
| ): Promise<PutBlobResult>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
packages/eve/src/execution/file-memory.integration.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { workflowEntry } from "#execution/workflow-entry.js"; | ||
| import { createTestRuntime } from "#internal/testing/app-harness.js"; | ||
| import { captureTurnEvents, filterEventsByType } from "#internal/testing/events.js"; | ||
| import { start } from "#internal/workflow/runtime.js"; | ||
| import { defineMemory } from "#public/memory/index.js"; | ||
| import { inMemory, fileMemory } from "#public/memory/file/index.js"; | ||
| import { createBundledRuntimeCompiledArtifactsSource } from "#runtime/compiled-artifacts-source.js"; | ||
|
|
||
| describe("file memory integration", () => { | ||
| it("saves, recalls, and isolates indexed memories across scopes", async () => { | ||
| const backend = inMemory(); | ||
| const runtime = createTestRuntime({ | ||
| agent: { name: "file-memory-integration" }, | ||
| memories: [ | ||
| { | ||
| definition: defineMemory({ | ||
| provider: fileMemory({ backend }), | ||
| scope: (context) => [context.session.auth.current!.principalId], | ||
| }), | ||
| slot: "facts", | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await runtime.run(async () => { | ||
| const first = await runTurn({ | ||
| message: "Call facts__save_memory with one concise memory.", | ||
| principalId: "user-1", | ||
| }); | ||
| const recalled = await runTurn({ | ||
| message: "Show the persistent context you received.", | ||
| principalId: "user-1", | ||
| }); | ||
| const isolated = await runTurn({ | ||
| message: "Show the persistent context you received.", | ||
| principalId: "user-2", | ||
| }); | ||
| expect( | ||
| first.some( | ||
| (event) => | ||
| event.type === "actions.requested" && | ||
| event.data.actions.some( | ||
| (action) => action.kind === "tool-call" && action.toolName === "facts__save_memory", | ||
| ), | ||
| ), | ||
| ).toBe(true); | ||
| const recalledMessage = filterEventsByType(recalled, "message.completed").at(-1)?.data | ||
| .message; | ||
| const isolatedMessage = filterEventsByType(isolated, "message.completed").at(-1)?.data | ||
| .message; | ||
| expect(recalledMessage).toContain("# Persistent memories"); | ||
| expect(recalledMessage).toContain("0: structured-output"); | ||
| expect(isolatedMessage).not.toContain("# Persistent memories"); | ||
| expect(isolatedMessage).not.toContain("structured-output"); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| async function runTurn(input: { readonly message: string; readonly principalId: string }) { | ||
| const run = await start(workflowEntry, [ | ||
| { | ||
| input: { message: input.message }, | ||
| serializedContext: { | ||
| "eve.auth": { | ||
| attributes: {}, | ||
| authenticator: "test", | ||
| principalId: input.principalId, | ||
| principalType: "user", | ||
| }, | ||
| "eve.bundle": { source: createBundledRuntimeCompiledArtifactsSource() }, | ||
| "eve.channel": { kind: "http", state: {} }, | ||
| "eve.continuationToken": `http:file-memory:${input.principalId}:${crypto.randomUUID()}`, | ||
| "eve.mode": "conversation", | ||
| }, | ||
| }, | ||
| ]); | ||
| const stream = captureTurnEvents(run); | ||
|
|
||
| try { | ||
| return await stream.nextTurn(); | ||
| } finally { | ||
| stream.dispose(); | ||
| await run.cancel(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /** One versioned text document loaded from a memory backend. */ | ||
| export interface MemoryDocument { | ||
| /** Complete UTF-8 document contents. */ | ||
| readonly content: string; | ||
| /** Opaque backend version used for optimistic writes. */ | ||
| readonly version: string; | ||
| } | ||
|
|
||
| /** Input shared by document reads. */ | ||
| export interface MemoryDocumentReadInput { | ||
| /** Stable eve scope key for the authored memory slot. */ | ||
| readonly key: string; | ||
| readonly signal: AbortSignal; | ||
| } | ||
|
|
||
| /** Input for a conditional document replacement. */ | ||
| export interface MemoryDocumentWriteInput extends MemoryDocumentReadInput { | ||
| readonly content: string; | ||
| /** Version returned by {@link MemoryDocumentBackend.read}, or `null` for create-only. */ | ||
| readonly expectedVersion: string | null; | ||
| } | ||
|
|
||
| /** | ||
| * Storage seam for one bounded memory file per eve scope key. | ||
| * | ||
| * Implementations may map the key to a KV entry, blob object, database row, | ||
| * or another durable store. Writes must reject stale `expectedVersion` values | ||
| * with {@link MemoryDocumentConflictError}. | ||
| */ | ||
| export interface MemoryDocumentBackend { | ||
| readonly read: (input: MemoryDocumentReadInput) => Promise<MemoryDocument | null>; | ||
| readonly write: (input: MemoryDocumentWriteInput) => Promise<MemoryDocument>; | ||
| } | ||
|
|
||
| /** Raised when a document changed between read and conditional write. */ | ||
| export class MemoryDocumentConflictError extends Error { | ||
| readonly key: string; | ||
|
|
||
| constructor(key: string) { | ||
| super(`Memory document "${key}" changed before it could be updated.`); | ||
| this.name = "MemoryDocumentConflictError"; | ||
| this.key = key; | ||
| } | ||
|
|
||
| /** Narrows conflicts across bundle and workflow boundaries. */ | ||
| static is(error: unknown): error is MemoryDocumentConflictError { | ||
| return ( | ||
| error instanceof MemoryDocumentConflictError || | ||
| (typeof error === "object" && | ||
| error !== null && | ||
| (error as { readonly name?: unknown }).name === "MemoryDocumentConflictError" && | ||
| typeof (error as { readonly key?: unknown }).key === "string") | ||
| ); | ||
| } | ||
| } |
47 changes: 47 additions & 0 deletions
47
packages/eve/src/public/memory/file/backends/default.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { get, put } from "#compiled/@vercel/blob/index.js"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| import { defaultFileMemoryBackend } from "#public/memory/file/backends/default.js"; | ||
|
|
||
| vi.mock("#compiled/@vercel/blob/index.js", () => ({ | ||
| BlobPreconditionFailedError: class BlobPreconditionFailedError extends Error {}, | ||
| get: vi.fn(), | ||
| put: vi.fn(), | ||
| })); | ||
|
|
||
| const originalVercel = process.env.VERCEL; | ||
| const signal = new AbortController().signal; | ||
|
|
||
| describe("default file-memory backend", () => { | ||
| afterEach(() => { | ||
| vi.clearAllMocks(); | ||
| if (originalVercel === undefined) delete process.env.VERCEL; | ||
| else process.env.VERCEL = originalVercel; | ||
| }); | ||
|
|
||
| it("uses process-local storage outside Vercel and caches that selection", async () => { | ||
| delete process.env.VERCEL; | ||
| const backend = defaultFileMemoryBackend(); | ||
| await backend.write({ content: "local", expectedVersion: null, key: "mem_a", signal }); | ||
| process.env.VERCEL = "1"; | ||
|
|
||
| await expect(backend.read({ key: "mem_a", signal })).resolves.toMatchObject({ | ||
| content: "local", | ||
| }); | ||
| expect(get).not.toHaveBeenCalled(); | ||
| expect(put).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("defers Vercel Blob selection until the first operation", async () => { | ||
| delete process.env.VERCEL; | ||
| const backend = defaultFileMemoryBackend(); | ||
| process.env.VERCEL = "1"; | ||
| vi.mocked(get).mockResolvedValue(null); | ||
|
|
||
| await expect(backend.read({ key: "mem_a", signal })).resolves.toBeNull(); | ||
| expect(get).toHaveBeenCalledWith( | ||
| "eve/memory/file/mem_a/MEMORY.md", | ||
| expect.objectContaining({ access: "private", useCache: false }), | ||
| ); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this token? Let's recommend OIDC