1use crate::service_discovery::json_lite::JsonValue;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Role {
9 User,
10 Agent,
11}
12
13impl Role {
14 fn as_str(self) -> &'static str {
15 match self {
16 Role::User => "ROLE_USER",
17 Role::Agent => "ROLE_AGENT",
18 }
19 }
20}
21
22#[derive(Debug, Clone, PartialEq)]
25pub struct Part {
26 pub content: PartContent,
27 pub media_type: Option<String>,
28 pub filename: Option<String>,
29 pub metadata: Option<String>,
31}
32
33#[derive(Debug, Clone, PartialEq)]
35pub enum PartContent {
36 Text(String),
37 Raw(Vec<u8>),
39 Url(String),
40 Data(String),
42}
43
44impl Part {
45 pub fn text(text: impl Into<String>) -> Part {
46 Part { content: PartContent::Text(text.into()), media_type: None, filename: None, metadata: None }
47 }
48
49 pub fn data(json: impl Into<String>) -> Part {
51 Part { content: PartContent::Data(json.into()), media_type: Some("application/json".to_string()), filename: None, metadata: None }
52 }
53
54 pub fn raw(bytes: Vec<u8>, media_type: &str) -> Part {
55 Part { content: PartContent::Raw(bytes), media_type: Some(media_type.to_string()), filename: None, metadata: None }
56 }
57
58 pub fn url(url: impl Into<String>, media_type: &str) -> Part {
59 Part { content: PartContent::Url(url.into()), media_type: Some(media_type.to_string()), filename: None, metadata: None }
60 }
61
62 pub fn filename(mut self, name: impl Into<String>) -> Part {
63 self.filename = Some(name.into());
64 self
65 }
66
67 pub fn as_text(&self) -> Option<&str> {
69 match &self.content {
70 PartContent::Text(t) => Some(t),
71 _ => None,
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq)]
78pub struct Message {
79 pub message_id: String,
80 pub context_id: Option<String>,
81 pub task_id: Option<String>,
82 pub role: Role,
83 pub parts: Vec<Part>,
84 pub metadata: Option<String>,
85 pub extensions: Vec<String>,
86 pub reference_task_ids: Vec<String>,
87}
88
89impl Message {
90 pub fn agent_text(text: impl Into<String>) -> Message {
92 Message::agent(vec![Part::text(text)])
93 }
94
95 pub fn agent(parts: Vec<Part>) -> Message {
97 Message {
98 message_id: super::new_id(),
99 context_id: None,
100 task_id: None,
101 role: Role::Agent,
102 parts,
103 metadata: None,
104 extensions: Vec::new(),
105 reference_task_ids: Vec::new(),
106 }
107 }
108
109 pub fn text(&self) -> String {
111 self.parts.iter().filter_map(Part::as_text).collect::<Vec<_>>().join("\n")
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum TaskState {
118 Submitted,
119 Working,
120 InputRequired,
121 AuthRequired,
122 Completed,
123 Failed,
124 Canceled,
125 Rejected,
126}
127
128impl TaskState {
129 pub fn as_str(self) -> &'static str {
130 match self {
131 TaskState::Submitted => "TASK_STATE_SUBMITTED",
132 TaskState::Working => "TASK_STATE_WORKING",
133 TaskState::InputRequired => "TASK_STATE_INPUT_REQUIRED",
134 TaskState::AuthRequired => "TASK_STATE_AUTH_REQUIRED",
135 TaskState::Completed => "TASK_STATE_COMPLETED",
136 TaskState::Failed => "TASK_STATE_FAILED",
137 TaskState::Canceled => "TASK_STATE_CANCELED",
138 TaskState::Rejected => "TASK_STATE_REJECTED",
139 }
140 }
141
142 pub fn parse(s: &str) -> Option<TaskState> {
143 [
144 TaskState::Submitted,
145 TaskState::Working,
146 TaskState::InputRequired,
147 TaskState::AuthRequired,
148 TaskState::Completed,
149 TaskState::Failed,
150 TaskState::Canceled,
151 TaskState::Rejected,
152 ]
153 .into_iter()
154 .find(|t| t.as_str() == s)
155 }
156
157 pub fn is_terminal(self) -> bool {
159 matches!(self, TaskState::Completed | TaskState::Failed | TaskState::Canceled | TaskState::Rejected)
160 }
161
162 pub fn is_interrupted(self) -> bool {
164 matches!(self, TaskState::InputRequired | TaskState::AuthRequired)
165 }
166}
167
168#[derive(Debug, Clone, PartialEq)]
170pub struct TaskStatus {
171 pub state: TaskState,
172 pub message: Option<Message>,
173 pub timestamp: String,
175}
176
177#[derive(Debug, Clone, PartialEq)]
179pub struct Artifact {
180 pub artifact_id: String,
181 pub name: Option<String>,
182 pub description: Option<String>,
183 pub parts: Vec<Part>,
184 pub metadata: Option<String>,
185}
186
187impl Artifact {
188 pub fn new(name: impl Into<String>, parts: Vec<Part>) -> Artifact {
189 Artifact { artifact_id: super::new_id(), name: Some(name.into()), description: None, parts, metadata: None }
190 }
191
192 pub fn text(name: impl Into<String>, text: impl Into<String>) -> Artifact {
194 Artifact::new(name, vec![Part::text(text)])
195 }
196}
197
198#[derive(Debug, Clone, PartialEq)]
200pub struct Task {
201 pub id: String,
202 pub context_id: String,
203 pub status: TaskStatus,
204 pub artifacts: Vec<Artifact>,
205 pub history: Vec<Message>,
206 pub metadata: Option<String>,
207}
208
209pub(crate) fn quote(s: &str) -> String {
212 let mut out = String::with_capacity(s.len() + 2);
213 out.push('"');
214 for c in s.chars() {
215 match c {
216 '"' => out.push_str("\\\""),
217 '\\' => out.push_str("\\\\"),
218 '\n' => out.push_str("\\n"),
219 '\r' => out.push_str("\\r"),
220 '\t' => out.push_str("\\t"),
221 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
222 c => out.push(c),
223 }
224 }
225 out.push('"');
226 out
227}
228
229pub(crate) struct Obj(String);
231
232impl Obj {
233 pub(crate) fn new() -> Obj {
234 Obj(String::from("{"))
235 }
236
237 fn key(&mut self, key: &str) {
238 if self.0.len() > 1 {
239 self.0.push(',');
240 }
241 self.0.push_str("e(key));
242 self.0.push(':');
243 }
244
245 pub(crate) fn str(mut self, key: &str, value: &str) -> Obj {
246 self.key(key);
247 self.0.push_str("e(value));
248 self
249 }
250
251 pub(crate) fn opt_str(self, key: &str, value: Option<&str>) -> Obj {
252 match value {
253 Some(v) => self.str(key, v),
254 None => self,
255 }
256 }
257
258 pub(crate) fn raw(mut self, key: &str, json: &str) -> Obj {
260 self.key(key);
261 self.0.push_str(json);
262 self
263 }
264
265 pub(crate) fn opt_raw(self, key: &str, json: Option<&str>) -> Obj {
266 match json {
267 Some(j) => self.raw(key, j),
268 None => self,
269 }
270 }
271
272 pub(crate) fn list(self, key: &str, items: &[String]) -> Obj {
273 let json = format!("[{}]", items.join(","));
274 self.raw(key, &json)
275 }
276
277 pub(crate) fn strs(self, key: &str, items: &[String]) -> Obj {
278 let quoted: Vec<String> = items.iter().map(|s| quote(s)).collect();
279 self.list(key, "ed)
280 }
281
282 pub(crate) fn done(mut self) -> String {
283 self.0.push('}');
284 self.0
285 }
286}
287
288impl Part {
289 pub(crate) fn to_json(&self) -> String {
290 let o = match &self.content {
291 PartContent::Text(t) => Obj::new().str("text", t),
292 PartContent::Raw(b) => Obj::new().str("raw", &crate::core::base64::Base64::encode(b).unwrap_or_default()),
293 PartContent::Url(u) => Obj::new().str("url", u),
294 PartContent::Data(d) => Obj::new().raw("data", d),
295 };
296 o.opt_str("mediaType", self.media_type.as_deref()).opt_str("filename", self.filename.as_deref()).opt_raw("metadata", self.metadata.as_deref()).done()
297 }
298}
299
300impl Message {
301 pub(crate) fn to_json(&self) -> String {
302 let parts: Vec<String> = self.parts.iter().map(Part::to_json).collect();
303 let mut o = Obj::new()
304 .str("messageId", &self.message_id)
305 .opt_str("contextId", self.context_id.as_deref())
306 .opt_str("taskId", self.task_id.as_deref())
307 .str("role", self.role.as_str())
308 .list("parts", &parts)
309 .opt_raw("metadata", self.metadata.as_deref());
310 if !self.extensions.is_empty() {
311 o = o.strs("extensions", &self.extensions);
312 }
313 if !self.reference_task_ids.is_empty() {
314 o = o.strs("referenceTaskIds", &self.reference_task_ids);
315 }
316 o.done()
317 }
318}
319
320impl TaskStatus {
321 pub(crate) fn to_json(&self) -> String {
322 let message = self.message.as_ref().map(Message::to_json);
323 Obj::new().str("state", self.state.as_str()).opt_raw("message", message.as_deref()).str("timestamp", &self.timestamp).done()
324 }
325}
326
327impl Artifact {
328 pub(crate) fn to_json(&self) -> String {
329 let parts: Vec<String> = self.parts.iter().map(Part::to_json).collect();
330 Obj::new()
331 .str("artifactId", &self.artifact_id)
332 .opt_str("name", self.name.as_deref())
333 .opt_str("description", self.description.as_deref())
334 .list("parts", &parts)
335 .opt_raw("metadata", self.metadata.as_deref())
336 .done()
337 }
338}
339
340impl Task {
341 pub(crate) fn to_json(&self, history_length: Option<usize>) -> String {
344 let keep = history_length.unwrap_or(usize::MAX).min(self.history.len());
345 let history: Vec<String> = self.history[self.history.len() - keep..].iter().map(Message::to_json).collect();
346 let artifacts: Vec<String> = self.artifacts.iter().map(Artifact::to_json).collect();
347 let mut o = Obj::new().str("id", &self.id).str("contextId", &self.context_id).raw("status", &self.status.to_json());
348 if !artifacts.is_empty() {
349 o = o.list("artifacts", &artifacts);
350 }
351 if !history.is_empty() {
352 o = o.list("history", &history);
353 }
354 o.opt_raw("metadata", self.metadata.as_deref()).done()
355 }
356}
357
358pub(crate) fn to_json(value: &JsonValue) -> String {
360 match value {
361 JsonValue::Null => "null".to_string(),
362 JsonValue::Bool(b) => b.to_string(),
363 JsonValue::Number(n) if n.fract() == 0.0 && n.abs() < 9.0e15 => format!("{}", *n as i64),
364 JsonValue::Number(n) => format!("{n}"),
365 JsonValue::String(s) => quote(s),
366 JsonValue::Array(items) => format!("[{}]", items.iter().map(to_json).collect::<Vec<_>>().join(",")),
367 JsonValue::Object(pairs) => format!("{{{}}}", pairs.iter().map(|(k, v)| format!("{}:{}", quote(k), to_json(v))).collect::<Vec<_>>().join(",")),
368 }
369}
370
371pub(crate) fn field<'a>(value: &'a JsonValue, camel: &str) -> Option<&'a JsonValue> {
376 value.get(camel).or_else(|| {
377 let mut snake = String::with_capacity(camel.len() + 4);
378 for c in camel.chars() {
379 if c.is_ascii_uppercase() {
380 snake.push('_');
381 snake.push(c.to_ascii_lowercase());
382 } else {
383 snake.push(c);
384 }
385 }
386 (snake != camel).then(|| value.get(&snake)).flatten()
387 })
388}
389
390fn string_field(value: &JsonValue, key: &str) -> Option<String> {
391 field(value, key).and_then(JsonValue::as_str).map(str::to_string)
392}
393
394fn object_field(value: &JsonValue, key: &str) -> Option<String> {
395 field(value, key).filter(|v| matches!(v, JsonValue::Object(_))).map(to_json)
396}
397
398fn strings(value: &JsonValue, key: &str) -> Vec<String> {
399 field(value, key).and_then(JsonValue::as_array).map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_string)).collect()).unwrap_or_default()
400}
401
402impl Part {
403 pub(crate) fn from_json(value: &JsonValue) -> Result<Part, String> {
404 let present: Vec<&str> = ["text", "raw", "url", "data"].into_iter().filter(|k| value.get(k).is_some()).collect();
405 if present.len() != 1 {
406 return Err("a part must contain exactly one of text, raw, url, data".to_string());
407 }
408 let content = match present[0] {
409 "text" => PartContent::Text(string_field(value, "text").ok_or("part text must be a string")?),
410 "url" => PartContent::Url(string_field(value, "url").ok_or("part url must be a string")?),
411 "raw" => {
412 let encoded = string_field(value, "raw").ok_or("part raw must be a base64 string")?;
413 PartContent::Raw(crate::core::base64::Base64::decode(encoded).map_err(|_| "part raw isn't valid base64".to_string())?)
414 }
415 _ => PartContent::Data(to_json(value.get("data").unwrap_or(&JsonValue::Null))),
416 };
417 Ok(Part { content, media_type: string_field(value, "mediaType"), filename: string_field(value, "filename"), metadata: object_field(value, "metadata") })
418 }
419}
420
421impl Message {
422 pub(crate) fn from_json(value: &JsonValue) -> Result<Message, String> {
423 if !matches!(value, JsonValue::Object(_)) {
424 return Err("message must be an object".to_string());
425 }
426 let message_id = string_field(value, "messageId").filter(|s| !s.is_empty()).ok_or("message.messageId is required")?;
427 let role = match value.get("role").and_then(JsonValue::as_str) {
428 Some("ROLE_USER") | Some("user") => Role::User,
429 Some("ROLE_AGENT") | Some("agent") => Role::Agent,
430 _ => return Err("message.role must be ROLE_USER or ROLE_AGENT".to_string()),
431 };
432 let parts_json = value.get("parts").and_then(JsonValue::as_array).ok_or("message.parts is required")?;
433 if parts_json.is_empty() {
434 return Err("message.parts must not be empty".to_string());
435 }
436 let parts = parts_json.iter().map(Part::from_json).collect::<Result<Vec<_>, _>>()?;
437 Ok(Message {
438 message_id,
439 context_id: string_field(value, "contextId").filter(|s| !s.is_empty()),
440 task_id: string_field(value, "taskId").filter(|s| !s.is_empty()),
441 role,
442 parts,
443 metadata: object_field(value, "metadata"),
444 extensions: strings(value, "extensions"),
445 reference_task_ids: strings(value, "referenceTaskIds"),
446 })
447 }
448}
449
450pub(crate) fn timestamp() -> String {
452 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
453 let secs = now.as_secs();
454 let (days, rest) = ((secs / 86_400) as i64, secs % 86_400);
455 let z = days + 719_468;
457 let era = z.div_euclid(146_097);
458 let doe = z - era * 146_097;
459 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
460 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
461 let mp = (5 * doy + 2) / 153;
462 let day = doy - (153 * mp + 2) / 5 + 1;
463 let month = if mp < 10 { mp + 3 } else { mp - 9 };
464 let year = yoe + era * 400 + if month <= 2 { 1 } else { 0 };
465 format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{:03}Z", rest / 3600, rest % 3600 / 60, rest % 60, now.subsec_millis())
466}