1use std::num::NonZeroUsize;
4
5use serde_json::{Value, json};
6
7use super::tools::ToolResult;
8
9pub struct HeldWords<'a> {
11 draft: &'a str,
12 words: &'a str,
13}
14
15impl std::fmt::Debug for HeldWords<'_> {
16 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 formatter.debug_struct("HeldWords").finish_non_exhaustive()
18 }
19}
20
21#[derive(Debug, thiserror::Error)]
23#[error("held_result_invalid")]
24pub struct HeldResultInvalid;
25
26impl<'a> HeldWords<'a> {
27 pub fn new(
34 draft: &'a str,
35 words: &'a str,
36 identifier_bytes: NonZeroUsize,
37 words_bytes: NonZeroUsize,
38 ) -> Result<Self, HeldResultInvalid> {
39 if draft.is_empty()
40 || draft.len() > identifier_bytes.get()
41 || draft.chars().any(char::is_control)
42 || words.is_empty()
43 || words.len() > words_bytes.get()
44 {
45 return Err(HeldResultInvalid);
46 }
47 Ok(Self { draft, words })
48 }
49
50 #[must_use]
52 pub fn tool_result(&self) -> ToolResult {
53 ToolResult::ok(json!({"draft":self.draft, "words":self.words}))
54 }
55}
56
57#[must_use]
60pub fn text(result: &ToolResult) -> Option<String> {
61 if result.is_error {
62 return None;
63 }
64 let object = result.structured.as_object()?;
65 if object.len() != 2 {
66 return None;
67 }
68 let draft = object.get("draft")?.as_str()?;
69 let words = object.get("words")?.as_str()?;
70 if draft.is_empty() || draft.chars().any(char::is_control) || words.is_empty() {
71 return None;
72 }
73 Some(format!("held by draft {draft}: {words}"))
74}
75
76pub fn from_wire(
81 answer: &Value,
82 identifier_bytes: NonZeroUsize,
83 words_bytes: NonZeroUsize,
84) -> Result<Option<ToolResult>, HeldResultInvalid> {
85 let paired = answer.as_object().is_some_and(|object| {
86 object.len() == 2 && object.contains_key("draft") && object.contains_key("words")
87 });
88 if answer.get("state").and_then(Value::as_str) != Some("held") && !paired {
89 return Ok(None);
90 }
91 if answer.get("refusal").is_some()
92 || answer.get("error").is_some()
93 || answer
94 .get("isError")
95 .is_some_and(|value| value.as_bool() != Some(false))
96 || answer
97 .get("is_error")
98 .is_some_and(|value| value.as_bool() != Some(false))
99 {
100 return Err(HeldResultInvalid);
101 }
102 let draft = answer
103 .get("draft")
104 .and_then(Value::as_str)
105 .ok_or(HeldResultInvalid)?;
106 let words = answer
107 .get("words")
108 .and_then(Value::as_str)
109 .ok_or(HeldResultInvalid)?;
110 Ok(Some(
111 HeldWords::new(draft, words, identifier_bytes, words_bytes)?.tool_result(),
112 ))
113}
114
115impl std::fmt::Debug for ToolResult {
116 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 formatter
118 .debug_struct("ToolResult")
119 .field("is_error", &self.is_error)
120 .finish_non_exhaustive()
121 }
122}
123
124#[cfg(test)]
125#[path = "held_tests.rs"]
126mod tests;