1use serde::Deserialize;
10use serde_json::Value;
11
12use crate::error::Error;
13
14pub(crate) const LINE_LIMIT: usize = 8 * 1024 * 1024;
17
18pub(crate) const REASON_LIMIT: usize = 64 * 1024;
20
21macro_rules! words {
24 ($(#[$m:meta])* $name:ident { $($(#[$vm:meta])* $var:ident = $word:literal,)* }) => {
25 $(#[$m])*
26 #[derive(Debug, Clone, PartialEq, Eq)]
27 #[non_exhaustive]
28 pub enum $name {
29 $($(#[$vm])* $var,)*
30 Other(String),
32 }
33
34 impl $name {
35 pub fn as_str(&self) -> &str {
37 match self {
38 $(Self::$var => $word,)*
39 Self::Other(word) => word,
40 }
41 }
42
43 #[allow(dead_code)]
44 pub(crate) fn from_wire(word: &str) -> Self {
45 match word {
46 $($word => Self::$var,)*
47 other => Self::Other(other.to_string()),
48 }
49 }
50 }
51
52 impl<'de> Deserialize<'de> for $name {
53 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
54 Ok(Self::from_wire(&String::deserialize(d)?))
55 }
56 }
57 };
58}
59
60words! {
61 UserKind {
63 Message = "message",
65 Steer = "steer",
67 }
68}
69
70words! {
71 ToolStatus {
73 Ok = "ok",
75 Failed = "failed",
77 }
78}
79
80words! {
81 ApprovalDecision {
83 AllowedOnce = "allowed_once",
85 AllowedForTurn = "allowed_for_turn",
87 AllowedForSession = "allowed_for_session",
89 Denied = "denied",
91 DeniedAndStopped = "denied_and_stopped",
93 TimedOut = "timed_out",
95 Refused = "refused",
97 }
98}
99
100words! {
101 ApprovalKind {
103 Gate = "gate",
105 Protected = "protected",
107 Removal = "removal",
109 DecodeAndRun = "decode_and_run",
111 Credential = "credential",
113 Escape = "escape",
115 Mcp = "mcp",
117 JudgeFlag = "judge_flag",
119 JudgeUnavailable = "judge_unavailable",
121 ClientTool = "client_tool",
123 }
124}
125
126words! {
127 Choice {
129 AllowOnce = "allow_once",
131 AllowForTurn = "allow_for_turn",
133 Deny = "deny",
135 DenyAndStop = "deny_and_stop",
137 }
138}
139
140words! {
141 Outcome {
143 Completed = "completed",
145 ToolFailed = "tool_failed",
147 ModelFailed = "model_failed",
149 MaxIterations = "max_iterations",
151 Interrupted = "interrupted",
153 Truncated = "truncated",
155 PromptBlocked = "prompt_blocked",
157 }
158}
159
160words! {
161 Disposition {
163 Started = "started",
165 Queued = "queued",
167 Accepted = "accepted",
169 Dequeued = "dequeued",
171 Idle = "idle",
173 Stale = "stale",
175 Interrupting = "interrupting",
177 Answered = "answered",
179 Done = "done",
181 Set = "set",
183 Quitting = "quitting",
185 }
186}
187
188#[derive(Debug, Clone, PartialEq)]
190#[non_exhaustive]
191pub enum Event {
192 Init(Info),
194 User(UserMessage),
196 Reasoning(Text),
198 Assistant(Text),
200 ToolUse(ToolUse),
202 ToolResult(ToolResult),
204 Approval(Approval),
206 ApprovalRequest(ApprovalRequest),
208 Compacted(Compacted),
210 Result(TurnResult),
212}
213
214#[derive(Debug, Clone, PartialEq, Deserialize)]
216#[non_exhaustive]
217pub struct Info {
218 pub session_id: String,
220 pub model: Option<String>,
222 pub mode: Option<String>,
224 pub cwd: Option<String>,
226 pub harness_version: String,
228 #[serde(default)]
230 pub commands: Vec<String>,
231 #[serde(default)]
233 pub resumed: bool,
234 #[serde(default)]
236 pub client_tools: Vec<String>,
237}
238
239#[derive(Debug, Clone, PartialEq, Deserialize)]
241#[non_exhaustive]
242pub struct UserMessage {
243 pub text: String,
245 pub kind: UserKind,
247}
248
249#[derive(Debug, Clone, PartialEq, Deserialize)]
251#[non_exhaustive]
252pub struct Text {
253 pub text: String,
255}
256
257#[derive(Debug, Clone, PartialEq, Deserialize)]
259#[non_exhaustive]
260pub struct ToolUse {
261 pub id: String,
263 pub tool: String,
265 pub input: Value,
268}
269
270#[derive(Debug, Clone, PartialEq, Deserialize)]
272#[non_exhaustive]
273pub struct ToolResult {
274 pub id: String,
276 pub tool: String,
278 pub status: ToolStatus,
280 pub output: Option<Value>,
282 pub error: Option<String>,
284}
285
286#[derive(Debug, Clone, PartialEq, Deserialize)]
288#[non_exhaustive]
289pub struct Approval {
290 pub id: String,
292 pub tool: String,
294 pub decision: ApprovalDecision,
296 pub reason: Option<String>,
298 pub session: Option<String>,
300 pub agent: Option<String>,
302}
303
304#[derive(Debug, Clone, PartialEq, Deserialize)]
306#[non_exhaustive]
307pub struct ApprovalRequest {
308 pub id: String,
310 pub tool_use_id: String,
312 pub tool: String,
314 pub kind: ApprovalKind,
316 pub reason: Option<String>,
318 pub escape: Option<Escape>,
320 pub summary: String,
322 pub input: Value,
325 pub choices: Vec<Choice>,
327 pub turn_grant_skips_kind: bool,
329 pub session: Option<String>,
331 pub agent: Option<String>,
333 #[serde(skip)]
335 pub(crate) responder: Responder,
336}
337
338#[derive(Clone, Default)]
342pub(crate) struct Responder(pub(crate) Option<std::sync::Weak<crate::client::Shared>>);
343
344impl std::fmt::Debug for Responder {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.write_str("Responder")
347 }
348}
349
350impl PartialEq for Responder {
353 fn eq(&self, _: &Self) -> bool {
354 true
355 }
356}
357
358#[derive(Debug, Clone, PartialEq, Deserialize)]
360#[non_exhaustive]
361pub struct Escape {
362 pub kind: String,
364 pub reason: String,
366}
367
368#[derive(Debug, Clone, PartialEq, Deserialize)]
370#[non_exhaustive]
371pub struct Compacted {
372 pub trigger: Option<String>,
374 pub pre_tokens: Option<u64>,
376}
377
378#[derive(Debug, Clone, PartialEq, Deserialize)]
380#[non_exhaustive]
381pub struct TurnResult {
382 pub session_id: String,
384 pub outcome: Option<Outcome>,
386 pub is_error: bool,
388 pub result: Option<String>,
390 pub error: Option<String>,
392 pub model: Option<String>,
394 pub duration_ms: u64,
396 pub model_calls: u64,
398 pub usage: Usage,
400 pub denied: Vec<Denied>,
402 pub turn_id: Option<String>,
404 pub prompt_id: Option<String>,
406 pub cost: Option<Cost>,
408}
409
410#[derive(Debug, Clone, PartialEq, Deserialize)]
412#[non_exhaustive]
413pub struct Usage {
414 pub input_tokens: u64,
416 pub output_tokens: u64,
418 pub cached_input_tokens: Option<u64>,
420 pub reasoning_tokens: u64,
422}
423
424#[derive(Debug, Clone, PartialEq, Deserialize)]
426#[non_exhaustive]
427pub struct Denied {
428 pub tool: String,
430 pub summary: String,
432}
433
434#[derive(Debug, Clone, PartialEq, Deserialize)]
436#[non_exhaustive]
437pub struct Cost {
438 pub usd: Option<f64>,
440 pub priced: u64,
442 pub unpriced: u64,
444 pub free: u64,
446 pub bound: Option<String>,
448 pub basis: Option<String>,
450}
451
452#[derive(Debug, Clone, PartialEq, Deserialize)]
454#[non_exhaustive]
455pub struct State {
456 pub session_id: String,
458 pub model: Option<String>,
460 pub thinking: Option<String>,
462 pub mode: Option<String>,
464 pub away: bool,
466 pub turn: TurnState,
468 pub open_requests: Vec<String>,
470 pub queued: u64,
472 pub context: ContextState,
474}
475
476#[derive(Debug, Clone, PartialEq)]
478#[non_exhaustive]
479pub enum TurnState {
480 Idle,
482 Running {
484 id: Option<String>,
486 prompt_id: Option<String>,
488 },
489}
490
491impl<'de> Deserialize<'de> for TurnState {
492 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
493 #[derive(Deserialize)]
494 struct Running {
495 id: Option<String>,
496 prompt_id: Option<String>,
497 }
498 #[derive(Deserialize)]
499 #[serde(untagged)]
500 enum Raw {
501 Word(String),
502 Running(Running),
503 }
504 match Raw::deserialize(d)? {
505 Raw::Word(w) if w == "idle" => Ok(TurnState::Idle),
506 Raw::Word(w) => Err(serde::de::Error::custom(format!(
507 "unknown turn state {w:?}"
508 ))),
509 Raw::Running(r) => Ok(TurnState::Running {
510 id: r.id,
511 prompt_id: r.prompt_id,
512 }),
513 }
514 }
515}
516
517#[derive(Debug, Clone, PartialEq, Deserialize)]
519#[non_exhaustive]
520pub struct ContextState {
521 pub last_prompt_tokens: Option<u64>,
523 pub window: Option<u64>,
525 pub percent: Option<u64>,
527 pub auto_compact_at_percent: Option<u64>,
529}
530
531#[derive(Debug, Clone, PartialEq, Eq)]
533pub enum Decision {
534 AllowOnce,
536 AllowForTurn,
538 Deny {
540 reason: Option<String>,
542 },
543 DenyAndStop,
545}
546
547impl Decision {
548 pub(crate) fn as_wire(&self) -> &'static str {
550 match self {
551 Decision::AllowOnce => "allow_once",
552 Decision::AllowForTurn => "allow_for_turn",
553 Decision::Deny { .. } => "deny",
554 Decision::DenyAndStop => "deny_and_stop",
555 }
556 }
557}
558
559#[derive(Debug, Clone)]
561pub(crate) struct Response {
562 pub id: Option<String>,
563 pub command: String,
565 pub ok: bool,
566 pub disposition: Option<String>,
567 pub data: Option<Value>,
568 pub error: Option<(String, String)>,
569}
570
571#[derive(Debug)]
573pub(crate) enum Line {
574 Init(Box<Info>, u64),
575 Event(Box<Event>),
576 Response(Response),
577 ClientToolCall(ToolCall),
579 ClientToolCancel(String),
581 Other,
583}
584
585#[derive(Debug, Deserialize)]
587pub(crate) struct ToolCall {
588 pub id: String,
589 pub name: String,
590 pub input: Value,
591 #[serde(skip)]
594 pub too_deep: bool,
595}
596
597struct WithoutInput(serde_json::Map<String, Value>);
601
602impl<'de> Deserialize<'de> for WithoutInput {
603 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
604 struct Visit;
605 impl<'de> serde::de::Visitor<'de> for Visit {
606 type Value = WithoutInput;
607 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 f.write_str("a JSON object")
609 }
610 fn visit_map<A: serde::de::MapAccess<'de>>(
611 self,
612 mut map: A,
613 ) -> Result<WithoutInput, A::Error> {
614 let mut out = serde_json::Map::new();
615 while let Some(key) = map.next_key::<String>()? {
616 let value = if key == "input" {
617 map.next_value::<serde::de::IgnoredAny>()?;
618 Value::Null
619 } else {
620 map.next_value::<Value>()?
621 };
622 out.insert(key, value);
623 }
624 Ok(WithoutInput(out))
625 }
626 }
627 d.deserialize_map(Visit)
628 }
629}
630
631fn field<T: serde::de::DeserializeOwned>(ty: &str, v: Value) -> Result<T, Error> {
632 serde_json::from_value(v).map_err(|e| Error::Protocol(format!("a `{ty}` line: {e}")))
633}
634
635pub(crate) fn parse(line: &[u8]) -> Result<Line, Error> {
642 let not_json = |e: serde_json::Error| {
643 Error::Protocol(format!(
644 "a line that is not JSON ({e}): {}",
645 String::from_utf8_lossy(&line[..line.len().min(200)])
646 ))
647 };
648 let (value, too_deep) = match serde_json::from_slice::<Value>(line) {
649 Ok(value) => (value, false),
650 Err(e) if e.to_string().starts_with("recursion limit exceeded") => {
651 let WithoutInput(map) = serde_json::from_slice(line).map_err(not_json)?;
652 (Value::Object(map), true)
653 }
654 Err(e) => return Err(not_json(e)),
655 };
656 let Value::Object(map) = value else {
657 return Err(Error::Protocol("a line that is not a JSON object".into()));
658 };
659 let Some(ty) = map.get("type").and_then(Value::as_str).map(str::to_string) else {
660 return Err(Error::Protocol("a line with no string `type`".into()));
661 };
662 let v = Value::Object(map);
663 Ok(match ty.as_str() {
664 "init" => {
665 let version = v
666 .get("v")
667 .and_then(Value::as_u64)
668 .ok_or_else(|| Error::Protocol("an `init` line with no `v`".into()))?;
669 Line::Init(Box::new(field(&ty, v)?), version)
670 }
671 "user" => Line::Event(Box::new(Event::User(field(&ty, v)?))),
672 "reasoning" => Line::Event(Box::new(Event::Reasoning(field(&ty, v)?))),
673 "assistant" => Line::Event(Box::new(Event::Assistant(field(&ty, v)?))),
674 "tool_use" => Line::Event(Box::new(Event::ToolUse(field(&ty, v)?))),
675 "tool_result" => Line::Event(Box::new(Event::ToolResult(field(&ty, v)?))),
676 "approval" => Line::Event(Box::new(Event::Approval(field(&ty, v)?))),
677 "approval_request" => Line::Event(Box::new(Event::ApprovalRequest(field(&ty, v)?))),
678 "compacted" => Line::Event(Box::new(Event::Compacted(field(&ty, v)?))),
679 "result" => Line::Event(Box::new(Event::Result(field(&ty, v)?))),
680 "client_tool_call" => {
681 let mut call: ToolCall = field(&ty, v)?;
682 call.too_deep = too_deep;
683 Line::ClientToolCall(call)
684 }
685 "client_tool_cancel" => {
686 #[derive(Deserialize)]
687 struct Cancel {
688 id: String,
689 }
690 Line::ClientToolCancel(field::<Cancel>(&ty, v)?.id)
691 }
692 "response" => {
693 #[derive(Deserialize)]
694 struct ErrorBody {
695 code: String,
696 message: String,
697 }
698 #[derive(Deserialize)]
699 struct Raw {
700 #[serde(default)]
701 id: Value,
702 command: String,
703 ok: bool,
704 disposition: Option<String>,
705 data: Option<Value>,
706 error: Option<ErrorBody>,
707 }
708 let raw: Raw = field(&ty, v)?;
709 Line::Response(Response {
710 id: raw.id.as_str().map(str::to_string),
711 command: raw.command,
712 ok: raw.ok,
713 disposition: raw.disposition,
714 data: raw.data,
715 error: raw.error.map(|e| (e.code, e.message)),
716 })
717 }
718 _ => Line::Other,
719 })
720}
721
722pub(crate) fn encode(command: &Value) -> Result<String, Error> {
725 let mut line = serde_json::to_string(command).map_err(|e| Error::Protocol(e.to_string()))?;
726 if line.len() + 1 > LINE_LIMIT {
727 return Err(Error::TooLarge);
728 }
729 line.push('\n');
730 Ok(line)
731}
732
733pub(crate) fn cut(text: &str, limit: usize) -> &str {
735 if text.len() <= limit {
736 return text;
737 }
738 let mut end = limit;
739 while !text.is_char_boundary(end) {
740 end -= 1;
741 }
742 &text[..end]
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748 use serde_json::json;
749
750 #[test]
751 fn an_unknown_type_and_field_are_tolerated() {
752 let line = parse(br#"{"type":"assistant","text":"hi","new":1}"#).unwrap();
753 assert!(
754 matches!(line, Line::Event(e) if matches!(&*e, Event::Assistant(t) if t.text == "hi"))
755 );
756 let line = parse(br#"{"type":"brand_new","x":1}"#).unwrap();
757 assert!(matches!(line, Line::Other));
758 }
759
760 #[test]
761 fn a_missing_required_field_or_not_json_is_a_protocol_error() {
762 assert!(matches!(
763 parse(br#"{"type":"tool_use","id":"a","tool":"t"}"#),
764 Err(Error::Protocol(_))
765 ));
766 assert!(matches!(parse(b"not json"), Err(Error::Protocol(_))));
767 assert!(matches!(parse(b"[1]"), Err(Error::Protocol(_))));
768 }
769
770 #[test]
771 fn a_line_at_the_limit_fits_and_one_byte_over_does_not() {
772 let fits = "a".repeat(LINE_LIMIT - 9);
774 assert!(encode(&json!({ "t": fits })).is_ok());
775 let over = "a".repeat(LINE_LIMIT - 8);
776 assert_eq!(encode(&json!({ "t": over })), Err(Error::TooLarge));
777 }
778
779 #[test]
780 fn the_client_tool_lines_parse_and_a_missing_field_is_a_protocol_error() {
781 let line = parse(
782 br#"{"type":"client_tool_call","id":"c","name":"pay","input":{"n":1},"turn_id":"t"}"#,
783 )
784 .unwrap();
785 assert!(
786 matches!(line, Line::ClientToolCall(c) if c.id == "c" && c.name == "pay" && c.input == json!({"n": 1}))
787 );
788 let line =
789 parse(br#"{"type":"client_tool_cancel","id":"c","reason":"timed_out"}"#).unwrap();
790 assert!(matches!(line, Line::ClientToolCancel(id) if id == "c"));
791 assert!(matches!(
792 parse(br#"{"type":"client_tool_call","id":"c","input":{}}"#),
793 Err(Error::Protocol(_))
794 ));
795 }
796
797 #[test]
800 fn lines_with_input_nested_127_deep_are_read_without_it() {
801 let input = format!("{}1{}", "[".repeat(127), "]".repeat(127));
802 let call = format!(
803 r#"{{"type":"client_tool_call","id":"c","name":"pay","input":{input},"turn_id":"t"}}"#
804 );
805 match parse(call.as_bytes()).unwrap() {
806 Line::ClientToolCall(c) => {
807 assert!(c.too_deep && c.input.is_null() && c.id == "c" && c.name == "pay");
808 }
809 other => panic!("{other:?}"),
810 }
811 let tool_use = format!(r#"{{"type":"tool_use","id":"u","tool":"pay","input":{input}}}"#);
812 assert!(matches!(
813 parse(tool_use.as_bytes()).unwrap(),
814 Line::Event(e) if matches!(&*e, Event::ToolUse(u) if u.input.is_null() && u.id == "u")
815 ));
816 let request = format!(
817 r#"{{"type":"approval_request","id":"r","tool_use_id":"u","tool":"pay","kind":"client_tool","summary":"pay","input":{input},"choices":["deny"],"turn_grant_skips_kind":false}}"#
818 );
819 assert!(matches!(
820 parse(request.as_bytes()).unwrap(),
821 Line::Event(e) if matches!(&*e, Event::ApprovalRequest(r) if r.input.is_null() && r.id == "r")
822 ));
823 let shallow = format!(
825 r#"{{"type":"client_tool_call","id":"c","name":"pay","input":{}1{}}}"#,
826 "[".repeat(100),
827 "]".repeat(100)
828 );
829 assert!(
830 matches!(parse(shallow.as_bytes()).unwrap(), Line::ClientToolCall(c) if !c.too_deep)
831 );
832 }
833
834 #[test]
835 fn cut_keeps_whole_characters() {
836 assert_eq!(cut("aé", 2), "a");
837 assert_eq!(cut("abc", 5), "abc");
838 }
839
840 #[test]
841 fn get_state_parses() {
842 let s: State = serde_json::from_value(json!({"away":false,"context":{"auto_compact_at_percent":80,"last_prompt_tokens":null,"percent":null,"window":null},"mode":"manual","model":"m","open_requests":[],"queued":0,"session_id":"s","thinking":"medium","turn":{"id":null,"prompt_id":"p"}})).unwrap();
843 assert_eq!(
844 s.turn,
845 TurnState::Running {
846 id: None,
847 prompt_id: Some("p".into())
848 }
849 );
850 }
851}
852
853#[cfg(test)]
854mod word_tests {
855 use super::*;
856 use serde_json::json;
857
858 #[test]
859 fn a_known_word_is_typed_and_an_unknown_one_kept() {
860 let r: ApprovalRequest = serde_json::from_value(json!({
861 "id": "r", "tool_use_id": "t", "tool": "pay", "kind": "client_tool",
862 "summary": "pay", "input": {}, "choices": ["allow_once", "brand_new"],
863 "turn_grant_skips_kind": false,
864 }))
865 .unwrap();
866 assert_eq!(r.kind, ApprovalKind::ClientTool);
867 assert_eq!(
868 r.choices,
869 vec![Choice::AllowOnce, Choice::Other("brand_new".into())]
870 );
871 assert_eq!(Choice::Other("x".into()).as_str(), "x");
872 assert_eq!(Outcome::from_wire("completed"), Outcome::Completed);
873 }
874}
875
876#[cfg(test)]
877mod conformance_tests {
878 use super::*;
879
880 #[test]
887 fn every_outbound_transcript_line_parses_into_its_type() {
888 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../conformance");
889 let mut files = 0;
890 let mut types = std::collections::BTreeSet::new();
891 for entry in std::fs::read_dir(&dir).unwrap().flatten() {
892 let path = entry.path();
893 if path.extension().is_none_or(|e| e != "jsonl") {
894 continue;
895 }
896 files += 1;
897 let name = path.file_stem().unwrap().to_string_lossy().into_owned();
898 for line in std::fs::read_to_string(&path).unwrap().lines() {
899 let entry: Value = serde_json::from_str(line).unwrap();
900 if entry["dir"] != "out" {
901 continue;
902 }
903 assert!(entry.get("invalid").is_none(), "{name}: {entry}");
904 let ty = entry["line"]["type"].as_str().unwrap().to_string();
905 let text = entry["line"].to_string();
906 let got = parse(text.as_bytes()).unwrap_or_else(|e| panic!("{name}: {text}: {e}"));
907 let ok = match (&got, ty.as_str()) {
908 (Line::Init(..), "init")
909 | (Line::Response(_), "response")
910 | (Line::ClientToolCall(_), "client_tool_call")
911 | (Line::ClientToolCancel(_), "client_tool_cancel")
912 | (Line::Other, "text_delta") => true,
913 (Line::Event(e), ty) => matches!(
914 (&**e, ty),
915 (Event::User(_), "user")
916 | (Event::Reasoning(_), "reasoning")
917 | (Event::Assistant(_), "assistant")
918 | (Event::ToolUse(_), "tool_use")
919 | (Event::ToolResult(_), "tool_result")
920 | (Event::Approval(_), "approval")
921 | (Event::ApprovalRequest(_), "approval_request")
922 | (Event::Compacted(_), "compacted")
923 | (Event::Result(_), "result")
924 ),
925 _ => false,
926 };
927 assert!(ok, "{name}: `{ty}` was read as {got:?}");
928 types.insert(ty);
929 }
930 }
931 let scripts = std::fs::read_dir(dir.join("scripts")).unwrap().count();
932 assert_eq!(files, scripts, "a script without a transcript");
933 let seen: Vec<&str> = types.iter().map(String::as_str).collect();
936 assert_eq!(
937 seen,
938 [
939 "approval",
940 "approval_request",
941 "assistant",
942 "client_tool_call",
943 "client_tool_cancel",
944 "compacted",
945 "init",
946 "reasoning",
947 "response",
948 "result",
949 "text_delta",
950 "tool_result",
951 "tool_use",
952 "user"
953 ]
954 );
955 }
956}