Skip to content

feat(plugins): pluggable storage backends — interface + LocalFile + Activepieces - #35

Merged
rrader26 merged 1 commit into
mainfrom
feat/pluggable-storage
May 12, 2026
Merged

feat(plugins): pluggable storage backends — interface + LocalFile + Activepieces#35
rrader26 merged 1 commit into
mainfrom
feat/pluggable-storage

Conversation

@rrader26

@rrader26 rrader26 commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • `MemoryBackend` + `RecipeBackend` interfaces. File-based stores become one implementation each.
  • New production-ready `ActivepiecesMemoryBackend` — hits the real Activepieces agent-memory API (`/v1/projects/:projectId/chatbots/:chatbotId/memory`) with `Authorization: Bearer sk-*` auth.
  • Same agentmark binary now supports local-only + Activepieces-backed modes. Backend selection is config-time injection — no conditional code paths.
  • 100% backwards-compatible: existing `MemoryStore` / `RecipeStore` exports + `storePath` config still work.

What's NOT in this PR (deliberate)

  • `RemoteRecipeBackend` — no Activepieces recipes endpoints exist yet, so we don't ship a stub. Local-file only until that service ships. The interface is in place to drop a real impl in later.
  • Speculative HTTP shapes against unknown services. Everything that ships hits real production endpoints.

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)

agentmark Activepieces
`key` `metadata.agentmark_key`
`value` `content` (string-coerced) + `metadata.raw_value`
`scope.type` `scope` (same five-level enum)
`scope.id` `metadata.scope_id`
`tags` `metadata.tags`
`source` (filter) always written as "agentmark"

The `source: agentmark` stamp means `list`, `search`, and `clear` only see records this backend wrote — no collision with manually-created Activepieces memories.

Test plan

  • `pnpm build` clean
  • `pnpm test` — 491 pass / 10 skip (20 new for Activepieces backend: auth shape, project vs chatbot routing, set replace-semantics, value coercion, multi-scope get, scope_id disambiguation, search routing, list fallback, tag filtering, error handling, describe)
  • All 471 prior tests pass unchanged
  • Manual: configure an Activepieces API key + project from Claude Code, store + recall a memory cross-session, verify via the Activepieces UI that the record appears with `source=agentmark` and the right metadata fields

🤖 Generated with Claude Code

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>
@rrader26
rrader26 merged commit 82c2ed6 into main May 12, 2026
5 checks passed
@rrader26 rrader26 changed the title feat(plugins): pluggable storage backends for Memory + Recipes feat(plugins): pluggable storage backends — interface + LocalFile + Activepieces May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants