Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/tinymemory-api/src/null_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,10 @@ fn every_optional_method_fails_with_its_advertised_family_name() {
block_on(driver.bootstrap_connection("gmail", "conn-1")),
Capability::SourceSync,
);
assert_unsupported(
block_on(driver.is_toolkit_syncable("gmail")),
Capability::SourceSync,
);
assert_unsupported(
block_on(driver.source_sync_state("gmail", "conn-1")),
Capability::SourceSync,
Expand Down
30 changes: 30 additions & 0 deletions crates/tinymemory-api/src/provider/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,36 @@ pub trait MemorySourceSync: Send + Sync {
Err(MemoryError::unsupported(Capability::SourceSync))
}

/// Whether this driver has a sync pipeline for `toolkit`.
///
/// # Why a predicate and not the list
///
/// The answer depends on a normalisation rule — the driver trims and
/// lower-cases before matching — and a caller given the list would have to
/// reimplement that rule to use it. It would then be right until the day
/// the driver's rule changed, and wrong silently after. Asking the question
/// keeps the rule on the side that owns it.
///
/// # What a caller does with `false`
///
/// Not "refuse the connection". A toolkit with no pipeline is still a
/// perfectly good agent-tool integration; what it cannot do is become a
/// *memory source*. A host that registers one anyway ships a source that
/// reports healthy and then fails every sync, which is a worse answer than
/// not offering it — so this is the question to ask before registering,
/// not after a sync fails.
///
/// # Errors
///
/// [`MemoryError::Unsupported`] from a driver that serves this family but
/// cannot enumerate its pipelines. Deliberately not `Ok(false)`: "I have no
/// pipeline for this" and "I cannot tell you" are different answers, and a
/// caller that conflated them would silently stop registering every source.
async fn is_toolkit_syncable(&self, toolkit: &str) -> Result<bool, MemoryError> {
let _ = toolkit;
Err(MemoryError::unsupported(Capability::SourceSync))
}

async fn source_sync_state(
&self,
toolkit: &str,
Expand Down
2 changes: 1 addition & 1 deletion crates/tinymemory-bus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! the members that carry them.
//!
//! TinyMemory ships as a loadable `TinyBus` module: `crates/tinymemory-module`
//! exports one object with 122 members on it, built as a `cdylib`. A host that
//! exports one object with 123 members on it, built as a `cdylib`. A host that
//! loads it — OpenHuman — can call into it but cannot `use` anything out of it,
//! so the payload vocabulary has to be published as an ordinary library. This
//! is that library.
Expand Down
5 changes: 4 additions & 1 deletion crates/tinymemory-bus/src/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ pub mod methods {
pub const RUN_SOURCE_SYNC: &str = "RunSourceSync";
/// `BootstrapConnection` — run one connection's first-time bootstrap.
pub const BOOTSTRAP_CONNECTION: &str = "BootstrapConnection";
/// `IsToolkitSyncable` — whether this driver has a pipeline for a toolkit.
pub const IS_TOOLKIT_SYNCABLE: &str = "IsToolkitSyncable";
/// `SourceSyncState` — the persisted cursor and budget for one connection.
pub const SOURCE_SYNC_STATE: &str = "SourceSyncState";
/// `SyncAuditLog` — past sync runs, newest first.
Expand All @@ -321,7 +323,7 @@ pub mod methods {
/// The order matters: `tinybus`'s `Interface::members()` returns declaration
/// order, and the module compares the two sequences directly rather than as
/// sets, so a reordering is caught alongside an addition or a removal.
pub const METHODS: [&str; 122] = [
pub const METHODS: [&str; 123] = [
methods::DRIVER_ID,
methods::CAPABILITIES,
methods::HEALTH,
Expand Down Expand Up @@ -436,6 +438,7 @@ pub const METHODS: [&str; 122] = [
methods::RUN_CONNECTION_SYNC,
methods::RUN_SOURCE_SYNC,
methods::BOOTSTRAP_CONNECTION,
methods::IS_TOOLKIT_SYNCABLE,
methods::SOURCE_SYNC_STATE,
methods::SYNC_AUDIT_LOG,
methods::ESTIMATE_SYNC_COST_USD,
Expand Down
1 change: 1 addition & 0 deletions crates/tinymemory-module/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,7 @@ mod exports {
"RunConnectionSync",
"RunSourceSync",
"BootstrapConnection",
"IsToolkitSyncable",
"SourceSyncState",
"SyncAuditLog",
"EstimateSyncCostUsd",
Expand Down
13 changes: 13 additions & 0 deletions crates/tinymemory-module/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
//!
//! RunConnectionSync(toolkit, connection_id) -> SyncRunOutcome
//! BootstrapConnection(toolkit, connection_id) -> ()
//! IsToolkitSyncable(toolkit) -> bool
//! RunSourceSync(source_id) -> SyncRunOutcome
//! SourceSyncState(toolkit, connection_id) -> Option<SourceSyncState>
//! SyncAuditLog(limit) -> [SyncAuditEntry]
Expand Down Expand Up @@ -1764,6 +1765,18 @@ impl MemoryService {
.map_err(|error| into_bus_error(&error))
}

/// Whether this driver has a sync pipeline for one toolkit.
///
/// Asked rather than answered from a list the caller holds, so the
/// normalisation the driver applies never has to be reimplemented on the
/// far side of the bus.
async fn is_toolkit_syncable(&self, toolkit: String) -> BusResult<bool> {
require_family!(self, as_source_sync, Capability::SourceSync)
.is_toolkit_syncable(&toolkit)
.await
.map_err(|error| into_bus_error(&error))
}

/// The persisted cursor, dedup and budget state for one connection.
///
/// `None` is "never synced", which is a state and not an error — a status
Expand Down
1 change: 1 addition & 0 deletions crates/tinymemory-module/tests/module_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ const EXPECTED_METHODS: &[&str] = &[
"RunConnectionSync",
"RunSourceSync",
"BootstrapConnection",
"IsToolkitSyncable",
"SourceSyncState",
"SyncAuditLog",
"EstimateSyncCostUsd",
Expand Down
7 changes: 7 additions & 0 deletions crates/tinymemory-tinycortex/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2546,6 +2546,13 @@ impl MemorySourceSync for TinycortexProvider {
})
}

async fn is_toolkit_syncable(&self, toolkit: &str) -> Result<bool, MemoryError> {
// The same predicate `ensure_syncable_toolkit` gates on, exposed rather
// than inferred: a caller that learned this from a failed sync would
// already have registered the source it should not have.
Ok(tinymemory_core::sync::pipelines::host::is_composio_toolkit_syncable(toolkit))
}

async fn source_sync_state(
&self,
toolkit: &str,
Expand Down
Loading