1use crate::core::session::SessionState;
2use crate::core::workflow::{self, WorkflowRun, WorkflowSpec};
3use chrono::Utc;
4use serde_json::Value;
5
6pub fn handle_with_session(
7 args: Option<&serde_json::Map<String, Value>>,
8 session: &mut SessionState,
9) -> String {
10 handle_with_session_agent(args, session, None)
11}
12
13pub fn handle_with_session_agent(
14 args: Option<&serde_json::Map<String, Value>>,
15 session: &mut SessionState,
16 agent_id: Option<&str>,
17) -> String {
18 let action = get_str(args, "action").unwrap_or_else(|| "status".to_string());
19
20 match action.as_str() {
21 "start" => handle_start(args, agent_id),
22 "status" => handle_status(session, agent_id),
23 "stop" => handle_stop(agent_id),
24 "transition" => handle_transition(args, session, agent_id),
25 "complete" => handle_complete(args, session, agent_id),
26 "evidence_add" => handle_evidence_add(args, session, agent_id),
27 "evidence_list" => handle_evidence_list(session, agent_id),
28 _ => "Unknown action. Use: start, status, transition, complete, evidence_add, evidence_list, stop".to_string(),
29 }
30}
31
32fn handle_start(args: Option<&serde_json::Map<String, Value>>, agent_id: Option<&str>) -> String {
33 let spec_json = get_str(args, "spec");
34 let name_override = get_str(args, "name");
35
36 let mut spec: WorkflowSpec = match spec_json.as_deref() {
37 Some(s) if !s.trim().is_empty() => match serde_json::from_str::<WorkflowSpec>(s) {
38 Ok(v) => v,
39 Err(e) => return format!("Invalid spec JSON: {e}"),
40 },
41 _ => WorkflowSpec::builtin_plan_code_test(),
42 };
43
44 if let Some(name) = name_override {
45 if !name.trim().is_empty() {
46 spec.name = name;
47 }
48 }
49
50 if let Err(e) = workflow::validate_spec(&spec) {
51 return format!("Invalid WorkflowSpec: {e}");
52 }
53
54 let run = WorkflowRun::new(spec);
55 if let Err(e) = workflow::save_active_for_agent(&run, agent_id) {
56 return format!("Failed to save workflow: {e}");
57 }
58
59 format!(
60 "Workflow started: {}\n State: {}\n Started: {}",
61 run.spec.name, run.current, run.started_at
62 )
63}
64
65fn handle_status(session: &SessionState, agent_id: Option<&str>) -> String {
66 let Ok(active) = workflow::load_active_for_agent(agent_id) else {
67 return "Error: failed to load active workflow.".to_string();
68 };
69 let Some(run) = active else {
70 return "No active workflow. Use action=start to begin.".to_string();
71 };
72
73 let ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
74
75 let elapsed_min = chrono::Utc::now()
76 .signed_duration_since(run.updated_at)
77 .num_minutes();
78 let mut lines = vec![
79 format!("Workflow: {}", run.spec.name),
80 format!(" State: {}", run.current),
81 format!(" Updated: {} ({elapsed_min}m ago)", run.updated_at),
82 ];
83 if elapsed_min > 20 {
84 lines.push(" WARNING: Workflow inactive >20min, will auto-expire at 30min. Use action=stop to exit now.".to_string());
85 }
86
87 if let Some(state) = run.spec.state(&run.current) {
88 if let Some(ref tools) = state.allowed_tools {
89 let mut tools = tools.clone();
90 tools.sort();
91 let tools = tools.into_iter().take(30).collect::<Vec<_>>();
92 lines.push(format!(
93 " Allowed tools ({} shown): {}",
94 tools.len(),
95 tools.join(", ")
96 ));
97 }
98 }
99
100 let transitions = workflow::allowed_transitions(&run.spec, &run.current);
101 if transitions.is_empty() {
102 lines.push(" Transitions: (none)".to_string());
103 } else {
104 lines.push(" Transitions:".to_string());
105 for t in transitions.iter().take(10) {
106 let missing = workflow::missing_evidence_for_state(&run.spec, &t.to, |k| {
107 run.evidence.iter().any(|e| e.key == k)
108 || session.has_evidence_key(k)
109 || ledger.has_key(k)
110 });
111 if missing.is_empty() {
112 lines.push(format!(" → {} (ok)", t.to));
113 } else {
114 lines.push(format!(" → {} (missing: {})", t.to, missing.join(", ")));
115 }
116 }
117 }
118
119 lines.join("\n")
120}
121
122fn handle_stop(agent_id: Option<&str>) -> String {
123 match workflow::clear_active_for_agent(agent_id) {
124 Ok(()) => "Workflow stopped (active cleared).".to_string(),
125 Err(e) => format!("Error clearing workflow: {e}"),
126 }
127}
128
129fn handle_transition(
130 args: Option<&serde_json::Map<String, Value>>,
131 session: &SessionState,
132 agent_id: Option<&str>,
133) -> String {
134 let Some(to) = get_str(args, "to") else {
135 return "Error: 'to' is required for transition".to_string();
136 };
137 let note = get_str(args, "value");
138
139 let Ok(active) = workflow::load_active_for_agent(agent_id) else {
140 return "Error: failed to load active workflow.".to_string();
141 };
142 let Some(mut run) = active else {
143 return "No active workflow. Use action=start to begin.".to_string();
144 };
145
146 let ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
147 if let Err(e) = workflow::can_transition(&run.spec, &run.current, &to, |k| {
148 run.evidence.iter().any(|e| e.key == k) || session.has_evidence_key(k) || ledger.has_key(k)
149 }) {
150 return format!("Transition blocked: {e}");
151 }
152
153 let from = run.current.clone();
154 run.current.clone_from(&to);
155 run.updated_at = Utc::now();
156 run.transitions
157 .push(crate::core::workflow::TransitionRecord {
158 from: from.clone(),
159 to: to.clone(),
160 note: note.clone(),
161 timestamp: Utc::now(),
162 });
163
164 if let Err(e) = workflow::save_active_for_agent(&run, agent_id) {
165 return format!("Failed to save workflow: {e}");
166 }
167
168 format!("Transition: {from} → {to}")
169}
170
171fn handle_complete(
172 args: Option<&serde_json::Map<String, Value>>,
173 session: &SessionState,
174 agent_id: Option<&str>,
175) -> String {
176 let Ok(active) = workflow::load_active_for_agent(agent_id) else {
177 return "Error: failed to load active workflow.".to_string();
178 };
179 let Some(mut run) = active else {
180 return "No active workflow. Use action=start to begin.".to_string();
181 };
182 let note = get_str(args, "value");
183
184 let done = "done".to_string();
185 if workflow::find_transition(&run.spec, &run.current, &done).is_none() {
186 return format!("No transition to 'done' from '{}'", run.current);
187 }
188
189 let ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
190 if let Err(e) = workflow::can_transition(&run.spec, &run.current, &done, |k| {
191 run.evidence.iter().any(|e| e.key == k) || session.has_evidence_key(k) || ledger.has_key(k)
192 }) {
193 return format!("Complete blocked: {e}");
194 }
195
196 let from = run.current.clone();
197 run.current.clone_from(&done);
198 run.updated_at = Utc::now();
199 run.transitions
200 .push(crate::core::workflow::TransitionRecord {
201 from: from.clone(),
202 to: done.clone(),
203 note,
204 timestamp: Utc::now(),
205 });
206
207 if let Err(e) = workflow::clear_active_for_agent(agent_id) {
208 return format!("Workflow completed but failed to clear: {e}");
209 }
210
211 format!("Workflow completed: {from} → done (workflow cleared)")
212}
213
214fn handle_evidence_add(
215 args: Option<&serde_json::Map<String, Value>>,
216 session: &mut SessionState,
217 agent_id: Option<&str>,
218) -> String {
219 let Some(key) = get_str(args, "key") else {
220 return "Error: key is required".to_string();
221 };
222 let value = get_str(args, "value");
223
224 let Ok(active) = workflow::load_active_for_agent(agent_id) else {
225 return "Error: failed to load active workflow.".to_string();
226 };
227 let Some(mut run) = active else {
228 return "No active workflow. Use action=start to begin.".to_string();
229 };
230
231 run.add_manual_evidence(&key, value.as_deref());
232 session.record_manual_evidence(&key, value.as_deref());
233 {
234 let mut ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
235 ledger.record_manual(&key, value.as_deref(), chrono::Utc::now());
236 let _ = ledger.save();
237 }
238
239 if let Err(e) = workflow::save_active_for_agent(&run, agent_id) {
240 return format!("Failed to save workflow: {e}");
241 }
242
243 format!("Evidence added: {key}")
244}
245
246fn handle_evidence_list(session: &SessionState, agent_id: Option<&str>) -> String {
247 let Ok(active) = workflow::load_active_for_agent(agent_id) else {
248 return "Error: failed to load active workflow.".to_string();
249 };
250 let Some(run) = active else {
251 return "No active workflow.".to_string();
252 };
253
254 let ledger = crate::core::evidence_ledger::EvidenceLedgerV1::load();
255 let mut lines = vec![format!("Evidence (workflow: {}):", run.spec.name)];
256 if run.evidence.is_empty() && session.evidence.is_empty() && ledger.items.is_empty() {
257 lines.push(" (none)".to_string());
258 return lines.join("\n");
259 }
260
261 if !run.evidence.is_empty() {
262 lines.push(" Manual (workflow):".to_string());
263 for e in run.evidence.iter().rev().take(20) {
264 let v = e.value.as_deref().unwrap_or("-");
265 lines.push(format!(" {} = {} ({})", e.key, v, e.timestamp));
266 }
267 }
268
269 if !session.evidence.is_empty() {
270 lines.push(" Session receipts (latest):".to_string());
271 for e in session.evidence.iter().rev().take(20) {
272 lines.push(format!(" {} ({:?})", e.key, e.kind));
273 }
274 }
275
276 if !ledger.items.is_empty() {
277 lines.push(" Ledger (latest):".to_string());
278 for e in ledger.items.iter().rev().take(20) {
279 lines.push(format!(" {} ({:?})", e.key, e.kind));
280 }
281 }
282
283 lines.join("\n")
284}
285
286fn get_str(args: Option<&serde_json::Map<String, Value>>, key: &str) -> Option<String> {
287 args?
288 .get(key)?
289 .as_str()
290 .map(std::string::ToString::to_string)
291}