use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use holon::provider::{
AgentProvider, ConversationMessage, ModelBlock, ProviderTurnRequest, ProviderTurnResponse,
};
use serde_json::json;
use tokio::sync::Mutex;
use tokio::time::{sleep, Duration};
use super::runtime_helpers::{delegated_prompt_text, preserves_prior_tool_context};
#[derive(Debug, Clone)]
pub struct CapturedTurnRequest {
pub prompt_text: String,
pub compression_epoch: u64,
pub working_memory_revision: u64,
}
pub struct ToolUsingProvider {
calls: Mutex<usize>,
}
impl ToolUsingProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for ToolUsingProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
assert!(!request.tools.is_empty());
return Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::ToolUse {
id: "tool-1".into(),
name: "AgentGet".into(),
input: json!({}),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
});
}
assert!(preserves_prior_tool_context(&request));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "tool loop complete".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct NotifyThenAgentGetProvider {
calls: Mutex<usize>,
}
impl NotifyThenAgentGetProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for NotifyThenAgentGetProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
assert!(request
.tools
.iter()
.any(|tool| tool.name == "NotifyOperator"));
assert!(request.tools.iter().any(|tool| tool.name == "AgentGet"));
return Ok(ProviderTurnResponse {
blocks: vec![
ModelBlock::ToolUse {
id: "notify-1".into(),
name: "NotifyOperator".into(),
input: json!({
"message": "Operator FYI\nContinuing with the default path."
}),
},
ModelBlock::ToolUse {
id: "agent-get-1".into(),
name: "AgentGet".into(),
input: json!({}),
},
],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
});
}
assert!(preserves_prior_tool_context(&request));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "continued after notifying operator".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct FileEditingProvider {
calls: Mutex<usize>,
}
impl FileEditingProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for FileEditingProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
assert!(request.tools.iter().any(|tool| tool.name == "ApplyPatch"));
assert!(request.tools.iter().any(|tool| tool.name == "ExecCommand"));
return Ok(ProviderTurnResponse {
blocks: vec![
ModelBlock::ToolUse {
id: "patch-1".into(),
name: "ApplyPatch".into(),
input: json!({
"patch": "--- /dev/null\n+++ b/notes/result.txt\n@@ -0,0 +1,1 @@\n+hello from holon\n"
}),
},
ModelBlock::ToolUse {
id: "read-1".into(),
name: "ExecCommand".into(),
input: json!({
"cmd": "cat notes/result.txt",
"workdir": "."
}),
},
],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
});
}
assert!(preserves_prior_tool_context(&request));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "file tools complete".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct TerminalResultBriefProvider {
calls: Mutex<usize>,
}
impl TerminalResultBriefProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for TerminalResultBriefProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
match *calls {
1 => {
assert!(request.tools.iter().any(|tool| tool.name == "ApplyPatch"));
assert!(request.tools.iter().any(|tool| tool.name == "ExecCommand"));
Ok(ProviderTurnResponse {
blocks: vec![
ModelBlock::Text {
text: "Let me create a summary document of what was changed.".into(),
},
ModelBlock::ToolUse {
id: "patch-1".into(),
name: "ApplyPatch".into(),
input: json!({
"patch": "--- /dev/null\n+++ b/notes/result.txt\n@@ -0,0 +1,1 @@\n+hello from holon\n"
}),
},
ModelBlock::ToolUse {
id: "verify-1".into(),
name: "ExecCommand".into(),
input: json!({
"cmd": "printf tests_passed",
"login": false
}),
},
],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
2 => Ok(ProviderTurnResponse {
blocks: vec![
ModelBlock::Text {
text: "Verification is complete. I'll package the final answer now.".into(),
},
ModelBlock::ToolUse {
id: "sleep-1".into(),
name: "Sleep".into(),
input: json!({
"reason": "sleep requested"
}),
},
],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
}),
_ => {
assert!(
request.tools.is_empty(),
"terminal delivery round should have no tools"
);
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "What changed: notes/result.txt\nWhy: to address the requested task: write and verify a file.\nVerification: successful verification command completed with exit status 0.".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
}
}
pub struct SleepOnlyCompletionAfterTextProvider {
calls: Mutex<usize>,
}
impl SleepOnlyCompletionAfterTextProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for SleepOnlyCompletionAfterTextProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
match *calls {
1 => {
assert!(request.tools.iter().any(|tool| tool.name == "ApplyPatch"));
Ok(ProviderTurnResponse {
blocks: vec![
ModelBlock::Text {
text: "Updated notes/result.txt and verified the requested change."
.into(),
},
ModelBlock::ToolUse {
id: "patch-1".into(),
name: "ApplyPatch".into(),
input: json!({
"patch": "--- /dev/null\n+++ b/notes/result.txt\n@@ -0,0 +1,1 @@\n+hello from holon\n"
}),
},
],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
2 => Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::ToolUse {
id: "sleep-1".into(),
name: "Sleep".into(),
input: json!({
"reason": "delivery complete"
}),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 20,
cache_usage: None,
request_diagnostics: None,
}),
_ => anyhow::bail!("unexpected provider call"),
}
}
}
pub struct ShellProvider {
calls: Mutex<usize>,
}
impl ShellProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for ShellProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
assert!(request.tools.iter().any(|tool| tool.name == "ExecCommand"));
return Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::ToolUse {
id: "exec-1".into(),
name: "ExecCommand".into(),
input: json!({
"cmd": "printf shell_ok"
}),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
});
}
let tool_result_text = request
.conversation
.iter()
.find_map(|message| match message {
ConversationMessage::UserToolResults(results) => Some(
results
.iter()
.map(|result| result.content.clone())
.collect::<Vec<_>>()
.join("\n"),
),
_ => None,
})
.unwrap_or_default();
assert!(tool_result_text.contains("shell_ok"));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "shell tools complete".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct TruncatedShellReinjectionProvider {
calls: Mutex<usize>,
payload: String,
}
impl TruncatedShellReinjectionProvider {
pub fn new(payload: String) -> Self {
Self {
calls: Mutex::new(0),
payload,
}
}
}
#[async_trait]
impl AgentProvider for TruncatedShellReinjectionProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
assert!(request.tools.iter().any(|tool| tool.name == "ExecCommand"));
return Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::ToolUse {
id: "exec-truncated-1".into(),
name: "ExecCommand".into(),
input: json!({
"cmd": format!("printf '%s' '{}'", self.payload),
"login": false,
"max_output_tokens": 32
}),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
});
}
let tool_result_text = request
.conversation
.iter()
.find_map(|message| match message {
ConversationMessage::UserToolResults(results) => Some(
results
.iter()
.map(|result| result.content.clone())
.collect::<Vec<_>>()
.join("\n"),
),
_ => None,
})
.unwrap_or_default();
assert!(
tool_result_text.contains("[output truncated: showing leading and trailing context]")
);
assert!(!tool_result_text.contains(&self.payload));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "truncated shell reinjection observed".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct LongShellProvider {
calls: Mutex<usize>,
}
impl LongShellProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for LongShellProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
return Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::ToolUse {
id: "exec-long-1".into(),
name: "ExecCommand".into(),
input: json!({
"cmd": "printf start && sleep 1 && printf done",
"yield_time_ms": 50
}),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
});
}
let tool_result_text = request
.conversation
.iter()
.find_map(|message| match message {
ConversationMessage::UserToolResults(results) => Some(
results
.iter()
.map(|result| result.content.clone())
.collect::<Vec<_>>()
.join("\n"),
),
_ => None,
})
.unwrap_or_default();
assert!(tool_result_text.contains("Command promoted to background task"));
assert!(tool_result_text.contains("Task:"));
assert!(tool_result_text.contains("Initial output:"));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "auto promotion observed".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct DelegatedBoundaryProvider;
#[async_trait]
impl AgentProvider for DelegatedBoundaryProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let prompt = delegated_prompt_text(&request);
let (delay_ms, text) = if prompt.contains("slow-child") {
(200, "slow child result")
} else if prompt.contains("alpha-child") {
(160, "alpha child result")
} else if prompt.contains("beta-child") {
(20, "beta child result")
} else if prompt.contains("fail-child") {
anyhow::bail!("child execution exploded");
} else {
anyhow::bail!("unexpected delegated prompt: {prompt}");
};
if delay_ms > 0 {
sleep(Duration::from_millis(delay_ms)).await;
}
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text { text: text.into() }],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct WakeHintProvider {
calls: Mutex<usize>,
}
impl WakeHintProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for WakeHintProvider {
async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
sleep(Duration::from_millis(250)).await;
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "first turn complete".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
} else {
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "wake follow-up complete".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
}
pub struct RecordingPromptProvider {
calls: Mutex<usize>,
requests: Mutex<Vec<CapturedTurnRequest>>,
replies: Vec<String>,
first_delay: Option<Duration>,
}
impl RecordingPromptProvider {
pub fn new(replies: &[&str]) -> Self {
Self {
calls: Mutex::new(0),
requests: Mutex::new(Vec::new()),
replies: replies.iter().map(|reply| (*reply).to_string()).collect(),
first_delay: None,
}
}
pub fn with_first_delay(mut self, delay: Duration) -> Self {
self.first_delay = Some(delay);
self
}
pub async fn captured_requests(&self) -> Vec<CapturedTurnRequest> {
self.requests.lock().await.clone()
}
}
#[async_trait]
impl AgentProvider for RecordingPromptProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
let call_index = *calls - 1;
drop(calls);
self.requests.lock().await.push(CapturedTurnRequest {
prompt_text: delegated_prompt_text(&request),
compression_epoch: request
.prompt_frame
.cache
.as_ref()
.map(|cache| cache.compression_epoch)
.unwrap_or_default(),
working_memory_revision: request
.prompt_frame
.cache
.as_ref()
.map(|cache| cache.working_memory_revision)
.unwrap_or_default(),
});
if call_index == 0 {
if let Some(delay) = self.first_delay {
sleep(delay).await;
}
}
let reply = self
.replies
.get(call_index)
.cloned()
.or_else(|| self.replies.last().cloned())
.unwrap_or_else(|| "recorded turn".into());
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text { text: reply }],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct ToolErrorProvider {
calls: Mutex<usize>,
}
impl ToolErrorProvider {
pub fn new() -> Self {
Self {
calls: Mutex::new(0),
}
}
}
#[async_trait]
impl AgentProvider for ToolErrorProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
return Ok(ProviderTurnResponse {
blocks: vec![
ModelBlock::ToolUse {
id: "bad-exec".into(),
name: "ExecCommand".into(),
input: json!({
"yield_time_ms": 10
}),
},
ModelBlock::ToolUse {
id: "bad-tool".into(),
name: "DefinitelyNotATool".into(),
input: json!({}),
},
ModelBlock::ToolUse {
id: "retired-read".into(),
name: "Read".into(),
input: json!({
"file_path": "notes/result.txt"
}),
},
],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
});
}
let tool_results = request
.conversation
.iter()
.rev()
.find_map(|message| match message {
ConversationMessage::UserToolResults(results) => Some(results.clone()),
_ => None,
})
.unwrap_or_default();
assert_eq!(tool_results.len(), 3);
assert!(tool_results.iter().all(|result| result.is_error));
assert!(tool_results.iter().any(|result| {
result.error.as_ref().is_some_and(|error| {
error.kind == "invalid_tool_input"
&& error
.details
.as_ref()
.and_then(|details| details.get("parse_error"))
.and_then(|value| value.as_str())
.is_some_and(|parse_error| parse_error.contains("missing field `cmd`"))
})
}));
assert!(tool_results.iter().any(|result| result
.content
.contains("tool DefinitelyNotATool was not exposed in this round")));
assert!(tool_results.iter().any(|result| result
.content
.contains("tool Read was not exposed in this round")));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "tool failures handled".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct RuntimeFailureProvider;
#[async_trait]
impl AgentProvider for RuntimeFailureProvider {
async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
anyhow::bail!("provider transport broke")
}
}
pub struct VerboseRuntimeFailureProvider;
#[async_trait]
impl AgentProvider for VerboseRuntimeFailureProvider {
async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
anyhow::bail!(
"provider transport broke with a very long first line {}\
\nraw backend body: {{\"detail\":\"unexpected backend body that should stay out of briefs\"}}",
"x".repeat(260)
)
}
}
pub struct UseWorkspaceProvider {
calls: Mutex<usize>,
workspace_path: PathBuf,
branch_name: String,
}
impl UseWorkspaceProvider {
pub fn new(workspace_path: PathBuf, branch_name: impl Into<String>) -> Self {
Self {
calls: Mutex::new(0),
workspace_path,
branch_name: branch_name.into(),
}
}
}
#[async_trait]
impl AgentProvider for UseWorkspaceProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
if *calls == 1 {
assert!(request.tools.iter().any(|tool| tool.name == "UseWorkspace"));
return Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::ToolUse {
id: "workspace-1".into(),
name: "UseWorkspace".into(),
input: json!({
"path": self.workspace_path,
"mode": "isolated",
"isolation_label": self.branch_name,
}),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
});
}
let tool_result_text = request
.conversation
.iter()
.find_map(|message| match message {
ConversationMessage::UserToolResults(results) => Some(
results
.iter()
.map(|result| result.content.clone())
.collect::<Vec<_>>()
.join("\n"),
),
_ => None,
})
.unwrap_or_default();
assert!(tool_result_text.contains("\"mode\": \"isolated\""));
assert!(tool_result_text.contains("\"projection_kind\": \"git_worktree_root\""));
assert!(!tool_result_text.contains("\"access_mode\""));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "entered worktree successfully".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct WorktreeLifecycleProvider {
calls: Mutex<usize>,
workspace_path: PathBuf,
branch_name: String,
expected_exit_result: String,
}
impl WorktreeLifecycleProvider {
pub fn new(
workspace_path: PathBuf,
branch_name: impl Into<String>,
expected_exit_result: impl Into<String>,
) -> Self {
Self {
calls: Mutex::new(0),
workspace_path,
branch_name: branch_name.into(),
expected_exit_result: expected_exit_result.into(),
}
}
}
#[async_trait]
impl AgentProvider for WorktreeLifecycleProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let mut calls = self.calls.lock().await;
*calls += 1;
match *calls {
1 => {
assert!(request.tools.iter().any(|tool| tool.name == "UseWorkspace"));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::ToolUse {
id: "use-1".into(),
name: "UseWorkspace".into(),
input: json!({
"path": self.workspace_path,
"mode": "isolated",
"isolation_label": self.branch_name,
}),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
2 => Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "entered worktree".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
}),
3 => {
assert!(request.tools.iter().any(|tool| tool.name == "UseWorkspace"));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::ToolUse {
id: "use-home-1".into(),
name: "UseWorkspace".into(),
input: json!({
"workspace_id": "agent_home",
}),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
4 => {
let tool_results = request
.conversation
.iter()
.rev()
.find_map(|message| match message {
ConversationMessage::UserToolResults(results) => Some(results.clone()),
_ => None,
})
.unwrap_or_default();
assert!(tool_results
.iter()
.any(|result| result.content.contains(&self.expected_exit_result)));
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "exited worktree".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
_ => Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "done".into(),
}],
stop_reason: None,
input_tokens: 50,
output_tokens: 20,
cache_usage: None,
request_diagnostics: None,
}),
}
}
}
pub struct WorktreeCapturingProvider {
prompts: Mutex<Vec<String>>,
reply: String,
}
impl WorktreeCapturingProvider {
pub fn new(reply: impl Into<String>) -> Self {
Self {
prompts: Mutex::new(Vec::new()),
reply: reply.into(),
}
}
pub async fn prompts(&self) -> Vec<String> {
self.prompts.lock().await.clone()
}
}
#[async_trait]
impl AgentProvider for WorktreeCapturingProvider {
async fn complete_turn(&self, request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
self.prompts
.lock()
.await
.push(request.prompt_frame.system_prompt.clone());
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: self.reply.clone(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
pub struct DelayedTextProvider;
#[async_trait]
impl AgentProvider for DelayedTextProvider {
async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
sleep(Duration::from_millis(250)).await;
Ok(ProviderTurnResponse {
blocks: vec![ModelBlock::Text {
text: "Made changes in worktree".into(),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}