speccoder_runtime/acp/
calls.rs1use std::path::PathBuf;
13
14#[derive(Debug, Clone, Default, PartialEq)]
17pub struct ToolReport {
18 pub id: String,
20 pub title: Option<String>,
22 pub kind: Option<String>,
24 pub status: Option<String>,
26 pub raw_input: Option<serde_json::Value>,
28 pub locations: Option<Vec<PathBuf>>,
30}
31
32impl ToolReport {
33 #[must_use]
36 pub fn from_wire(id: String, fields: &serde_json::Value) -> Self {
37 let text = |k: &str| fields.get(k).and_then(|v| v.as_str()).map(str::to_owned);
38 Self {
39 id,
40 title: text("title"),
41 kind: text("kind"),
42 status: text("status"),
43 raw_input: fields.get("rawInput").filter(|v| !v.is_null()).cloned(),
44 locations: fields
45 .get("locations")
46 .and_then(|v| v.as_array())
47 .map(|ls| {
48 ls.iter()
49 .filter_map(|l| l.get("path").and_then(|p| p.as_str()))
50 .map(PathBuf::from)
51 .collect()
52 }),
53 }
54 }
55}
56
57#[derive(Debug, Clone, Default, PartialEq)]
61pub struct ToolRequest {
62 pub id: String,
64 pub title: Option<String>,
66 pub kind: Option<String>,
69 pub raw_input: Option<serde_json::Value>,
71 pub locations: Vec<PathBuf>,
73 pub status: Option<String>,
75}
76
77impl ToolRequest {
78 pub fn merge(&mut self, r: &ToolReport) {
81 if let Some(t) = &r.title {
82 self.title = Some(t.chars().take(200).collect());
83 }
84 if let Some(k) = &r.kind {
85 self.kind = Some(k.clone());
86 }
87 if let Some(i) = &r.raw_input {
88 self.raw_input = Some(i.clone());
89 }
90 if let Some(l) = &r.locations {
91 self.locations.clone_from(l);
92 }
93 if let Some(s) = &r.status {
94 self.status = Some(s.clone());
95 }
96 }
97
98 #[must_use]
100 pub fn shown_title(&self) -> String {
101 match self.title.as_deref() {
102 Some(t) if !t.trim().is_empty() => t.to_owned(),
103 _ => format!("untitled call {}", self.id),
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use serde_json::json;
112
113 fn wire(v: serde_json::Value) -> ToolReport {
114 ToolReport::from_wire("c1".into(), &v)
115 }
116
117 #[test]
118 fn a_present_field_replaces_and_an_absent_or_null_one_keeps() {
119 let mut call = ToolRequest {
120 id: "c1".into(),
121 ..ToolRequest::default()
122 };
123 call.merge(&wire(json!({
124 "title": "Run a command", "kind": "execute",
125 "rawInput": {"command": "ls"}, "locations": [{"path": "/a"}]
126 })));
127 call.merge(&wire(json!({"rawInput": {"command": "rm -rf build"}})));
128 assert_eq!(call.title.as_deref(), Some("Run a command"));
129 assert_eq!(call.raw_input, Some(json!({"command": "rm -rf build"})));
130 call.merge(&wire(
131 json!({"title": null, "kind": null, "rawInput": null, "locations": null}),
132 ));
133 assert_eq!(call.kind.as_deref(), Some("execute"), "null keeps");
134 assert_eq!(call.locations, vec![PathBuf::from("/a")]);
135 call.merge(&wire(json!({"locations": []})));
136 assert!(call.locations.is_empty(), "an empty list is said");
137 assert_eq!(
138 ToolRequest {
139 id: "t9".into(),
140 ..ToolRequest::default()
141 }
142 .shown_title(),
143 "untitled call t9"
144 );
145 }
146}