2
0
This commit is contained in:
2026-03-24 19:27:52 +09:00
parent 54eb61a4f3
commit 2a1945eb5e
4 changed files with 54 additions and 17 deletions

View File

@@ -63,6 +63,8 @@ pub struct AgentConfig {
pub host: Option<String>, pub host: Option<String>,
#[serde(default)] #[serde(default)]
pub protocol: Option<String>, pub protocol: Option<String>,
#[serde(default)]
pub depends_on: Vec<String>,
} }
fn default_cwd() -> String { fn default_cwd() -> String {
@@ -128,7 +130,7 @@ fn load_file_full(path: &Path) -> LoadedConfig {
} }
if let (Some(name), Some(task), Some(cwd)) = (multi.name, multi.task, multi.cwd) { if let (Some(name), Some(task), Some(cwd)) = (multi.name, multi.task, multi.cwd) {
return LoadedConfig { return LoadedConfig {
agents: vec![AgentConfig { name, task, cwd, host: None, protocol: None }], agents: vec![AgentConfig { name, task, cwd, host: None, protocol: None, depends_on: vec![] }],
interval: multi.interval, interval: multi.interval,
}; };
} }

View File

@@ -27,7 +27,7 @@ pub fn run(config_or_task: &str, cwd_override: Option<&str>, name_override: Opti
.map(|p| p.display().to_string()) .map(|p| p.display().to_string())
.unwrap_or_else(|_| ".".to_string())); .unwrap_or_else(|_| ".".to_string()));
let name = name_override.unwrap_or("task").to_string(); let name = name_override.unwrap_or("task").to_string();
(vec![config::AgentConfig { name, task: config_or_task.to_string(), cwd, host: None, protocol: None }], None) (vec![config::AgentConfig { name, task: config_or_task.to_string(), cwd, host: None, protocol: None, depends_on: vec![] }], None)
}; };
if configs.is_empty() { if configs.is_empty() {
@@ -355,28 +355,63 @@ fn strip_dir_listing(text: &str) -> &str {
} }
fn spawn_and_wait(configs: &[config::AgentConfig], running: &Arc<AtomicBool>) -> Vec<Agent> { fn spawn_and_wait(configs: &[config::AgentConfig], running: &Arc<AtomicBool>) -> Vec<Agent> {
let mut agents = Vec::new(); let mut agents: Vec<Agent> = Vec::new();
let mut pending: Vec<&config::AgentConfig> = Vec::new();
let mut next_id = 1; let mut next_id = 1;
// Spawn agents without dependencies immediately, defer the rest
for cfg in configs { for cfg in configs {
let cwd = expand_tilde(&cfg.cwd); if cfg.depends_on.is_empty() {
match Agent::spawn_with_config(next_id, &cfg.name, &cfg.task, &cwd, cfg.host.as_deref(), cfg.protocol.as_deref()) { let cwd = expand_tilde(&cfg.cwd);
Ok(agent) => { match Agent::spawn_with_config(next_id, &cfg.name, &cfg.task, &cwd, cfg.host.as_deref(), cfg.protocol.as_deref()) {
eprintln!(" started: {} ({})", cfg.name, cwd); Ok(agent) => {
agents.push(agent); eprintln!(" started: {} ({})", cfg.name, cwd);
next_id += 1; agents.push(agent);
next_id += 1;
}
Err(e) => eprintln!(" failed: {}: {e}", cfg.name),
} }
Err(e) => eprintln!(" failed: {}: {e}", cfg.name), } else {
eprintln!(" waiting: {} (depends on: {})", cfg.name, cfg.depends_on.join(", "));
pending.push(cfg);
} }
} }
let mut last_status: Vec<String> = Vec::new(); let mut last_status: Vec<String> = Vec::new();
while running.load(Ordering::Relaxed) { while running.load(Ordering::Relaxed) {
// Poll running agents
let mut any_running = false; let mut any_running = false;
for agent in &mut agents { for agent in &mut agents {
agent.poll(); agent.poll();
if agent.is_running() { any_running = true; } if agent.is_running() { any_running = true; }
} }
// Check if any pending agents can now start
let mut newly_started = Vec::new();
pending.retain(|cfg| {
let deps_met = cfg.depends_on.iter().all(|dep| {
agents.iter().any(|a| &a.name == dep && !a.is_running())
});
if deps_met {
let cwd = expand_tilde(&cfg.cwd);
match Agent::spawn_with_config(next_id, &cfg.name, &cfg.task, &cwd, cfg.host.as_deref(), cfg.protocol.as_deref()) {
Ok(agent) => {
eprintln!(" started: {} ({})", cfg.name, cwd);
newly_started.push(agent);
}
Err(e) => eprintln!(" failed: {}: {e}", cfg.name),
}
false // remove from pending
} else {
true // keep waiting
}
});
for a in newly_started {
next_id += 1;
any_running = true;
agents.push(a);
}
if agents.iter().any(|a| a.dirty) { if agents.iter().any(|a| a.dirty) {
write_state(&agents); write_state(&agents);
for a in &mut agents { a.dirty = false; } for a in &mut agents { a.dirty = false; }
@@ -393,7 +428,7 @@ fn spawn_and_wait(configs: &[config::AgentConfig], running: &Arc<AtomicBool>) ->
last_status = current; last_status = current;
} }
if !any_running { break; } if !any_running && pending.is_empty() { break; }
std::thread::sleep(Duration::from_millis(200)); std::thread::sleep(Duration::from_millis(200));
} }
agents agents
@@ -426,7 +461,7 @@ fn extract_agent_configs(text: &str) -> Vec<config::AgentConfig> {
}; };
if name.is_empty() || task.is_empty() { return None; } if name.is_empty() || task.is_empty() { return None; }
Some(config::AgentConfig { name, task, cwd, host: None, protocol: None }) Some(config::AgentConfig { name, task, cwd, host: None, protocol: None, depends_on: vec![] })
}) })
.collect() .collect()
} }
@@ -749,6 +784,7 @@ pub fn commit() {
cwd, cwd,
host: None, host: None,
protocol: None, protocol: None,
depends_on: vec![],
}]; }];
let running = Arc::new(AtomicBool::new(true)); let running = Arc::new(AtomicBool::new(true));

