Skip to content

Commit afd3a11

Browse files
committed
Merge remote-tracking branch 'origin/main' into memory-module-port
# Conflicts: # vendor/tinycortex
2 parents d65c639 + cfb4d08 commit afd3a11

14 files changed

Lines changed: 865 additions & 110 deletions

File tree

Cargo.lock

Lines changed: 16 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,40 @@ that skips enforcement is the entire reason the policy layer exists.
7272

7373
## Remote engines
7474

75-
The `tinymemory-remote` crate supports the self-hosted native APIs of
76-
Supermemory, Mem0, and Cognee. Each adapter stores TinyMemory's key, category,
77-
session, and provenance in backend metadata (or a Cognee raw-data envelope), so
78-
exact CRUD and portability survive the seam while recall remains engine-native.
75+
The `tinymemory-remote` crate supports the managed and self-hosted native APIs
76+
of Supermemory and Cognee, plus self-hosted Mem0. Each adapter stores
77+
TinyMemory's key, category, session, and provenance in backend metadata (or a
78+
Cognee raw-data envelope), so exact CRUD and portability survive the seam while
79+
recall remains engine-native. Provider-facing dataset names, container tags,
80+
and filenames are bounded stable hashes, so every namespace and key accepted by
81+
the TinyMemory contract remains valid on the remote API.
7982

8083
```rust
8184
use tinymemory_remote::{SupermemoryMemory, supermemory_provider};
8285

83-
let memory = SupermemoryMemory::new("http://localhost:6767", Some("sm_..."))?;
86+
let memory = SupermemoryMemory::self_hosted("http://localhost:6767", "sm_...")?;
8487
let provider = supermemory_provider(memory);
8588
# Ok::<_, anyhow::Error>(provider)
8689
```
8790

91+
Managed APIs have explicit constructors so their authentication cannot be
92+
confused with a self-hosted token:
93+
94+
```rust
95+
use tinymemory_remote::{CogneeMemory, SupermemoryMemory};
96+
97+
let cognee = CogneeMemory::cloud("cognee-api-key")?;
98+
let supermemory = SupermemoryMemory::cloud("sm_...")?;
99+
100+
// Cognee also issues tenant-specific API origins.
101+
let tenant = CogneeMemory::api("https://tenant.example.cognee.ai", "api-key")?;
102+
# Ok::<_, anyhow::Error>((cognee, supermemory, tenant))
103+
```
104+
105+
Cognee Cloud uses `X-Api-Key`; authenticated self-hosted Cognee uses a bearer
106+
access token. Supermemory uses bearer API keys for both deployment modes. All
107+
constructors redact credentials from `Debug` output and transport errors.
108+
88109
All three advertise the mandatory Core, Recall, and Portability families. The
89110
live Docker harness and conformance command are documented in
90111
[`integration/remote-engines/`](integration/remote-engines/README.md).

adapters/remote/examples/conformance.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ use tinymemory_remote::{
1313

1414
/// Builds the command-line usage error returned for invalid arguments.
1515
fn usage() -> anyhow::Error {
16-
anyhow::anyhow!("usage: conformance <supermemory|mem0|cognee> <endpoint> [credential]")
16+
anyhow::anyhow!(
17+
"usage: conformance <supermemory|mem0|cognee|cognee-api> <endpoint> [credential]"
18+
)
1719
}
1820

1921
#[tokio::main]
@@ -36,6 +38,12 @@ async fn main() -> anyhow::Result<()> {
3638
&endpoint,
3739
credential.as_deref(),
3840
)?)),
41+
"cognee-api" => Arc::new(cognee_provider(CogneeMemory::api(
42+
&endpoint,
43+
credential
44+
.as_deref()
45+
.ok_or_else(|| anyhow::anyhow!("cognee-api requires a credential"))?,
46+
)?)),
3947
_ => return Err(usage()),
4048
};
4149

adapters/remote/src/cognee.rs

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,22 @@ use tinymemory_api::recall::RecallOpts;
88
use tinymemory_api::traits::Memory;
99
use tinymemory_api::types::MemoryTaint;
1010

11-
use crate::common::{encode, Dialect, HttpClient, RemoteMemory, StoredEntry};
11+
use crate::common::{stable_id, Dialect, HttpClient, RemoteMemory, StoredEntry};
1212

1313
/// Stable driver id used by configuration and status output.
1414
pub use tinymemory::registry::COGNEE_DRIVER_ID;
1515

16-
/// A self-hosted Cognee server exposed through TinyMemory's storage contract.
16+
/// Default base URL for Cognee's managed API.
17+
pub const COGNEE_API_ENDPOINT: &str = "https://api.cognee.ai";
18+
19+
/// A Cognee managed or self-hosted service exposed through TinyMemory's contract.
1720
#[derive(Debug)]
1821
pub struct CogneeMemory {
1922
inner: RemoteMemory<CogneeDialect>,
2023
}
2124

