Converted all external tool calling to async
This commit is contained in:
@@ -54,7 +54,7 @@ pub(crate) async fn tool_get_agent_output(
|
||||
// writer failed and nothing was persisted to disk.
|
||||
if log_files.is_empty()
|
||||
&& let Some(agent_name) = agent_name_filter
|
||||
&& let Ok(live_events) = ctx.services.agents.drain_events(story_id, agent_name)
|
||||
&& let Ok(live_events) = ctx.services.agents.drain_events(story_id, agent_name).await
|
||||
&& !live_events.is_empty()
|
||||
{
|
||||
all_lines.push(format!("=== {agent_name} (live) ==="));
|
||||
@@ -99,7 +99,7 @@ pub(crate) async fn tool_get_agent_output(
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn tool_get_agent_config(ctx: &AppContext) -> Result<String, String> {
|
||||
pub(crate) async fn tool_get_agent_config(ctx: &AppContext) -> Result<String, String> {
|
||||
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
|
||||
let config = ProjectConfig::load(&project_root)?;
|
||||
|
||||
@@ -116,6 +116,7 @@ pub(crate) fn tool_get_agent_config(ctx: &AppContext) -> Result<String, String>
|
||||
.services
|
||||
.agents
|
||||
.available_agents_for_stage(&config, stage)
|
||||
.await
|
||||
{
|
||||
available_names.extend(names);
|
||||
}
|
||||
@@ -144,7 +145,7 @@ pub(crate) fn tool_get_agent_config(ctx: &AppContext) -> Result<String, String>
|
||||
/// Returns turns used, max turns, remaining turns, budget used, max budget,
|
||||
/// and remaining budget for the named agent. Fails if the agent is not
|
||||
/// currently running or pending.
|
||||
pub(crate) fn tool_get_agent_remaining_turns_and_budget(
|
||||
pub(crate) async fn tool_get_agent_remaining_turns_and_budget(
|
||||
args: &Value,
|
||||
ctx: &AppContext,
|
||||
) -> Result<String, String> {
|
||||
@@ -158,7 +159,7 @@ pub(crate) fn tool_get_agent_remaining_turns_and_budget(
|
||||
.ok_or("Missing required argument: agent_name")?;
|
||||
|
||||
// Verify the agent exists and is running/pending.
|
||||
let agents = ctx.services.agents.list_agents()?;
|
||||
let agents = ctx.services.agents.list_agents().await?;
|
||||
let agent_info = agents
|
||||
.iter()
|
||||
.find(|a| a.story_id == story_id && a.agent_name == agent_name)
|
||||
@@ -270,12 +271,12 @@ mod tests {
|
||||
use crate::http::test_helpers::test_ctx;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn tool_get_agent_config_no_project_toml_returns_default_agent() {
|
||||
#[tokio::test]
|
||||
async fn tool_get_agent_config_no_project_toml_returns_default_agent() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
// No project.toml → default config with one fallback agent
|
||||
let result = tool_get_agent_config(&ctx).unwrap();
|
||||
let result = tool_get_agent_config(&ctx).await.unwrap();
|
||||
let parsed: Vec<Value> = serde_json::from_str(&result).unwrap();
|
||||
// Default config contains one agent entry with default values
|
||||
assert_eq!(
|
||||
@@ -457,34 +458,36 @@ mod tests {
|
||||
|
||||
// ── get_agent_remaining_turns_and_budget tests ──────────────────────────
|
||||
|
||||
#[test]
|
||||
fn tool_get_agent_remaining_turns_and_budget_missing_story_id() {
|
||||
#[tokio::test]
|
||||
async fn tool_get_agent_remaining_turns_and_budget_missing_story_id() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result =
|
||||
tool_get_agent_remaining_turns_and_budget(&json!({"agent_name": "coder-1"}), &ctx);
|
||||
tool_get_agent_remaining_turns_and_budget(&json!({"agent_name": "coder-1"}), &ctx)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("story_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_get_agent_remaining_turns_and_budget_missing_agent_name() {
|
||||
#[tokio::test]
|
||||
async fn tool_get_agent_remaining_turns_and_budget_missing_agent_name() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result =
|
||||
tool_get_agent_remaining_turns_and_budget(&json!({"story_id": "1_test"}), &ctx);
|
||||
tool_get_agent_remaining_turns_and_budget(&json!({"story_id": "1_test"}), &ctx).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("agent_name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_get_agent_remaining_turns_and_budget_no_agent_returns_error() {
|
||||
#[tokio::test]
|
||||
async fn tool_get_agent_remaining_turns_and_budget_no_agent_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_get_agent_remaining_turns_and_budget(
|
||||
&json!({"story_id": "99_nope", "agent_name": "coder-1"}),
|
||||
&ctx,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
@@ -493,8 +496,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_get_agent_remaining_turns_and_budget_completed_agent_returns_error() {
|
||||
#[tokio::test]
|
||||
async fn tool_get_agent_remaining_turns_and_budget_completed_agent_returns_error() {
|
||||
use crate::agents::AgentStatus;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
@@ -505,7 +508,8 @@ mod tests {
|
||||
let result = tool_get_agent_remaining_turns_and_budget(
|
||||
&json!({"story_id": "42_story", "agent_name": "coder-1"}),
|
||||
&ctx,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
@@ -514,8 +518,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_get_agent_remaining_turns_and_budget_running_agent_returns_data() {
|
||||
#[tokio::test]
|
||||
async fn tool_get_agent_remaining_turns_and_budget_running_agent_returns_data() {
|
||||
use crate::agents::AgentStatus;
|
||||
use crate::store::StoreOps;
|
||||
|
||||
@@ -531,6 +535,7 @@ mod tests {
|
||||
&json!({"story_id": "42_story", "agent_name": "coder-1"}),
|
||||
&ctx,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let parsed: Value = serde_json::from_str(&result).unwrap();
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ pub(crate) async fn tool_stop_agent(args: &Value, ctx: &AppContext) -> Result<St
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn tool_list_agents(ctx: &AppContext) -> Result<String, String> {
|
||||
pub(crate) async fn tool_list_agents(ctx: &AppContext) -> Result<String, String> {
|
||||
let project_root = ctx.services.agents.get_project_root(&ctx.state).ok();
|
||||
let agents = ctx.services.agents.list_agents()?;
|
||||
let agents = ctx.services.agents.list_agents().await?;
|
||||
let mut entries: Vec<serde_json::Value> = agents
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
@@ -156,11 +156,11 @@ mod tests {
|
||||
use crate::http::test_helpers::test_ctx;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn tool_list_agents_empty() {
|
||||
#[tokio::test]
|
||||
async fn tool_list_agents_empty() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_list_agents(&ctx).unwrap();
|
||||
let result = tool_list_agents(&ctx).await.unwrap();
|
||||
let parsed: Vec<serde_json::Value> = serde_json::from_str(&result).unwrap();
|
||||
assert!(parsed.is_empty());
|
||||
}
|
||||
|
||||
@@ -362,8 +362,8 @@ mod tests {
|
||||
// then exec() will be called — which would replace our test process.
|
||||
// So we only test that the function *runs* without panicking up to
|
||||
// the agent-kill step. We do this by checking the pool is empty.
|
||||
assert_eq!(ctx.services.agents.list_agents().unwrap().len(), 0);
|
||||
ctx.services.agents.kill_all_children(); // should not panic on empty pool
|
||||
assert_eq!(ctx.services.agents.list_agents().await.unwrap().len(), 0);
|
||||
ctx.services.agents.kill_all_children().await; // should not panic on empty pool
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -30,13 +30,13 @@ pub async fn dispatch_tool_call(
|
||||
// Agent tools (async)
|
||||
"start_agent" => agent_tools::tool_start_agent(&args, ctx).await,
|
||||
"stop_agent" => agent_tools::tool_stop_agent(&args, ctx).await,
|
||||
"list_agents" => agent_tools::tool_list_agents(ctx),
|
||||
"get_agent_config" => agent_tools::tool_get_agent_config(ctx),
|
||||
"reload_agent_config" => agent_tools::tool_get_agent_config(ctx),
|
||||
"list_agents" => agent_tools::tool_list_agents(ctx).await,
|
||||
"get_agent_config" => agent_tools::tool_get_agent_config(ctx).await,
|
||||
"reload_agent_config" => agent_tools::tool_get_agent_config(ctx).await,
|
||||
"get_agent_output" => agent_tools::tool_get_agent_output(&args, ctx).await,
|
||||
"wait_for_agent" => agent_tools::tool_wait_for_agent(&args, ctx).await,
|
||||
"get_agent_remaining_turns_and_budget" => {
|
||||
agent_tools::tool_get_agent_remaining_turns_and_budget(&args, ctx)
|
||||
agent_tools::tool_get_agent_remaining_turns_and_budget(&args, ctx).await
|
||||
}
|
||||
// Worktree tools
|
||||
"create_worktree" => agent_tools::tool_create_worktree(&args, ctx).await,
|
||||
@@ -46,7 +46,7 @@ pub async fn dispatch_tool_call(
|
||||
// Editor tools
|
||||
"get_editor_command" => agent_tools::tool_get_editor_command(&args, ctx),
|
||||
// Lifecycle tools
|
||||
"accept_story" => story_tools::tool_accept_story(&args, ctx),
|
||||
"accept_story" => story_tools::tool_accept_story(&args, ctx).await,
|
||||
// Story mutation tools (auto-commit to master)
|
||||
"check_criterion" => story_tools::tool_check_criterion(&args, ctx),
|
||||
"edit_criterion" => story_tools::tool_edit_criterion(&args, ctx),
|
||||
@@ -58,7 +58,7 @@ pub async fn dispatch_tool_call(
|
||||
// Bug lifecycle tools
|
||||
"create_bug" => story_tools::tool_create_bug(&args, ctx),
|
||||
"list_bugs" => story_tools::tool_list_bugs(ctx),
|
||||
"close_bug" => story_tools::tool_close_bug(&args, ctx),
|
||||
"close_bug" => story_tools::tool_close_bug(&args, ctx).await,
|
||||
// Refactor lifecycle tools
|
||||
"create_refactor" => story_tools::tool_create_refactor(&args, ctx),
|
||||
"list_refactors" => story_tools::tool_list_refactors(ctx),
|
||||
@@ -70,7 +70,7 @@ pub async fn dispatch_tool_call(
|
||||
"merge_agent_work" => merge_tools::tool_merge_agent_work(&args, ctx).await,
|
||||
"get_merge_status" => merge_tools::tool_get_merge_status(&args, ctx),
|
||||
"move_story_to_merge" => merge_tools::tool_move_story_to_merge(&args, ctx).await,
|
||||
"report_merge_failure" => merge_tools::tool_report_merge_failure(&args, ctx),
|
||||
"report_merge_failure" => merge_tools::tool_report_merge_failure(&args, ctx).await,
|
||||
// QA tools
|
||||
"request_qa" => qa_tools::tool_request_qa(&args, ctx).await,
|
||||
"approve_qa" => qa_tools::tool_approve_qa(&args, ctx).await,
|
||||
|
||||
@@ -171,7 +171,10 @@ pub(super) async fn tool_move_story_to_merge(
|
||||
.map_err(|e| format!("Serialization error: {e}"))
|
||||
}
|
||||
|
||||
pub(super) fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
pub(super) async fn tool_report_merge_failure(
|
||||
args: &Value,
|
||||
ctx: &AppContext,
|
||||
) -> Result<String, String> {
|
||||
let story_id = args
|
||||
.get("story_id")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -182,7 +185,10 @@ pub(super) fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Resul
|
||||
.ok_or("Missing required argument: reason")?;
|
||||
|
||||
slog!("[mergemaster] Merge failure reported for '{story_id}': {reason}");
|
||||
ctx.services.agents.set_merge_failure_reported(story_id);
|
||||
ctx.services
|
||||
.agents
|
||||
.set_merge_failure_reported(story_id)
|
||||
.await;
|
||||
|
||||
// The mergemaster provides a freeform reason string; use Other so the
|
||||
// auto-assigner does not re-spawn another mergemaster after this one fails.
|
||||
@@ -412,26 +418,26 @@ mod tests {
|
||||
assert!(req_names.contains(&"reason"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_report_merge_failure_missing_story_id() {
|
||||
#[tokio::test]
|
||||
async fn tool_report_merge_failure_missing_story_id() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_report_merge_failure(&json!({"reason": "conflicts"}), &ctx);
|
||||
let result = tool_report_merge_failure(&json!({"reason": "conflicts"}), &ctx).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("story_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_report_merge_failure_missing_reason() {
|
||||
#[tokio::test]
|
||||
async fn tool_report_merge_failure_missing_reason() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_report_merge_failure(&json!({"story_id": "42_story_foo"}), &ctx);
|
||||
let result = tool_report_merge_failure(&json!({"story_id": "42_story_foo"}), &ctx).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("reason"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_report_merge_failure_returns_confirmation() {
|
||||
#[tokio::test]
|
||||
async fn tool_report_merge_failure_returns_confirmation() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_report_merge_failure(
|
||||
@@ -440,7 +446,8 @@ mod tests {
|
||||
"reason": "Unresolvable merge conflicts in src/main.rs"
|
||||
}),
|
||||
&ctx,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
let msg = result.unwrap();
|
||||
assert!(msg.contains("42_story_foo"));
|
||||
|
||||
@@ -81,7 +81,7 @@ pub(super) async fn tool_approve_qa(args: &Value, ctx: &AppContext) -> Result<St
|
||||
move_story_to_done(story_id)?;
|
||||
|
||||
let pool = std::sync::Arc::clone(&ctx.services.agents);
|
||||
pool.remove_agents_for_story(story_id);
|
||||
pool.remove_agents_for_story(story_id).await;
|
||||
|
||||
let wt_path = crate::worktree::worktree_path(&project_root, story_id);
|
||||
if wt_path.exists() {
|
||||
|
||||
@@ -66,14 +66,14 @@ pub(crate) fn tool_list_bugs(ctx: &AppContext) -> Result<String, String> {
|
||||
.map_err(|e| format!("Serialization error: {e}"))
|
||||
}
|
||||
|
||||
pub(crate) fn tool_close_bug(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
pub(crate) async fn tool_close_bug(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
let bug_id = args
|
||||
.get("bug_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("Missing required argument: bug_id")?;
|
||||
|
||||
close_bug_to_archive(bug_id)?;
|
||||
ctx.services.agents.remove_agents_for_story(bug_id);
|
||||
ctx.services.agents.remove_agents_for_story(bug_id).await;
|
||||
|
||||
Ok(format!(
|
||||
"Bug '{bug_id}' closed, moved to bugs/archive/, and committed to master."
|
||||
@@ -422,17 +422,17 @@ mod tests {
|
||||
assert!(result.unwrap().contains("Created bug:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_close_bug_missing_bug_id() {
|
||||
#[tokio::test]
|
||||
async fn tool_close_bug_missing_bug_id() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_close_bug(&json!({}), &ctx);
|
||||
let result = tool_close_bug(&json!({}), &ctx).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("bug_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_close_bug_moves_to_archive() {
|
||||
#[tokio::test]
|
||||
async fn tool_close_bug_moves_to_archive() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo_in(tmp.path());
|
||||
let backlog_dir = tmp.path().join(".huskies/work/1_backlog");
|
||||
@@ -460,7 +460,9 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_close_bug(&json!({"bug_id": "9901_bug_crash"}), &ctx).unwrap();
|
||||
let result = tool_close_bug(&json!({"bug_id": "9901_bug_crash"}), &ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(result.contains("9901_bug_crash"));
|
||||
assert!(
|
||||
crate::db::read_content(crate::db::ContentKey::Story("9901_bug_crash")).is_some(),
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::http::context::AppContext;
|
||||
use crate::pipeline_state::{Stage, read_typed};
|
||||
use serde_json::Value;
|
||||
|
||||
pub(crate) fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
pub(crate) async fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String, String> {
|
||||
let story_id = args
|
||||
.get("story_id")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -33,7 +33,7 @@ pub(crate) fn tool_accept_story(args: &Value, ctx: &AppContext) -> Result<String
|
||||
}
|
||||
|
||||
move_story_to_done(story_id)?;
|
||||
ctx.services.agents.remove_agents_for_story(story_id);
|
||||
ctx.services.agents.remove_agents_for_story(story_id).await;
|
||||
|
||||
Ok(format!(
|
||||
"Story '{story_id}' accepted, moved to done/, and committed to master."
|
||||
@@ -146,27 +146,27 @@ mod tests {
|
||||
assert!(!story_file.exists(), "story file should be deleted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_accept_story_missing_story_id() {
|
||||
#[tokio::test]
|
||||
async fn tool_accept_story_missing_story_id() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_accept_story(&json!({}), &ctx);
|
||||
let result = tool_accept_story(&json!({}), &ctx).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("story_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_accept_story_nonexistent_story_returns_error() {
|
||||
#[tokio::test]
|
||||
async fn tool_accept_story_nonexistent_story_returns_error() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo_in(tmp.path());
|
||||
let ctx = test_ctx(tmp.path());
|
||||
// No story file in current/ — should fail
|
||||
let result = tool_accept_story(&json!({"story_id": "99_nonexistent"}), &ctx);
|
||||
let result = tool_accept_story(&json!({"story_id": "99_nonexistent"}), &ctx).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_accept_story_refuses_when_feature_branch_has_unmerged_code() {
|
||||
#[tokio::test]
|
||||
async fn tool_accept_story_refuses_when_feature_branch_has_unmerged_code() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo_in(tmp.path());
|
||||
|
||||
@@ -203,7 +203,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_accept_story(&json!({"story_id": "50_story_test"}), &ctx);
|
||||
let result = tool_accept_story(&json!({"story_id": "50_story_test"}), &ctx).await;
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"should refuse when feature branch has unmerged code"
|
||||
@@ -215,8 +215,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_accept_story_succeeds_when_no_feature_branch() {
|
||||
#[tokio::test]
|
||||
async fn tool_accept_story_succeeds_when_no_feature_branch() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
setup_git_repo_in(tmp.path());
|
||||
|
||||
@@ -234,7 +234,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let ctx = test_ctx(tmp.path());
|
||||
let result = tool_accept_story(&json!({"story_id": "51_story_no_branch"}), &ctx);
|
||||
let result = tool_accept_story(&json!({"story_id": "51_story_no_branch"}), &ctx).await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"should succeed when no feature branch: {result:?}"
|
||||
|
||||
Reference in New Issue
Block a user