Shared AI Tools Platform
All AI tool definitions live in src/mods/ai/tools/ — a shared platform module that any feature (assistant, dashboard briefings, scheduled insights) can import without depending on assistant internals.
Module Structure
Section titled “Module Structure”src/mods/ai/tools/├── mod.rs # Re-exports, tool_schema_for(), make_tool_execute! macro├── runtime.rs # Dedicated tokio runtime for tool dispatch├── contact_validation.rs # Batch contact ID validation helper├── calls/│ └── ai_get_call_details_tool.rs├── contacts/│ ├── ai_get_contact_details_tool.rs│ └── ai_get_bulk_contact_memory_tool.rs├── messages/│ └── ai_get_bulk_contact_messages_tool.rs├── tasks/│ └── ai_get_tasks_tool.rs└── sales/ ├── ai_get_active_offers_tool.rs └── ai_get_offer_details_tool.rsEvery sub-module is cfg-gated to not(target_family = "wasm") — tool handlers run server-side only. WASM builds see the type definitions but not the execution logic.
Tool Runtime
Section titled “Tool Runtime”Tool closures run synchronously (required by aisdk::ToolExecute), but handlers need async DB access. A dedicated multi-threaded tokio runtime in runtime.rs bridges the gap:
// runtime.rs — static 2-worker runtimestatic TOOL_RUNTIME: LazyLock<Result<Runtime, String>> = LazyLock::new(|| { tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .thread_name("ai-tool") .build() .map_err(|e| format!("Failed to create AI tool runtime: {e}")) });run_tool_async(future) spawns work on this runtime and blocks the caller via a sync_channel. Each tool gets a 30-second timeout — if the future exceeds it, tokio::time::timeout cancels it, releasing DB connections and preventing zombie tasks.
pub(crate) fn run_tool_async<F, T>(future: F) -> Result<T, String>where F: Future<Output = Result<T, String>> + Send + 'static, T: Send + 'static,{ let rt = TOOL_RUNTIME.as_ref().map_err(|e| { /* ... */ })?; let (tx, rx) = std::sync::mpsc::sync_channel(1); rt.spawn(async move { let result = match tokio::time::timeout(Duration::from_secs(30), future).await { Ok(r) => r, Err(_) => Err("Tool timed out after 30s".into()), }; let _ = tx.send(result); }); rx.recv().map_err(|e| format!("Tool channel disconnected: {e}"))?}The make_tool_execute! Macro
Section titled “The make_tool_execute! Macro”Eliminates boilerplate for building ToolExecute closures. Two forms:
One-liner — handler takes (captures..., input):
crate::make_tool_execute!("get_tasks", GetTasksInput, session @ input => { handler(session, input.status, input.category).await.map_err(|e| e.to_string())})Handler shorthand — handler takes the full input struct:
crate::make_tool_execute!("tool_name", InputType, handler => session, page_context)Both forms handle JSON deserialization, debug tracing (prefixed [ai-tool]), capture cloning, and dispatch through run_tool_async.
Building a Tool
Section titled “Building a Tool”Each tool is a build_*_tool function returning aisdk::core::tools::Tool:
pub fn build_get_tasks_tool(session: Session) -> Tool { #[derive(JsonSchema, Serialize, Deserialize)] struct GetTasksInput { status: Option<String>, category: Option<String>, contact_id: Option<String>, assigned_to: Option<String>, sort: Option<String>, limit: Option<u64>, page: Option<u64>, }
Tool { name: AiToolName::GetTasks.api_name().to_string(), description: "List tasks filtered by status, category, contact, or assignee.".to_string(), input_schema: tool_schema_for::<GetTasksInput>(), execute: crate::make_tool_execute!("get_tasks", GetTasksInput, session @ input => { handler(session, input.status, input.category, /* ... */).await .map_err(|e| e.to_string()) }), }}Key conventions:
- Input structs are private to the builder function (defined inline)
tool_schema_for::<T>()generates a JSON Schema stripped oftitle,format, and$schemato reduce LLM token usageAiToolNameenum is the single source of truth for tool name strings and UI labels- Handlers are
async fndefined below the builder, gated to non-WASM
Contact Validation Helper
Section titled “Contact Validation Helper”contact_validation::validate_contact_batch provides shared logic for tools that accept multiple contact IDs:
pub(crate) async fn validate_contact_batch( session: &Session, db: &DatabaseConnection, raw_ids: &[String], max_ids: usize,) -> Result<Vec<Uuid>, AppError>Parses UUIDs, deduplicates, enforces a max count, and verifies all contacts exist in the caller’s organization with proper access. Used by get_bulk_contact_memory and get_bulk_contact_messages.
PageContext
Section titled “PageContext”Tools that need page awareness (like get_contact_details) accept an optional PageContext:
pub struct PageContext { pub route: String, pub entity_type: Option<String>, pub entity_id: Option<String>, pub active_contact_scope: Option<AssistantActiveContactScope>, pub filter_query_string: Option<String>,}The assistant maps the current route to a PageContext via route_to_page_context, giving tools access to which entity the user is viewing.
Available Shared Tools
Section titled “Available Shared Tools”| Tool | Builder | Module |
|---|---|---|
get_call_details | build_get_call_details_tool | calls |
get_contact_details | build_get_contact_details_tool | contacts |
get_bulk_contact_memory | build_get_bulk_contact_memory_tool | contacts |
get_bulk_contact_messages | build_get_bulk_contact_messages_tool | messages |
get_tasks | build_get_tasks_tool | tasks |
get_active_offers | build_get_active_offers_tool | sales |
get_offer_details | build_get_offer_details_tool | sales |
All builders are re-exported from crate::mods::ai::tools — import from there, not from the sub-modules directly.