feat(plugins): pluggable storage backends — interface + LocalFile + Activepieces - #35
Merged
Conversation
Same agentmark binary now supports three deployment modes:
- Local-only (default): file-based, single-user laptop
- On-prem sync: RemoteMemoryBackend / RemoteRecipeBackend → internal HTTP service
- Cloud sync: same Remote backends → cloud HTTP service
Only the backend configuration differs at construction time; tool
handlers + plugin code paths are identical. This is the architecture
needed for ThinkFleet Desktop to ship one binary that works for
single-user developers, on-prem enterprises, and cloud SMB.
New types + classes (memory):
MemoryBackend interface
MemoryBackendDescription describe() shape
MemorySetInput set() input shape
LocalFileMemoryBackend default impl (renamed from MemoryStore)
RemoteMemoryBackend HTTP impl
RemoteMemoryError thrown on non-2xx
New types + classes (recipes): same pattern with RecipeBackend +
LocalFileRecipeBackend + RemoteRecipeBackend + RemoteRecipeError.
Plugin factories now accept `backend?: MemoryBackend / RecipeBackend`.
When omitted, the local-file backend is constructed as before (full
backwards compat). Pass a Remote backend to switch:
const memory = createMemoryPlugin({
backend: new RemoteMemoryBackend({
baseUrl: 'https://memory.thinkfleet.ai',
token: process.env.THINKFLEET_TOKEN,
workspaceId: 'ws_acme',
}),
})
Auth: Bearer token + optional X-Workspace-Id header for team scoping.
A single user can be a member of multiple workspaces by instantiating
multiple backends with different workspace ids.
Endpoint contracts (v1 proposal — subject to change before the service
ships) documented in the remote-backend.ts files. Memory uses POST
/v1/memory/records/get to avoid query-string size limits on the
scopes array; Recipes uses standard REST shapes.
Backwards compat:
- `MemoryStore` export remains, aliased to `LocalFileMemoryBackend`.
- `RecipeStore` export remains, aliased to `LocalFileRecipeBackend`.
- `createMemoryPlugin({ storePath, maxRecords, defaultScope })` still
works — wires to the local file backend.
- `createRecipesPlugin({ storePath })` still works — same.
Tests (21 new, 492 total): RemoteMemoryBackend request shape
(headers, baseUrl normalisation, verb mapping for every endpoint),
error handling (RemoteMemoryError with status + body), describe()
merging; same shape for RemoteRecipeBackend including 404→null/false
semantics for get/delete. All 471 prior tests pass unchanged (the
file-based backends behave identically — same class behind the
deprecated name).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
What's NOT in this PR (deliberate)
Architecture
```ts
interface MemoryBackend {
set(input): Promise
get(input): Promise<MemoryRecord | null>
deleteById(id): Promise
deleteByKey(key, scope): Promise
search(query): Promise<MemoryRecord[]>
list(scope?, prefix?, limit?): Promise<MemoryRecord[]>
clear(): Promise
describe(): Promise
}
class LocalFileMemoryBackend implements MemoryBackend { /* current file-based behavior / }
class ActivepiecesMemoryBackend implements MemoryBackend { / hits production Activepieces API */ }
```
Usage
```ts
import { createMemoryPlugin, ActivepiecesMemoryBackend } from '@thinkfleet/agentmark'
const memory = createMemoryPlugin({
backend: new ActivepiecesMemoryBackend({
baseUrl: process.env.AP_BASE_URL,
apiKey: process.env.AP_API_KEY, // must start with sk-
projectId: process.env.AP_PROJECT_ID,
chatbotId: process.env.AP_CHATBOT_ID, // optional
}),
})
createMcpServer({ plugins: [web, pdf, desktop, memory, meta] })
```
The same Claude Code / Cursor / Codex agent now writes to real Activepieces memory instead of a local file — and gets hybrid vector + BM25 search for free since the agentmark `search` tool routes to `/memory/search`.
Mapping (agentmark K/V → Activepieces rich shape)
The `source: agentmark` stamp means `list`, `search`, and `clear` only see records this backend wrote — no collision with manually-created Activepieces memories.
Test plan
🤖 Generated with Claude Code