cellstate/entity/
trajectory.rs1use anyhow::{anyhow, Result};
6use clap::{Arg, ArgMatches, Command};
7
8use cellstate_core::api_types::{
9 CreateTrajectoryRequest, ListTrajectoriesResponse, OutcomeGraphResponse, ReportOutcomeRequest,
10 TrajectoryResponse, UpdateTrajectoryRequest,
11};
12
13use super::client::ApiClient;
14use super::output::OutputConfig;
15
16const BASE: &str = "/api/v1/trajectories";
17
18pub fn build_command() -> Command {
19 Command::new("trajectory")
20 .about("Manage trajectories (threads of agent computation)")
21 .subcommand_required(true)
22 .subcommand(
23 Command::new("create")
24 .about("Create a new trajectory")
25 .arg(Arg::new("name").required(true).help("Trajectory name"))
26 .arg(
27 Arg::new("description")
28 .long("description")
29 .short('d')
30 .help("Description"),
31 )
32 .arg(Arg::new("agent-id").long("agent-id").help("Agent ID"))
33 .arg(
34 Arg::new("parent-id")
35 .long("parent-id")
36 .help("Parent trajectory ID"),
37 ),
38 )
39 .subcommand(
40 Command::new("list")
41 .about("List trajectories")
42 .arg(
43 Arg::new("status")
44 .long("status")
45 .help("Filter by status (active, completed, failed, suspended)"),
46 )
47 .arg(
48 Arg::new("agent-id")
49 .long("agent-id")
50 .help("Filter by agent"),
51 )
52 .arg(
53 Arg::new("limit")
54 .long("limit")
55 .default_value("20")
56 .help("Max results"),
57 )
58 .arg(
59 Arg::new("offset")
60 .long("offset")
61 .default_value("0")
62 .help("Offset"),
63 ),
64 )
65 .subcommand(
66 Command::new("get")
67 .about("Get trajectory details")
68 .arg(Arg::new("id").required(true).help("Trajectory ID")),
69 )
70 .subcommand(
71 Command::new("update")
72 .about("Update a trajectory")
73 .arg(Arg::new("id").required(true).help("Trajectory ID"))
74 .arg(Arg::new("name").long("name").help("New name"))
75 .arg(
76 Arg::new("description")
77 .long("description")
78 .short('d')
79 .help("New description"),
80 )
81 .arg(
82 Arg::new("status")
83 .long("status")
84 .help("New status (active, completed, failed, suspended)"),
85 ),
86 )
87 .subcommand(
88 Command::new("delete")
89 .about("Delete a trajectory")
90 .arg(Arg::new("id").required(true).help("Trajectory ID")),
91 )
92 .subcommand(
93 Command::new("scopes")
94 .about("List scopes in a trajectory")
95 .arg(Arg::new("id").required(true).help("Trajectory ID")),
96 )
97 .subcommand(
98 Command::new("children")
99 .about("List child trajectories")
100 .arg(Arg::new("id").required(true).help("Trajectory ID")),
101 )
102 .subcommand(
103 Command::new("outcome")
104 .about("Report trajectory outcome")
105 .arg(Arg::new("id").required(true).help("Trajectory ID"))
106 .arg(
107 Arg::new("status")
108 .required(true)
109 .help("Outcome status (success, failure, partial)"),
110 )
111 .arg(Arg::new("summary").required(true).help("Outcome summary")),
112 )
113 .subcommand(
114 Command::new("outcome-graph")
115 .about("Get outcome graph")
116 .arg(Arg::new("id").required(true).help("Trajectory ID")),
117 )
118}
119
120pub async fn dispatch(
121 matches: &ArgMatches,
122 client: &ApiClient,
123 output: &OutputConfig,
124 session: &crate::session::CliSession,
125) -> Result<()> {
126 match matches.subcommand() {
127 Some(("create", sub)) => {
128 let name = sub.get_one::<String>("name").unwrap().clone();
129 let req = CreateTrajectoryRequest {
130 name,
131 description: sub.get_one::<String>("description").cloned(),
132 parent_trajectory_id: sub
133 .get_one::<String>("parent-id")
134 .map(|s| s.parse())
135 .transpose()?,
136 agent_id: super::resolve_arg(sub, "agent-id", session.agent_id.as_deref())
137 .map(|s| s.parse())
138 .transpose()?,
139 metadata: None,
140 };
141 let resp: TrajectoryResponse = client.post(BASE, &req).await?;
142 output.print(&resp);
143 }
144 Some(("list", sub)) => {
145 let limit: i64 = sub.get_one::<String>("limit").unwrap().parse()?;
146 let offset: i64 = sub.get_one::<String>("offset").unwrap().parse()?;
147 let mut path = format!("{BASE}?limit={limit}&offset={offset}");
148 if let Some(status) = sub.get_one::<String>("status") {
149 path.push_str(&format!("&status={status}"));
150 }
151 if let Some(agent_id) = super::resolve_arg(sub, "agent-id", session.agent_id.as_deref())
152 {
153 path.push_str(&format!("&agent_id={agent_id}"));
154 }
155 let resp: ListTrajectoriesResponse = client.get(&path).await?;
156 output.print_list(&resp.trajectories, resp.total);
157 }
158 Some(("get", sub)) => {
159 let id = sub.get_one::<String>("id").unwrap();
160 let resp: TrajectoryResponse = client.get(&format!("{BASE}/{id}")).await?;
161 output.print(&resp);
162 }
163 Some(("update", sub)) => {
164 let id = sub.get_one::<String>("id").unwrap();
165 let req = UpdateTrajectoryRequest {
166 name: sub.get_one::<String>("name").cloned(),
167 description: sub.get_one::<String>("description").cloned(),
168 status: sub
169 .get_one::<String>("status")
170 .map(|s| s.parse().map_err(|e: String| anyhow!(e)))
171 .transpose()?,
172 metadata: None,
173 };
174 let resp: TrajectoryResponse = client.patch(&format!("{BASE}/{id}"), &req).await?;
175 output.print(&resp);
176 }
177 Some(("delete", sub)) => {
178 let id = sub.get_one::<String>("id").unwrap();
179 client.delete(&format!("{BASE}/{id}")).await?;
180 if output.json {
181 println!(r#"{{"deleted": "{id}"}}"#);
182 } else {
183 println!("Deleted trajectory {id}");
184 }
185 }
186 Some(("scopes", sub)) => {
187 let id = sub.get_one::<String>("id").unwrap();
188 let resp: serde_json::Value = client.get_raw(&format!("{BASE}/{id}/scopes")).await?;
189 output.print_value(&resp);
190 }
191 Some(("children", sub)) => {
192 let id = sub.get_one::<String>("id").unwrap();
193 let resp: serde_json::Value = client.get_raw(&format!("{BASE}/{id}/children")).await?;
194 output.print_value(&resp);
195 }
196 Some(("outcome", sub)) => {
197 let id = sub.get_one::<String>("id").unwrap();
198 let req = ReportOutcomeRequest {
199 status: sub
200 .get_one::<String>("status")
201 .unwrap()
202 .parse()
203 .map_err(|e: String| anyhow!(e))?,
204 summary: sub.get_one::<String>("summary").unwrap().clone(),
205 produced_artifacts: vec![],
206 produced_notes: vec![],
207 linked_trajectory_ids: vec![],
208 error: None,
209 };
210 let resp: serde_json::Value = client
211 .post_raw(&format!("{BASE}/{id}/outcome"), &req)
212 .await?;
213 output.print_value(&resp);
214 }
215 Some(("outcome-graph", sub)) => {
216 let id = sub.get_one::<String>("id").unwrap();
217 let resp: OutcomeGraphResponse =
218 client.get(&format!("{BASE}/{id}/outcome-graph")).await?;
219 output.print(&resp);
220 }
221 _ => unreachable!("subcommand_required(true) prevents this"),
222 }
223 Ok(())
224}