Skip to content

Commit cf439dd

Browse files
authored
Merge pull request anomalyco#882 from affaan-m/feat/ecc2-live-streaming
feat(ecc2): live output streaming per agent
2 parents 61f70de + 9903ae5 commit cf439dd

7 files changed

Lines changed: 1019 additions & 204 deletions

File tree

‎ecc2/src/main.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod worktree;
77

88
use anyhow::Result;
99
use clap::Parser;
10+
use std::path::PathBuf;
1011
use tracing_subscriber::EnvFilter;
1112

1213
#[derive(Parser, Debug)]
@@ -46,6 +47,17 @@ enum Commands {
4647
},
4748
/// Run as background daemon
4849
Daemon,
50+
#[command(hide = true)]
51+
RunSession {
52+
#[arg(long)]
53+
session_id: String,
54+
#[arg(long)]
55+
task: String,
56+
#[arg(long)]
57+
agent: String,
58+
#[arg(long)]
59+
cwd: PathBuf,
60+
},
4961
}
5062

5163
#[tokio::main]
@@ -91,6 +103,14 @@ async fn main() -> Result<()> {
91103
println!("Starting ECC daemon...");
92104
session::daemon::run(db, cfg).await?;
93105
}
106+
Some(Commands::RunSession {
107+
session_id,
108+
task,
109+
agent,
110+
cwd,
111+
}) => {
112+
session::manager::run_session(&cfg, &session_id, &task, &agent, &cwd).await?;
113+
}
94114
}
95115

96116
Ok(())

‎ecc2/src/session/manager.rs‎

Lines changed: 128 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::path::{Path, PathBuf};
44
use std::process::Stdio;
55
use tokio::process::Command;
66

7+
use super::output::SessionOutputStore;
8+
use super::runtime::capture_command_output;
79
use super::store::StateStore;
810
use super::{Session, SessionMetrics, SessionState};
911
use crate::config::Config;
@@ -18,18 +20,7 @@ pub async fn create_session(
1820
) -> Result<String> {
1921
let repo_root =
2022
std::env::current_dir().context("Failed to resolve current working directory")?;
21-
let agent_program = agent_program(agent_type)?;
22-
23-
create_session_in_dir(
24-
db,
25-
cfg,
26-
task,
27-
agent_type,
28-
use_worktree,
29-
&repo_root,
30-
&agent_program,
31-
)
32-
.await
23+
queue_session_in_dir(db, cfg, task, agent_type, use_worktree, &repo_root).await
3324
}
3425

