Pluggable import/export between Flair and foreign memory systems.
Flair stores memories in its own schema. The rest of the agent ecosystem stores memories too — agentic-stack's .agent/ directory, Mem0, Letta, Zep, Anthropic memory, and more every week. Bridges let Flair speak those formats without a core-code change per integration.
The design goal is agent-authorable first: a capable agent should be able to ship a working bridge by reading this doc and running one command.
| Shape | When to use | Artifact |
|---|---|---|
| File (YAML descriptor) | Foreign system stores memories as files on disk (.agent/, JSONL, Markdown) |
.flair-bridge/<name>.yaml in a project, or ~/.flair/bridges/<name>.yaml user-wide |
| API (TypeScript plugin) | Foreign system exposes an HTTP API (Mem0, Letta, Zep, Anthropic memory) | npm package named flair-bridge-<name> |
Pick one. The runtime normalizes both into the same memory-record shape before import.
Every bridge deals in BridgeMemory objects. Fields:
interface BridgeMemory {
// Identity
id?: string // Flair ID if round-tripping; omit on first import
foreignId?: string // original ID in the foreign system (preserved)
// Content
content: string // REQUIRED
subject?: string
tags?: string[]
visibility?: "private" | "shared" | "public"
// Durability & lifecycle
durability?: "ephemeral" | "standard" | "persistent" | "permanent"
createdAt?: string // ISO-8601; defaults to now on import
validFrom?: string
validTo?: string
expiresAt?: string
// Ownership
agentId?: string // required on import unless --agent is passed
// Provenance
source?: string
derivedFrom?: string[]
}Required on import: content and (agentId or the --agent flag).
Flair-owned, never set by a bridge: contentHash, embedding, embeddingModel, retrievalCount, lastRetrieved, promotionStatus, _safetyFlags, any *By audit field. These are computed on ingest; if a bridge emits them they're ignored.
flair bridge list # installed bridges
flair bridge scaffold <name> [--file|--api] # emit starter files
flair bridge import <name> [src] [opts] # foreign → Flair (Shape A YAML / built-ins)
flair bridge test <name> [--fixture <path>] # round-trip diff (coming in slice 3)
flair bridge export <name> <dst> [opts] # Flair → foreign (coming in slice 3)list, scaffold, and import are live as of 0.6.0+. test and export are stubbed with pointers to slice 3.
Common runtime options for import:
| Flag | Meaning |
|---|---|
--agent <id> |
Default agent ID for memories that don't carry one (or set FLAIR_AGENT_ID) |
--cwd <dir> |
Filesystem root the descriptor's relative paths resolve against (default: cwd) |
--dry-run |
Parse + validate + count, don't write to Flair |
--port <port> |
Harper HTTP port |
--url <url> |
Flair base URL (overrides --port) |
--key <path> |
Ed25519 private key path (default: resolved from agent) |
agentic-stack ships as a built-in. Drop into a directory that has agentic-stack lessons and run:
$ flair bridge list
name kind source description
agentic-stack file builtin Import agentic-stack lessons.jsonl into Flair persistent memories
$ ls .agent/memory/semantic/
lessons.jsonl
$ flair bridge import agentic-stack --agent mybot --dry-run
agentic-stack: would import 47 memories. Re-run without --dry-run to write to Flair.
$ flair bridge import agentic-stack --agent mybot
47 imported (lesson-47)
agentic-stack: imported 47/47 memories.Each lesson lands as a Flair memory tagged source: "agentic-stack/lessons" with durability: persistent, the foreign id preserved as foreignId (so re-importing is idempotent on the same source).
A few things worth knowing:
--agentis required unless your descriptor maps anagentIdcolumn. If you forget,flair bridge importerrors with a one-line operator-pointer hint plus a structuredBridgeRuntimeErrorJSON on stderr.--dry-runis your friend. Validates the descriptor, parses every record, applies the mapping, but skips the PUT. Use it to confirm the count and check a few records before committing.- Output is throttled to one progress line every 2 seconds (or every 25 records, whichever comes first), so big imports don't flood your terminal.
- Errors are structured. Every error includes
bridge,op,path,record,field,expected,got,hint(per §10 of the spec). Thehintis the part you act on; the rest is for an LLM to self-correct without operator help.
For bridges whose source or target is a directory of files, write a YAML descriptor. No code required.
# .flair-bridge/agentic-stack.yaml
name: agentic-stack
version: 1
kind: file
description: "agentic-stack .agent/memory/semantic/lessons.jsonl"
detect:
anyExists:
- ".agent/AGENTS.md"
- ".agent/memory/semantic/lessons.jsonl"
import:
sources:
- path: ".agent/memory/semantic/lessons.jsonl"
format: jsonl
map:
content: "$.claim"
subject: "$.topic"
tags: "$.tags"
foreignId: "$.id"
durability: "persistent"
source: "agentic-stack/lessons"
export:
targets:
- path: ".agent/memory/semantic/lessons.jsonl"
format: jsonl
when: "durability in ['persistent', 'permanent']"
map:
id: "foreignId ?? id"
claim: "content"
topic: "subject"
tags: "tags"- Path expressions: JSONPath subset —
$.field,$.nested.field,$.array[*]. when:boolean expression overBridgeMemoryfields. Omit for "always".- Formats supported in 1.0:
jsonl,json,yaml,markdown-frontmatter. - What the runtime gives you: file discovery, format parsing, schema validation, error reporting with
line:column. You only describe the mapping.
For bridges that talk to HTTP APIs:
// flair-bridge-mem0/index.ts
import type { MemoryBridge, BridgeMemory, BridgeContext } from "@tpsdev-ai/flair";
export const bridge: MemoryBridge = {
name: "mem0",
version: 1,
kind: "api",
options: {
apiKey: { env: "MEM0_API_KEY", required: true, description: "Mem0 API token" },
userId: { required: true, description: "Mem0 user ID" },
baseUrl: { default: "https://api.mem0.ai" },
},
async *import(opts, ctx: BridgeContext) {
const res = await ctx.fetch(`${opts.baseUrl}/v1/memories/?user_id=${opts.userId}`, {
headers: { Authorization: `Token ${opts.apiKey}` },
});
const payload = await res.json();
for (const m of payload.results ?? []) {
yield {
foreignId: String(m.id),
content: String(m.memory),
createdAt: m.created_at,
tags: m.categories,
source: "mem0",
};
}
},
async export(memories, opts, ctx) {
for await (const m of memories) {
await ctx.fetch(`${opts.baseUrl}/v1/memories/`, {
method: "POST",
headers: { Authorization: `Token ${opts.apiKey}` },
body: JSON.stringify({
messages: [{ role: "user", content: m.content }],
user_id: opts.userId,
}),
});
}
},
};BridgeContext gives you:
ctx.fetch— instrumented, rate-limited HTTP. Always use this instead of the globalfetch— it's how the runtime applies per-bridge throttling and audit logging.ctx.log— structured{debug, info, warn, error}logger.ctx.cache— per-bridge key/value cache with optional TTL. Useful for pagination cursors, auth refresh, etc.
Code plugins have no direct filesystem or network access outside ctx. This keeps the surface area small and auditable.
flair bridge list scans, in precedence order:
- Built-ins shipped inside
@tpsdev-ai/flair - Project YAML —
.flair-bridge/*.yamlin the current working directory - User YAML —
~/.flair/bridges/*.yaml - npm packages — anything matching
flair-bridge-*or@scope/flair-bridge-*undernode_modules/
Earlier sources win on name conflict. So a built-in adapter can't be accidentally shadowed by a same-named npm package, but a project-local YAML can intentionally override an npm bridge for local development.
Publish bridges to npm as flair-bridge-<name>. Example package.json:
{
"name": "flair-bridge-mem0",
"version": "0.1.0",
"description": "Flair bridge for Mem0",
"main": "index.js",
"flair": { "kind": "api", "version": 1 },
"peerDependencies": { "@tpsdev-ai/flair": ">=0.6.0" }
}There's no registry to curate; npm is the registry. flair bridge list surfaces everything installed.
Every bridge ships a fixture. flair bridge test <name> will run (slice 2):
- Import the fixture →
BridgeMemory[] - Export those memories to a tmp target
- Re-import the tmp target →
BridgeMemory[] - Structural diff on the round-trip-stable fields:
content,subject,tags,durability - Pass iff the diff is empty
Pass = ship. Iterate against this signal.
Different sources have different blast radii.
| Source | Default trust |
|---|---|
| Built-in | Allowed |
| Declarative YAML (no code) | Allowed; remote API calls require --allow-remote at invocation |
| Code plugin from npm | Requires flair bridge allow <name> on first use |
| Remote API calls | Always require --allow-remote on the invocation |
A code plugin is untrusted JavaScript. The explicit allow gate keeps an unreviewed npm package from executing on your machine the first time flair bridge import names it. VM isolation is a 1.1 hardening pass.
Every bridge error is structured:
{
"bridge": "agentic-stack",
"op": "import",
"path": ".agent/memory/semantic/lessons.jsonl",
"record": 42,
"field": "$.claim",
"expected": "string",
"got": "null",
"hint": "lessons.jsonl row 42 has no 'claim' field — skipped"
}Printed as a single JSON object per line when --json; human-formatted otherwise. Field paths, expected/got, and a hint — that's the minimum an LLM needs to self-correct without operator help.
If you want an agent to write a bridge for you, here's the one-shot prompt:
You are implementing a Flair bridge for <FOREIGN_SYSTEM>. Read the sections above on the memory record, on Shape A (file-format targets) or Shape B (API targets) depending on <FOREIGN_SYSTEM>, and on round-trip testing. Produce either
.flair-bridge/<name>.yaml(file targets) or anindex.tsimplementingMemoryBridge(API targets). Include a fixture atfixtures/<name>.fixture.jsonorfixtures/<name>.mock.json. Runflair bridge test <name>and iterate until the round-trip diff is empty.
That's the bar. If an agent can't ship a working bridge from this doc plus the scaffold, the doc is the bug.
The authoritative contract is in specs/FLAIR-BRIDGES.md. This doc is the user-facing view; the spec covers edge cases, future extensions, and design rationale.