2225
impl CogneeMemory {
23-
/// Connect to a Cognee server.
26+
/// Connect to a self-hosted Cognee server.
2427
///
2528
/// `access_token` is sent as a bearer token. Local deployments with
2629
/// backend access control disabled may pass `None`.
@@ -29,12 +32,53 @@ impl CogneeMemory {
2932
///
3033
/// Returns an error when `endpoint` is not an HTTP(S) URL.
3134
pub fn new(endpoint: &str, access_token: Option<&str>) -> anyhow::Result<Self> {
35+
Self::self_hosted(endpoint, access_token)
36+
}
37+
38+
/// Connect to a self-hosted Cognee server.
39+
///
40+
/// `access_token` is sent as a bearer token. Local deployments with
41+
/// authentication disabled may pass `None`.
42+
///
43+
/// # Errors
44+
///
45+
/// Returns an error when `endpoint` is not an HTTP(S) URL.
46+
pub fn self_hosted(endpoint: &str, access_token: Option<&str>) -> anyhow::Result<Self> {
3247
Ok(Self {
3348
inner: RemoteMemory::new(CogneeDialect {
3449
client: HttpClient::bearer(endpoint, access_token)?,
3550
}),
3651
})
3752
}
53+
54+
/// Connect to a Cognee managed API using `X-Api-Key` authentication.
55+
///
56+
/// This accepts a custom endpoint because Cognee Cloud may issue a
57+
/// tenant-specific base URL. Use [`Self::cloud`] for the shared default.
58+
///
59+
/// # Errors
60+
///
61+
/// Returns an error when `endpoint` is invalid or `api_key` is blank.
62+
pub fn api(endpoint: &str, api_key: &str) -> anyhow::Result<Self> {
63+
anyhow::ensure!(
64+
!api_key.trim().is_empty(),
65+
"cognee API key must not be empty"
66+
);
67+
Ok(Self {
68+
inner: RemoteMemory::new(CogneeDialect {
69+
client: HttpClient::api_key(endpoint, Some(api_key))?,
70+
}),
71+
})
72+
}
73+
74+
/// Connect to Cognee's shared managed API endpoint.
75+
///
76+
/// # Errors
77+
///
78+
/// Returns an error when `api_key` is blank.
79+
pub fn cloud(api_key: &str) -> anyhow::Result<Self> {
80+
Self::api(COGNEE_API_ENDPOINT, api_key)
81+
}
3882
}
3983

4084
#[async_trait]
@@ -128,11 +172,11 @@ struct Dataset {
128172
impl CogneeDialect {
129173
/// Encodes a TinyMemory namespace as a collision-free Cognee dataset name.
130174
fn dataset_name(namespace: &str) -> String {
131-
format!("tinymemory__{}", encode(namespace))
175+
format!("tinymemory__{}", stable_id("dataset", namespace))
132176
}
133177
/// Encodes a TinyMemory key as the uploaded envelope's filename.
134178
fn filename(key: &str) -> String {
135-
format!("{}.tinymemory.json", encode(key))
179+
format!("{}.tinymemory.json", stable_id("key", key))
136180
}
137181

138182
/// Discovers only datasets owned by the TinyMemory adapter.
@@ -239,33 +283,45 @@ impl Dialect for CogneeDialect {
239283

240284
/// Replaces an existing envelope and uploads the new exact record.
241285
async fn upsert(&self, entry: StoredEntry) -> anyhow::Result<()> {
242-
if let Some(existing) = self
286+
let existing = self
243287
.entries()
244288
.await?
245289
.into_iter()
246-
.find(|item| item.namespace == entry.namespace && item.key == entry.key)
247-
{
248-
self.delete_entry(&existing).await?;
249-
}
290+
.find(|item| item.namespace == entry.namespace && item.key == entry.key);
250291
let body = serde_json::to_vec(&entry)?;
251-
let form = multipart::Form::new()
252-
.text("datasetName", Self::dataset_name(&entry.namespace))
253-
.text("run_in_background", "false")
254-
.part(
255-
"data",
256-
multipart::Part::bytes(body)
257-
.file_name(Self::filename(&entry.key))
258-
.mime_str("application/json")?,
259-
);
292+
let form = multipart::Form::new().part(
293+
"data",
294+
multipart::Part::bytes(body)
295+
.file_name(Self::filename(&entry.key))
296+
.mime_str("application/json")?,
297+
);
298+
let (method, path, form) = if let Some(existing) = existing {
299+
let (dataset_id, data_id) = existing
300+
.remote_id
301+
.split_once(':')
302+
.ok_or_else(|| anyhow!("Cognee record has no dataset id"))?;
303+
(
304+
Method::PATCH,
305+
format!("api/v1/update?data_id={data_id}&dataset_id={dataset_id}"),
306+
form,
307+
)
308+
} else {
309+
(
310+
Method::POST,
311+
"api/v1/remember".to_owned(),
312+
form.text("datasetName", Self::dataset_name(&entry.namespace))
313+
.text("run_in_background", "false"),
314+
)
315+
};
260316
let response = self
261317
.client
262-
.multipart("api/v1/remember")?
318+
.multipart(method, &path)?
263319
.multipart(form)
264320
.send()
265321
.await?;
266322
if !response.status().is_success() {
267323
return Err(anyhow!(
268-
"memory API api/v1/remember returned HTTP {}",
324+
"memory API {path} returned HTTP {}",
269325
response.status()
270326
));
271327
}

0 commit comments

Comments
 (0)