1use std::fs;
2use std::io::{stdout, Write};
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use serde_json::json;
7
8use crate::agent::{
9 analysis_report, compute_stats, load_agent_state, load_sim_agent_state, load_state, log_path,
10 pid_path, reset_sim, run_agent_loop, save_state, sim_journal_path, sim_state_path,
11 spawn_background, state_summary, stop_daemon,
12};
13use crate::cli::{AgentCommands, AgentSimCommands};
14use crate::config::RuntimeConfig;
15use crate::flatten::{build_agent_options_flatten_plan, execute_agent_options_flatten};
16use crate::output::{OutputFormat, ResponseEnvelope};
17use crate::rules::{rules_json_schema, RulesConfig};
18use crate::ui::context::DashboardContext;
19use crate::ui::dashboard::render_dashboard;
20use crate::ui::discover::resolve_rules_file;
21
22pub async fn run(runtime: &RuntimeConfig, command: AgentCommands) -> Result<()> {
23 match command {
24 AgentCommands::Schema => {
25 runtime.emit(ResponseEnvelope::ok("agent schema", rules_json_schema()));
26 }
27 AgentCommands::Validate { file } => {
28 let rules = RulesConfig::load(&file)?;
29 runtime.emit(ResponseEnvelope::ok(
30 "agent validate",
31 json!({
32 "valid": true,
33 "agent_id": rules.agent_id,
34 "accounts": rules.accounts.len(),
35 "watchlist": rules.watchlist,
36 "llm_enabled": rules.llm.enabled,
37 "telegram_enabled": rules.notify.telegram.enabled,
38 "simulation": rules.simulation,
39 }),
40 ));
41 }
42 AgentCommands::Status { rules_file } => {
43 if runtime.output == OutputFormat::Json {
44 if let Some(rules_path) = rules_file {
45 let ctx = DashboardContext::load(&rules_path)?;
46 runtime.emit(ResponseEnvelope::ok("agent status", ctx.to_json()));
47 } else {
48 emit_legacy_status(runtime)?;
49 }
50 } else if let Some(rules_path) = rules_file {
51 let ctx = DashboardContext::load(&rules_path)?;
52 print!("{}", render_dashboard(&ctx));
53 stdout().flush().ok();
54 } else if let Ok(rules_path) = resolve_rules_file(None, runtime.is_interactive()) {
55 let ctx = DashboardContext::load(&rules_path)?;
56 print!("{}", render_dashboard(&ctx));
57 stdout().flush().ok();
58 } else {
59 emit_legacy_status(runtime)?;
60 }
61 }
62 AgentCommands::Run {
63 file,
64 once,
65 background,
66 } => {
67 if background {
68 if once {
69 anyhow::bail!("--background cannot be combined with --once");
70 }
71 let mut extra = Vec::new();
72 if runtime.dry_run {
73 extra.push("--dry-run".into());
74 }
75 if runtime.simulate {
76 extra.push("--simulate".into());
77 }
78 if runtime.trust {
79 extra.push("--trust".into());
80 }
81 if runtime.yes {
82 extra.push("--yes".into());
83 }
84 if runtime.output == OutputFormat::Json {
85 extra.push("--json".into());
86 }
87 let pid = spawn_background(&file, &extra)?;
88 runtime.emit(ResponseEnvelope::ok(
89 "agent background",
90 json!({
91 "pid": pid,
92 "pid_file": pid_path(&file),
93 "log_file": log_path(&file),
94 "rules": file,
95 "simulate": runtime.simulate,
96 }),
97 ));
98 return Ok(());
99 }
100 run_agent_loop(runtime, &file, once, None).await?;
101 }
102 AgentCommands::Stop { file } => {
103 stop_daemon(&file)?;
104 runtime.emit(ResponseEnvelope::ok(
105 "agent stop",
106 json!({ "stopped": true, "rules": file }),
107 ));
108 }
109 AgentCommands::CloseAll { file } => {
110 let rules = RulesConfig::load(&file)?;
111 let state = if runtime.simulate {
112 load_sim_agent_state(&file, &rules.agent_id)
113 } else {
114 load_agent_state(&file, &rules.agent_id)
115 };
116 let api = runtime.build_api()?;
117 let plan = build_agent_options_flatten_plan(&api, &rules, &state).await?;
118 let data = execute_agent_options_flatten(runtime, &api, &file, &rules, &plan).await?;
119 runtime.emit(ResponseEnvelope::ok("agent close-all", data));
120 }
121 AgentCommands::Sim { command } => run_sim(runtime, command).await?,
122 }
123 Ok(())
124}
125
126async fn run_sim(runtime: &RuntimeConfig, command: AgentSimCommands) -> Result<()> {
127 match command {
128 AgentSimCommands::Stats { file } => {
129 let rules = RulesConfig::load(&file)?;
130 let mut state = load_sim_agent_state(&file, &rules.agent_id);
131 state.agent_id = rules.agent_id.clone();
132 let stats = compute_stats(&state, &rules);
133 runtime.emit(ResponseEnvelope::ok(
134 "agent sim stats",
135 json!({
136 "rules": file,
137 "state_path": sim_state_path(&file),
138 "stats": stats,
139 }),
140 ));
141 }
142 AgentSimCommands::Report { file, output } => {
143 let rules = RulesConfig::load(&file)?;
144 let mut state = load_sim_agent_state(&file, &rules.agent_id);
145 state.agent_id = rules.agent_id.clone();
146 let report = analysis_report(&state, &rules);
147 if let Some(path) = output {
148 let text = serde_json::to_string_pretty(&report)?;
149 fs::write(&path, text).with_context(|| format!("write report {}", path.display()))?;
150 runtime.emit(ResponseEnvelope::ok(
151 "agent sim report",
152 json!({ "rules": file, "output": path }),
153 ));
154 } else {
155 runtime.emit(ResponseEnvelope::ok("agent sim report", report));
156 }
157 }
158 AgentSimCommands::Reset { file } => {
159 if !runtime.yes {
160 anyhow::bail!("sim reset requires --yes (live agent-state is not modified)");
161 }
162 let rules = RulesConfig::load(&file)?;
163 let path = sim_state_path(&file);
164 let mut state = load_sim_agent_state(&file, &rules.agent_id);
165 state.agent_id = rules.agent_id.clone();
166 reset_sim(&mut state, &rules);
167 save_state(&path, &state)?;
168 let journal = sim_journal_path(&file);
169 if journal.exists() {
170 fs::remove_file(&journal)
171 .with_context(|| format!("remove journal {}", journal.display()))?;
172 }
173 runtime.emit(ResponseEnvelope::ok(
174 "agent sim reset",
175 json!({
176 "rules": file,
177 "state_path": path,
178 "starting_budget_usd": state
179 .sim
180 .as_ref()
181 .map(|s| s.starting_budget_usd),
182 }),
183 ));
184 }
185 }
186 Ok(())
187}
188
189fn emit_legacy_status(runtime: &RuntimeConfig) -> Result<()> {
190 let path = directories::ProjectDirs::from("com", "schwabinvestbot", "schwab")
191 .map(|d| d.data_local_dir().join("agent-state.json"))
192 .unwrap_or_else(|| PathBuf::from("agent-state.json"));
193 let state = load_state(&path)?;
194 runtime.emit(ResponseEnvelope::ok(
195 "agent status",
196 json!({ "state_path": path, "state": state_summary(&state) }),
197 ));
198 Ok(())
199}