use std::{
sync::{Arc, Mutex},
time::Duration,
};
use async_trait::async_trait;
use serde_json::Value;
use tokio::sync::mpsc;
use crate::{
BuiltinProvider, ContentBlock, Role, Runtime,
agent::AgentEvent,
provider::{ContentBlockDelta, ContentBlockStart, ProviderEvent},
runtime::{RunOptions, VolatileRuntimeStore},
tool::{
ParallelToolContext, ToolContext, ToolDefinition, ToolExecutionCategory, ToolExecutor,
ToolProgress, ToolResult, ToolSpec,
},
};
use super::support::{ScriptedProvider, controlled_stream, model_info, tool_use_stream};
struct BackgroundTool {
category: ToolExecutionCategory,
handles: mpsc::UnboundedSender<ToolProgress>,
}
impl ToolDefinition for BackgroundTool {
fn descriptor(&self) -> ToolSpec {
ToolSpec::builder("background")
.execution_category(self.category)
.build()
}
}
#[async_trait]
impl ToolExecutor for BackgroundTool {
fn execution_category(&self, _input: &Value) -> ToolExecutionCategory {
self.category
}
async fn execute_mut(&self, ctx: ToolContext<'_>, _input: Value) -> ToolResult {
ctx.emit_progress("starting".to_string());
self.handles.send(ctx.progress()).expect("handle receiver");
Ok("started".to_string())
}
async fn execute(&self, ctx: ParallelToolContext, _input: Value) -> ToolResult {
ctx.emit_progress("starting".to_string());
self.handles.send(ctx.progress()).expect("handle receiver");
Ok("started".to_string())
}
}
async fn background_progress(category: ToolExecutionCategory) {
let model = model_info("model", BuiltinProvider::Anthropic);
let (stream, tx) = controlled_stream();
let provider = ScriptedProvider::new(
BuiltinProvider::Anthropic,
vec![model.clone()],
vec![
tool_use_stream(&model.id, "call-1", "background", "{}"),
stream,
],
);
let provider_handle = provider.clone();
let (handles, mut handle_rx) = mpsc::unbounded_channel();
let runtime = Runtime::empty_builder()
.with_store(VolatileRuntimeStore::new())
.with_provider_instance(provider)
.with_tool(BackgroundTool { category, handles })
.build()
.expect("build runtime");
let mut agent = runtime.spawn("parent", model.clone()).expect("parent");
let other_agent = runtime.spawn("other", model.clone()).expect("other agent");
let mut other_events = other_agent.subscribe_events();
let mut events = agent.subscribe_events();
let mut snapshots = agent.watch_snapshot();
let tapped = Arc::new(Mutex::new(Vec::new()));
let tapped_events = Arc::clone(&tapped);
let _tap = agent.register_event_tap(move |event| {
tapped_events
.lock()
.expect("tapped events")
.push(event.clone());
});
let expected = |progress: &str| AgentEvent::ToolExecutionProgress {
id: "call-1".to_string(),
name: "background".to_string(),
progress: progress.to_string(),
};
let drive = async {
while provider_handle.recorded_requests().await.len() < 2 {
snapshots.changed().await.expect("agent snapshot updates");
}
let progress = handle_rx.recv().await.expect("owned progress handle");
let mut before = Vec::new();
while let Ok(event) = events.try_recv() {
before.push(event);
}
assert!(before.contains(&expected("starting")));
assert!(
before
.iter()
.any(|event| matches!(event, AgentEvent::ToolExecutionFinished { .. }))
);
let background = progress.clone();
tokio::spawn(async move {
background.emit("halfway");
background.emit("almost done");
})
.await
.expect("background task");
for message in ["halfway", "almost done"] {
assert_eq!(
events.try_recv().expect("immediate progress"),
expected(message)
);
}
assert!(events.try_recv().is_err(), "no duplicate events");
let observed = tapped.lock().expect("tapped events").clone();
assert_eq!(
&observed[observed.len() - 2..],
&[expected("halfway"), expected("almost done")]
);
assert!(
other_events.try_recv().is_err(),
"progress stays on its parent"
);
for event in [
ProviderEvent::MessageStarted {
id: "final".to_string(),
model: model.id.clone(),
role: Role::Assistant,
},
ProviderEvent::ContentBlockStarted {
index: 0,
kind: ContentBlockStart::Text,
},
ProviderEvent::ContentBlockDelta {
index: 0,
delta: ContentBlockDelta::Text("done".to_string()),
},
ProviderEvent::ContentBlockStopped { index: 0 },
ProviderEvent::MessageStopped,
] {
tx.send(Ok(event)).expect("provider stream");
}
drop(tx);
progress
};
let (result, progress) = tokio::time::timeout(Duration::from_secs(5), async {
tokio::join!(
agent.run(vec![ContentBlock::text("go")], RunOptions::default()),
drive
)
})
.await
.expect("run and progress complete without waiting for a round boundary");
assert_eq!(result.expect("run succeeds").text(), "done");
assert_eq!(
provider_handle.recorded_requests().await.len(),
2,
"progress does not keep the run going"
);
assert!(
!agent.has_deferred(),
"progress is never queued as deferred context"
);
assert!(
!agent.history().iter().any(|message| {
let text = message.text();
text.contains("halfway") || text.contains("almost done")
}),
"progress is not model context"
);
while events.try_recv().is_ok() {}
progress.emit("finished in background");
assert_eq!(
events.try_recv().expect("late progress"),
expected("finished in background")
);
}
#[tokio::test(start_paused = true)]
async fn mutable_tool_progress_outlives_its_call() {
background_progress(ToolExecutionCategory::ExclusiveLocalMutation).await;
}
#[tokio::test(start_paused = true)]
async fn parallel_tool_progress_outlives_its_call() {
background_progress(ToolExecutionCategory::ReadOnlyParallel).await;
}