api: fix sync wrapper to be safe from any calling context

Run the async API call on a dedicated thread with its own tokio
runtime so it works whether called from a sync context or from
within an existing tokio runtime (daemon).

Also drops the log closure capture issue — uses a simple eprintln
fallback since the closure can't cross thread boundaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent Overstreet 2026-03-18 23:07:49 -04:00
parent a29b6d4c5d
commit 643f9890df

View file

@ -99,17 +99,23 @@ pub async fn call_api_with_tools(
Err(format!("agent exceeded {} tool turns", max_turns))
}
/// Synchronous wrapper — creates a tokio runtime and blocks.
/// Used by the existing sync call path in knowledge.rs.
/// Synchronous wrapper — runs the async function on a dedicated thread
/// with its own tokio runtime. Safe to call from any context.
pub fn call_api_with_tools_sync(
agent: &str,
prompt: &str,
log: &dyn Fn(&str),
) -> Result<String, String> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("tokio runtime: {}", e))?;
rt.block_on(call_api_with_tools(agent, prompt, log))
// Run on a new thread to avoid conflicts with any existing runtime
let agent = agent.to_string();
let prompt = prompt.to_string();
std::thread::scope(|s| {
s.spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("tokio runtime: {}", e))?;
rt.block_on(call_api_with_tools(&agent, &prompt, &|msg| eprintln!("[api] {}", msg)))
}).join().unwrap()
})
}