3526
pub fn list_sessions(db: &StateStore) -> Result<Vec<Session>> {
@@ -62,35 +53,107 @@ fn resolve_session(db: &StateStore, id: &str) -> Result<Session> {
6253
session.ok_or_else(|| anyhow::anyhow!("Session not found: {id}"))
6354
}
6455

65-
async fn create_session_in_dir(
56+
pub async fn run_session(
57+
cfg: &Config,
58+
session_id: &str,
59+
task: &str,
60+
agent_type: &str,
61+
working_dir: &Path,
62+
) -> Result<()> {
63+
let db = StateStore::open(&cfg.db_path)?;
64+
let session = resolve_session(&db, session_id)?;
65+
66+
if session.state != SessionState::Pending {
67+
tracing::info!(
68+
"Skipping run_session for {} because state is {}",
69+
session_id,
70+
session.state
71+
);
72+
return Ok(());
73+
}
74+
75+
let agent_program = agent_program(agent_type)?;
76+
let command = build_agent_command(&agent_program, task, session_id, working_dir);
77+
capture_command_output(
78+
cfg.db_path.clone(),
79+
session_id.to_string(),
80+
command,
81+
SessionOutputStore::default(),
82+
)
83+
.await?;
84+
Ok(())
85+
}
86+
87+
async fn queue_session_in_dir(
6688
db: &StateStore,
6789
cfg: &Config,
6890
task: &str,
6991
agent_type: &str,
7092
use_worktree: bool,
7193
repo_root: &Path,
72-
agent_program: &Path,
7394
) -> Result<String> {
95+
let session = build_session_record(task, agent_type, use_worktree, cfg, repo_root)?;
96+
db.insert_session(&session)?;
97+
98+
let working_dir = session
99+
.worktree
100+
.as_ref()
101+
.map(|worktree| worktree.path.as_path())
102+
.unwrap_or(repo_root);
103+
104+
match spawn_session_runner(task, &session.id, agent_type, working_dir).await {
105+
Ok(()) => Ok(session.id),
106+
Err(error) => {
107+
db.update_state(&session.id, &SessionState::Failed)?;
108+
109+
if let Some(worktree) = session.worktree.as_ref() {
110+
let _ = crate::worktree::remove(&worktree.path);
111+
}
112+
113+
Err(error.context(format!("Failed to queue session {}", session.id)))
114+
}
115+
}
116+
}
117+
118+
fn build_session_record(
119+
task: &str,
120+
agent_type: &str,
121+
use_worktree: bool,
122+
cfg: &Config,
123+
repo_root: &Path,
124+
) -> Result<Session> {
74125
let id = uuid::Uuid::new_v4().to_string()[..8].to_string();
75126
let now = chrono::Utc::now();
76127

77-
let wt = if use_worktree {
128+
let worktree = if use_worktree {
78129
Some(worktree::create_for_session_in_repo(&id, cfg, repo_root)?)
79130
} else {
80131
None
81132
};
82133

83-
let session = Session {
84-
id: id.clone(),
134+
Ok(Session {
135+
id,
85136
task: task.to_string(),
86137
agent_type: agent_type.to_string(),
87138
state: SessionState::Pending,
88139
pid: None,
89-
worktree: wt,
140+
worktree,
90141
created_at: now,
91142
updated_at: now,
92143
metrics: SessionMetrics::default(),
93-
};
144+
})
145+
}
146+
147+
async fn create_session_in_dir(
148+
db: &StateStore,
149+
cfg: &Config,
150+
task: &str,
151+
agent_type: &str,
152+
use_worktree: bool,
153+
repo_root: &Path,
154+
agent_program: &Path,
155+
) -> Result<String> {
156+
let session = build_session_record(task, agent_type, use_worktree, cfg, repo_root)?;
94157

95158
db.insert_session(&session)?;
96159

@@ -118,19 +181,60 @@ async fn create_session_in_dir(
118181
}
119182
}
120183

121-
async fn spawn_claude_code(
122-
agent_program: &Path,
184+
async fn spawn_session_runner(
123185
task: &str,
124186
session_id: &str,
187+
agent_type: &str,
125188
working_dir: &Path,
126-
) -> Result<u32> {
127-
let child = Command::new(agent_program)
189+
) -> Result<()> {
190+
let current_exe = std::env::current_exe().context("Failed to resolve ECC executable path")?;
191+
let child = Command::new(&current_exe)
192+
.arg("run-session")
193+
.arg("--session-id")
194+
.arg(session_id)
195+
.arg("--task")
196+
.arg(task)
197+
.arg("--agent")
198+
.arg(agent_type)
199+
.arg("--cwd")
200+
.arg(working_dir)
201+
.stdin(Stdio::null())
202+
.stdout(Stdio::null())
203+
.stderr(Stdio::null())
204+
.spawn()
205+
.with_context(|| {
206+
format!(
207+
"Failed to spawn ECC runner from {}",
208+
current_exe.display()
209+
)
210+
})?;
211+
212+
child
213+
.id()
214+
.ok_or_else(|| anyhow::anyhow!("ECC runner did not expose a process id"))?;
215+
Ok(())
216+
}
217+
218+
fn build_agent_command(agent_program: &Path, task: &str, session_id: &str, working_dir: &Path) -> Command {
219+
let mut command = Command::new(agent_program);
220+
command
128221
.arg("--print")
129222
.arg("--name")
130223
.arg(format!("ecc-{session_id}"))
131224
.arg(task)
132225
.current_dir(working_dir)
133-
.stdin(Stdio::null())
226+
.stdin(Stdio::null());
227+
command
228+
}
229+
230+
async fn spawn_claude_code(
231+
agent_program: &Path,
232+
task: &str,
233+
session_id: &str,
234+
working_dir: &Path,
235+
) -> Result<u32> {
236+
let mut command = build_agent_command(agent_program, task, session_id, working_dir);
237+
let child = command
134238
.stdout(Stdio::null())
135239
.stderr(Stdio::null())
136240
.spawn()

‎ecc2/src/session/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
pub mod daemon;
22
pub mod manager;
3+
pub mod output;
4+
pub mod runtime;
35
pub mod store;
46

57
use chrono::{DateTime, Utc};

0 commit comments

Comments
 (0)