This guide describes how to embed Beetle Memory into a Rust project through bm-sdk.
Choose the profile that matches the deployment target and runtime role:
| Use case | Profile feature | ProfileId |
|---|---|---|
| Beetle Memory macOS standalone desktop app | profile-desktop-macos-standalone-memory |
ProfileId::DesktopMacosStandaloneMemory |
| Rust desktop host on macOS | profile-desktop-macos-embedded-sdk |
ProfileId::DesktopMacosEmbeddedSdk |
| Rust desktop host on Windows | profile-desktop-windows-embedded-sdk |
ProfileId::DesktopWindowsEmbeddedSdk |
| Linux hardware device runtime | profile-linux-device-standalone-memory |
ProfileId::LinuxDeviceStandaloneMemory |
| Linux server memory gateway | profile-server-linux-memory-gateway |
ProfileId::ServerLinuxMemoryGateway |
| ESP embedded SDK host | profile-esp-embedded-sdk |
ProfileId::EspEmbeddedSdk |
| ESP standalone memory runtime | profile-esp-standalone-memory |
ProfileId::EspStandaloneMemory |
From this repository:
[dependencies]
bm-sdk = { path = "crates/sdk", features = ["profile-desktop-macos-embedded-sdk"] }After the crates are published:
[dependencies]
bm-sdk = { version = "0.1.0", features = ["profile-desktop-macos-embedded-sdk"] }Use exactly one profile feature for a build.
For tests and short-lived sessions:
use bm_sdk::{ProfileId, StoreBackendConfig, StorePlatform};
let profile = ProfileId::DesktopMacosEmbeddedSdk;
let store = StorePlatform::open(StoreBackendConfig::in_memory(profile)?)?;For durable desktop or server storage:
let store = StorePlatform::open(StoreBackendConfig::file(
"/var/lib/beetle-memory",
ProfileId::ServerLinuxMemoryGateway,
)?)?;For sqlite-backed storage:
let store = StorePlatform::open(StoreBackendConfig::sqlite(
"/var/lib/beetle-memory/memory.sqlite3",
ProfileId::ServerLinuxMemoryGateway,
)?)?;ESP profiles should use StoreBackendConfig::embedded(profile) or in_memory(profile).
use bm_sdk::{AgentSkillDirConfig, MemoryIdentity, MemoryRuntime, MemoryScope, ProfileId};
let runtime = MemoryRuntime::builder()
.identity(MemoryIdentity::new("agent-main", "owner-default")?)
.scope(MemoryScope::new("local", "chat-1")?)
.profile(ProfileId::DesktopMacosEmbeddedSdk)
.store_platform(store)
.add_agent_skill_dir(AgentSkillDirConfig::read_only("./skills", "host-project"))
.build()?;agent_id identifies the agent instance. owner_id identifies the owner or tenant. Normal single-agent hosts do not pass subject_id: the SDK creates space:<owner_id> and the default agent:<agent_id> subject automatically, while hiding the system_governor / human_user / relationship graph details. Only advanced multi-subject hosts configure a custom subject registry, relationship graph, or mounted subject. channel and chat_id define the default memory scope for runtime operations.
add_agent_skill_dir is optional and read-only. The host still owns standard Agent Skill add/edit/import/delete/execute flows; Beetle Memory only scans SKILL.md summaries for recall and projection.
Procedural memory is the current direct write path for reusable runtime knowledge:
use bm_sdk::{MemoryWriteRequest, RuntimeSkillWrite, RuntimeSkillWriteSource};
let report = runtime.write(MemoryWriteRequest::Procedural {
writes: vec![RuntimeSkillWrite {
name: "release_guard".to_string(),
topic: "release".to_string(),
title: "Release guard".to_string(),
summary: "Verify release artifacts before publishing.".to_string(),
content: "Run examples, platform gates, and publish dry-run.".to_string(),
citations: vec!["integration-guide".to_string()],
source_chat_id: Some("chat-1".to_string()),
observed_at: 1_800_000_000,
}],
source: RuntimeSkillWriteSource::Manual,
})?;
assert!(report.accepted);Long-term extraction writes should be produced by the extraction pipeline and passed through MemoryWriteRequest::LongTermExtraction.
use bm_sdk::{
MemoryProjectionRequest, MemoryRecallRequest, PressureLevel, RuntimeLifecycleModeInput,
};
let recall = runtime.recall(MemoryRecallRequest {
query: "release artifacts".to_string(),
limit: 4,
tool_registry_refs: Vec::new(),
})?;
let projection = runtime.project(MemoryProjectionRequest {
user_query: "How should this host release?".to_string(),
system_max_len: 4096,
recent_messages_limit: 8,
pressure: PressureLevel::Normal,
mode_input: RuntimeLifecycleModeInput::default(),
tool_registry_refs: Vec::new(),
})?;
let memory_block = projection.system_memory_block;Use the projected memory block as part of your model-context assembly. Keep your host prompt assembly responsible for final ordering with system, developer, user, and tool messages.
MemoryRuntime::maintain is available for hosts that configure an LLM client. Generic adapters reject maintain because they cannot safely invent the LLM/HTTP boundary for the application.
let capabilities = runtime.capabilities();
if capabilities.lifecycle.maintain_lightweight.visible {
// Call runtime.maintain(...) from the host path that owns LLM injection.
}Hosts should submit candidate facts or procedures and let Beetle Memory decide which memory plane may change. This keeps SDK, HTTP, gateway, and future hosts on the same memory-governance contract.
use bm_sdk::{
LongTermMemoryKind, MemoryCandidateContent, MemoryCandidateTarget,
MemoryEvidenceAuthority, MemoryPrivacyClass, MemoryWriteCandidate,
MemoryWriteRequest,
};
runtime.write(MemoryWriteRequest::Candidates {
candidates: vec![MemoryWriteCandidate {
candidate_id: "turn-1:preferred-name".to_string(),
authority: MemoryEvidenceAuthority::UserAsserted,
target: MemoryCandidateTarget::LongTermMemory {
kind: LongTermMemoryKind::Profile,
topic: "preferred_name".to_string(),
},
privacy: MemoryPrivacyClass::SharedWithSubject,
content: MemoryCandidateContent::Text {
topic: "preferred_name".to_string(),
body: "The user prefers to be called Qingchuan.".to_string(),
keywords: vec!["name".to_string()],
},
evidence_refs: vec!["chat-1:turn-1".to_string()],
}],
})?;If post-turn LLM services are unavailable, finalize_turn_and_maintain still
commits the transcript and writes a deferred governance job under
memory/governance_jobs/pending.json. Run MemoryRuntime::run_due_governance
when services recover. The queue is isolated by memory space / subject / channel
/ chat / turn; hosts must not reimplement this queue or retry with host-owned
semantics.
Operator surfaces should use MemoryRuntime::deferred_governance_report() or
inspect.deferred_governance for pending / retrying / failed / terminal counts,
recent jobs, scope, subject, turn, reason, and last error for the current runtime
scope.
project() returns MemoryProjectionReport.audit as the projection diagnostic
source of truth. It includes source planes, selected ids, section chars,
source/render budgets, scope, and private gate decisions. Hosts may display
these fields, but must not infer projection behavior by reading store internals.
For conservative compaction, call MemoryRuntime::run_retention_compaction().
It only runs SDK-owned hygiene, factual evidence metadata compaction, and runtime
skill governance, and reports host_direct_deletion_allowed=false; quota
pressure must not let hosts delete accepted memory.
A complete SDK host turn uses one public path:
- Open a
StorePlatformor injectArc<dyn Platform>intoMemoryRuntime. - Build
MemoryIdentityandMemoryScopefrom stable host owner, agent, channel, and conversation ids. - Submit
MemoryWriteRequest::Candidatesfor facts, preferences, procedures, diagnostics, subject hints, and soul candidates. - Finalize the turn through canonical turn semantics when transcript governance is required.
- Use
recallandprojectto build model context; do not assemble memory planes in the host. - Use
inspectfor operator visibility and safe recovery context. - Use memory-space export, migration dry-run, apply/import, and replay for replacement or release gates.
Generic host fixtures and Beetle-derived fixtures under fixtures/sdk-host-readiness/ follow this same path. Beetle-derived data is legacy-shaped evidence only, not a special SDK branch.
use bm_sdk::{
apply_memory_space_migration, export_memory_space, preview_memory_space_migration,
ContinuitySnapshotImportMode, MemoryExportRequest, MemoryImportRequest,
MemoryReplayRequest, MemorySpaceExportRequest, MemorySpaceMigrateApplyRequest,
MemorySpaceMigratePreviewRequest,
};
let exported = runtime.export(MemoryExportRequest {
chat_id: "chat-1".to_string(),
})?;
runtime.import(MemoryImportRequest {
snapshot: exported.snapshot,
target_chat_id: "chat-2".to_string(),
mode: ContinuitySnapshotImportMode::FullRestore,
})?;
let replay = runtime.replay(MemoryReplayRequest {
chat_id: "chat-2".to_string(),
limit: 32,
})?;
let space = export_memory_space(
&store_platform,
MemorySpaceExportRequest {
memory_space_id: "space-main".to_string(),
include_private: true,
},
)?;
let preview = preview_memory_space_migration(MemorySpaceMigratePreviewRequest {
source_memory_space_id: "space-main".to_string(),
target_memory_space_id: "space-copy".to_string(),
snapshot: space.snapshot.clone(),
});
if !preview.loss_risk {
apply_memory_space_migration(
&target_store_platform,
MemorySpaceMigrateApplyRequest {
target_memory_space_id: "space-copy".to_string(),
snapshot: space.snapshot,
},
)?;
}Use BootstrapImport for limited bootstrap migration and FullRestore when restoring full continuity state.
Use memory-space export/preview/apply when replacing a host memory implementation or moving a configured SDK store.
Dry-run reports must be inspected before apply. Treat loss_risk, schema id, record counts, state fingerprint, event fingerprint, and privacy redaction count as release evidence. A host replacing its own memory implementation should keep a generic fixture and one host-derived legacy fixture in the readiness gate.
preview.manifest also reports whole-space snapshot mode, plane/privacy counts,
and subject remap state. Apply is still a whole-space snapshot import. When
source and target memory spaces differ, the manifest marks
subject_remap.required=true and applied=false; do not treat that as completed
subject key rewrite.
use bm_sdk::{MemoryInspectionRequest, PressureLevel, RuntimeLifecycleModeInput};
let inspect = runtime.inspect(MemoryInspectionRequest {
query: "migration readiness".to_string(),
system_max_len: 4096,
pressure: PressureLevel::Normal,
mode_input: RuntimeLifecycleModeInput::default(),
})?;
assert!(inspect.capabilities.inspection.visible);Operator inspect is the supported path for selected ids, plane evidence, capability visibility, deferred governance queue state, lifecycle diagnosis, and safe actions. A host UI may display this report, but it must not infer write decisions, replay state, or projection contents from private store files.
Hosts must not:
- write memory plane files directly;
- decide plane routing outside
MemoryRuntime; - maintain a second long-term extraction, subject, soul, private garden, or procedural write policy;
- build memory projection by reading store internals;
- treat Beetle, an IDE, Ollama, or a device channel as a kernel source kind;
- swallow deferred governance jobs or retry them with host-owned semantics;
- keep compatibility fields that pollute the current SDK contract.
let catalog = runtime.capabilities();
if catalog.adapter.http.visible {
// It is safe for this profile/policy/privacy combination to expose HTTP.
}Do not expose a protocol or operation just because the crate compiles. The capability catalog is the runtime truth.
Add a smoke test in the integrating project that:
- Opens the selected store backend.
- Builds
MemoryRuntime. - Injects
Arc<dyn Platform>intoMemoryRuntime. - Writes one
MemoryWriteCandidateand checks the governance report. - Finalizes one turn with maintenance unavailable and verifies a deferred job.
- Checks
deferred_governance_report()andinspect.deferred_governance. - Recalls or projects the candidate-backed memory from a different chat and checks
MemoryProjectionReport.audit. - Calls
run_retention_compaction()and verifies that host deletion of accepted memory is not allowed. - Runs migration dry-run and apply/import through the public memory-space migrator and checks
preview.manifest. - Runs operator inspect and replay against the migrated store.