1use anyhow::{anyhow, Result};
6use clap::{Arg, ArgMatches, Command};
7
8use cellstate_core::api_types::{CreateTurnRequest, TurnResponse};
9
10use super::client::ApiClient;
11use super::output::OutputConfig;
12use super::require_arg;
13
14const BASE: &str = "/api/v1/turns";
15
16pub fn build_command() -> Command {
17 Command::new("turn")
18 .about("Manage turns (individual messages within a scope)")
19 .subcommand_required(true)
20 .subcommand(
21 Command::new("create")
22 .about("Create a new turn")
23 .arg(
24 Arg::new("scope-id")
25 .long("scope-id")
26 .help("Scope ID this turn belongs to (defaults to session scope)"),
27 )
28 .arg(
29 Arg::new("role")
30 .long("role")
31 .required(true)
32 .help("Role of the turn (user, assistant, system, tool)"),
33 )
34 .arg(
35 Arg::new("content")
36 .long("content")
37 .required(true)
38 .help("Content of the turn"),
39 )
40 .arg(
41 Arg::new("sequence")
42 .long("sequence")
43 .default_value("0")
44 .help("Sequence number within the scope"),
45 )
46 .arg(
47 Arg::new("token-count")
48 .long("token-count")
49 .default_value("0")
50 .help("Token count"),
51 )
52 .arg(
53 Arg::new("tool-calls")
54 .long("tool-calls")
55 .help("Tool calls as JSON"),
56 )
57 .arg(
58 Arg::new("tool-results")
59 .long("tool-results")
60 .help("Tool results as JSON"),
61 )
62 .arg(
63 Arg::new("metadata")
64 .long("metadata")
65 .help("Additional metadata as JSON"),
66 ),
67 )
68 .subcommand(
69 Command::new("get")
70 .about("Get turn details")
71 .arg(Arg::new("id").required(true).help("Turn ID")),
72 )
73}
74
75pub async fn dispatch(
76 matches: &ArgMatches,
77 client: &ApiClient,
78 output: &OutputConfig,
79 session: &crate::session::CliSession,
80) -> Result<()> {
81 match matches.subcommand() {
82 Some(("create", sub)) => {
83 let scope_id =
84 require_arg(sub, "scope-id", session.scope_id.as_deref(), "scope")?.parse()?;
85 let role = sub
86 .get_one::<String>("role")
87 .unwrap()
88 .parse()
89 .map_err(|e: String| anyhow!(e))?;
90 let content = sub.get_one::<String>("content").unwrap().clone();
91 let sequence: i32 = sub.get_one::<String>("sequence").unwrap().parse()?;
92 let token_count: i32 = sub.get_one::<String>("token-count").unwrap().parse()?;
93 let tool_calls = sub
94 .get_one::<String>("tool-calls")
95 .map(|s| serde_json::from_str(s))
96 .transpose()?;
97 let tool_results = sub
98 .get_one::<String>("tool-results")
99 .map(|s| serde_json::from_str(s))
100 .transpose()?;
101 let metadata = sub
102 .get_one::<String>("metadata")
103 .map(|s| serde_json::from_str(s))
104 .transpose()?;
105 let req = CreateTurnRequest {
106 scope_id,
107 sequence,
108 role,
109 content,
110 token_count,
111 tool_calls,
112 tool_results,
113 metadata,
114 };
115 let resp: TurnResponse = client.post(BASE, &req).await?;
116 output.print(&resp);
117 }
118 Some(("get", sub)) => {
119 let id = sub.get_one::<String>("id").unwrap();
120 let resp: TurnResponse = client.get(&format!("{BASE}/{id}")).await?;
121 output.print(&resp);
122 }
123 _ => unreachable!("subcommand_required(true) prevents this"),
124 }
125 Ok(())
126}