cellstate/entity/
trajectory.rs

1//! `cellstate trajectory` — Manage trajectories (threads of agent computation).
2//!
3//! Subcommands: create, list, get, update, delete, scopes, children, outcome, outcome-graph.
4
5use 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}