use std::fmt;
use std::sync::Arc;
use meerkat_core::AgentToolDispatcher;
use crate::{AgentIdentity, DesiredLocalCallbackTool, MobId};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentityLocalMaterializationKey {
mob_id: MobId,
identity: AgentIdentity,
intent_revision: u64,
intent_digest: String,
intent_authority_digest: String,
}
impl IdentityLocalMaterializationKey {
#[must_use]
pub fn new(
mob_id: MobId,
identity: AgentIdentity,
intent_revision: u64,
intent_digest: impl Into<String>,
intent_authority_digest: impl Into<String>,
) -> Self {
Self {
mob_id,
identity,
intent_revision,
intent_digest: intent_digest.into(),
intent_authority_digest: intent_authority_digest.into(),
}
}
#[must_use]
pub fn mob_id(&self) -> &MobId {
&self.mob_id
}
#[must_use]
pub fn identity(&self) -> &AgentIdentity {
&self.identity
}
#[must_use]
pub const fn intent_revision(&self) -> u64 {
self.intent_revision
}
#[must_use]
pub fn intent_digest(&self) -> &str {
&self.intent_digest
}
#[must_use]
pub fn intent_authority_digest(&self) -> &str {
&self.intent_authority_digest
}
}
impl fmt::Display for IdentityLocalMaterializationKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{}/{}@{}",
self.mob_id, self.identity, self.intent_revision
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum IdentityLocalExternalToolsError {
#[error("no process-local external-tool services are registered")]
Missing,
#[error("process-local external-tool services are registered for stale authority {registered}")]
AuthorityMismatch {
registered: IdentityLocalMaterializationKey,
},
#[error(
"process-local external-tool definitions do not match sealed desired material: {detail}"
)]
DefinitionMismatch { detail: String },
#[error("process-local external-tool services are temporarily unavailable: {reason}")]
Unavailable { reason: String },
}
pub trait IdentityLocalExternalToolsProvider: Send + Sync {
fn external_tools_for(
&self,
key: &IdentityLocalMaterializationKey,
) -> Result<Option<Arc<dyn AgentToolDispatcher>>, IdentityLocalExternalToolsError>;
}
impl<F> IdentityLocalExternalToolsProvider for F
where
F: Fn(
&IdentityLocalMaterializationKey,
) -> Result<Option<Arc<dyn AgentToolDispatcher>>, IdentityLocalExternalToolsError>
+ Send
+ Sync,
{
fn external_tools_for(
&self,
key: &IdentityLocalMaterializationKey,
) -> Result<Option<Arc<dyn AgentToolDispatcher>>, IdentityLocalExternalToolsError> {
self(key)
}
}
pub(super) fn resolve_identity_local_external_tools(
provider: Option<&dyn IdentityLocalExternalToolsProvider>,
key: &IdentityLocalMaterializationKey,
required: &[DesiredLocalCallbackTool],
) -> Result<Option<Arc<dyn AgentToolDispatcher>>, IdentityLocalExternalToolsError> {
if required.is_empty() {
return Ok(None);
}
let dispatcher = provider
.ok_or(IdentityLocalExternalToolsError::Missing)?
.external_tools_for(key)?
.ok_or(IdentityLocalExternalToolsError::Missing)?;
let dispatcher_tools = dispatcher.tools();
if dispatcher_tools
.iter()
.any(|tool| tool.provenance.is_some())
{
return Err(IdentityLocalExternalToolsError::DefinitionMismatch {
detail: "identity-local callback dispatchers must expose definitions without independently minted provenance".to_string(),
});
}
let mut observed = dispatcher_tools
.iter()
.map(|tool| DesiredLocalCallbackTool {
name: tool.name.clone(),
description: tool.description.clone(),
input_schema: tool.input_schema.clone(),
})
.collect::<Vec<_>>();
observed.sort_by(|left, right| left.name.cmp(&right.name));
if observed != required {
let expected_names = required
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>()
.join(", ");
let observed_names = observed
.iter()
.map(|tool| tool.name.as_str())
.collect::<Vec<_>>()
.join(", ");
return Err(IdentityLocalExternalToolsError::DefinitionMismatch {
detail: format!(
"expected [{expected_names}], observed [{observed_names}] (name, description, and input schema must match exactly)"
),
});
}
Ok(Some(dispatcher))
}