View File

@@ -26,8 +26,7 @@ fn main() {
println!("{}", env!("CARGO_PKG_VERSION")); println!("{}", env!("CARGO_PKG_VERSION"));
} }
Some("help" | "--help" | "-h") => print_help(), Some("help" | "--help" | "-h") => print_help(),
None => print_help(), None | Some("tui") => {
Some("tui") => {
// Show logo before entering alternate screen // Show logo before entering alternate screen
eprintln!("\x1b[38;5;226m{}\x1b[0m\n\x1b[1m aishell\x1b[0m v{}\n", eprintln!("\x1b[38;5;226m{}\x1b[0m\n\x1b[1m aishell\x1b[0m v{}\n",
LOGO, env!("CARGO_PKG_VERSION")); LOGO, env!("CARGO_PKG_VERSION"));

View File

@@ -1,6 +1,6 @@
{ {
"ssh": "[protocol: ssh]\n[host: {host}]\nExecute commands via: ssh {host} \"command\"", "ssh": "[protocol: ssh]\n[host: {host}]\nThe host value is an SSH config name or user@host. Execute commands via: ssh {host} \"command\"\nDo not specify port or identity file — they are configured in ~/.ssh/config.\nIf the command fails, report the error. Do not retry with different connection parameters.",
"https": "[protocol: https]\n[endpoint: {host}]\nAccess via HTTP/HTTPS API calls.", "https": "[protocol: https]\n[endpoint: {host}]\nAccess via HTTP/HTTPS API calls. Use curl or equivalent.",
"at": "[protocol: at]\n[did: {host}]\nAccess via AT Protocol. Use ailog or atproto API.", "at": "[protocol: at]\n[did: {host}]\nAccess via AT Protocol. Use ailog or atproto API.",
"git": "[protocol: git]\n[remote: {host}]\nAccess via git commands." "git": "[protocol: git]\n[remote: {host}]\nAccess via git commands (clone, pull, push, log, etc)."
} }