Skip to main content

megacode_agent_sdk/
wire.rs

1//! The conversation's lines (spec slice-ar R8.5, `docs/headless.md` "A
2//! conversation", `docs/protocol/stream-json.schema.json`): the typed events
3//! a program reads, and the commands the SDK writes.
4//!
5//! Reading is lenient (R6.3): an unknown line type is skipped and an unknown
6//! field ignored; a known type missing a field it requires is a protocol
7//! error.
8
9use serde::Deserialize;
10use serde_json::Value;
11
12use crate::error::Error;
13
14/// harness's limit on one command line, newline included
15/// (`cli/rpc.rs`'s `LINE_CAP`).
16pub(crate) const LINE_LIMIT: usize = 8 * 1024 * 1024;
17
18/// The most bytes an approval's reason carries (R8.5.2).
19pub(crate) const REASON_LIMIT: usize = 64 * 1024;
20
21/// A closed list of wire words, read leniently: a word this SDK does not
22/// know is `Other` (R6.3).
23macro_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            /// A word this SDK does not know.
31            Other(String),
32        }
33
34        impl $name {
35            /// The wire's word.
36            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    /// `user.kind`.
62    UserKind {
63        /// A prompt.
64        Message = "message",
65        /// A steer that landed.
66        Steer = "steer",
67    }
68}
69
70words! {
71    /// `tool_result.status`.
72    ToolStatus {
73        /// The call succeeded.
74        Ok = "ok",
75        /// The call failed.
76        Failed = "failed",
77    }
78}
79
80words! {
81    /// `approval.decision`: what a gate decided.
82    ApprovalDecision {
83        /// Allowed once.
84        AllowedOnce = "allowed_once",
85        /// Allowed for the turn.
86        AllowedForTurn = "allowed_for_turn",
87        /// Allowed for the session (old logs only).
88        AllowedForSession = "allowed_for_session",
89        /// Denied.
90        Denied = "denied",
91        /// Denied, and the turn stopped.
92        DeniedAndStopped = "denied_and_stopped",
93        /// Nobody answered in time.
94        TimedOut = "timed_out",
95        /// Refused while away.
96        Refused = "refused",
97    }
98}
99
100words! {
101    /// `approval_request.kind`: why a gate asks.
102    ApprovalKind {
103        /// The mode's gate.
104        Gate = "gate",
105        /// A protected path.
106        Protected = "protected",
107        /// A removal.
108        Removal = "removal",
109        /// Decoded text run as a command.
110        DecodeAndRun = "decode_and_run",
111        /// A credential.
112        Credential = "credential",
113        /// An escape from the sandbox.
114        Escape = "escape",
115        /// An MCP call.
116        Mcp = "mcp",
117        /// The judge flagged it.
118        JudgeFlag = "judge_flag",
119        /// The judge could not decide.
120        JudgeUnavailable = "judge_unavailable",
121        /// A client tool marked `ask`.
122        ClientTool = "client_tool",
123    }
124}
125
126words! {
127    /// One of an `approval_request`'s `choices`.
128    Choice {
129        /// `allow_once`
130        AllowOnce = "allow_once",
131        /// `allow_for_turn`
132        AllowForTurn = "allow_for_turn",
133        /// `deny`
134        Deny = "deny",
135        /// `deny_and_stop`
136        DenyAndStop = "deny_and_stop",
137    }
138}
139
140words! {
141    /// `result.outcome`: how a turn ended.
142    Outcome {
143        /// The model answered.
144        Completed = "completed",
145        /// A tool call failed.
146        ToolFailed = "tool_failed",
147        /// The model call failed.
148        ModelFailed = "model_failed",
149        /// The turn hit its iteration limit.
150        MaxIterations = "max_iterations",
151        /// The turn was interrupted.
152        Interrupted = "interrupted",
153        /// The reply was cut by its output cap.
154        Truncated = "truncated",
155        /// The provider blocked the prompt.
156        PromptBlocked = "prompt_blocked",
157    }
158}
159
160words! {
161    /// A command's outcome, from its `response` line.
162    Disposition {
163        /// `started`
164        Started = "started",
165        /// `queued`
166        Queued = "queued",
167        /// `accepted`
168        Accepted = "accepted",
169        /// `dequeued`
170        Dequeued = "dequeued",
171        /// `idle`
172        Idle = "idle",
173        /// `stale`
174        Stale = "stale",
175        /// `interrupting`
176        Interrupting = "interrupting",
177        /// `answered`
178        Answered = "answered",
179        /// `done`
180        Done = "done",
181        /// `set`
182        Set = "set",
183        /// `quitting`
184        Quitting = "quitting",
185    }
186}
187
188/// One event of a session, one per outbound line type (R8.5.1).
189#[derive(Debug, Clone, PartialEq)]
190#[non_exhaustive]
191pub enum Event {
192    /// The session started (yielded by `query` only).
193    Init(Info),
194    /// A prompt, or a steer that landed.
195    User(UserMessage),
196    /// The model's reasoning.
197    Reasoning(Text),
198    /// The model's visible text.
199    Assistant(Text),
200    /// A tool call.
201    ToolUse(ToolUse),
202    /// A tool call's outcome.
203    ToolResult(ToolResult),
204    /// A gate's decision.
205    Approval(Approval),
206    /// A gate waiting for an answer.
207    ApprovalRequest(ApprovalRequest),
208    /// The context was compacted.
209    Compacted(Compacted),
210    /// The turn ended.
211    Result(TurnResult),
212}
213
214/// `init`: the session's facts.
215#[derive(Debug, Clone, PartialEq, Deserialize)]
216#[non_exhaustive]
217pub struct Info {
218    /// The session's id.
219    pub session_id: String,
220    /// The model the session runs.
221    pub model: Option<String>,
222    /// The session's mode.
223    pub mode: Option<String>,
224    /// The session's working folder, where harness reports one.
225    pub cwd: Option<String>,
226    /// The harness version.
227    pub harness_version: String,
228    /// The commands this harness takes.
229    #[serde(default)]
230    pub commands: Vec<String>,
231    /// Whether this is a resumed session.
232    #[serde(default)]
233    pub resumed: bool,
234    /// The client tools' names, in order.
235    #[serde(default)]
236    pub client_tools: Vec<String>,
237}
238
239/// `user`: a message that joined the conversation.
240#[derive(Debug, Clone, PartialEq, Deserialize)]
241#[non_exhaustive]
242pub struct UserMessage {
243    /// The text.
244    pub text: String,
245    /// A prompt, or a steer that landed.
246    pub kind: UserKind,
247}
248
249/// `reasoning` and `assistant`: text.
250#[derive(Debug, Clone, PartialEq, Deserialize)]
251#[non_exhaustive]
252pub struct Text {
253    /// The text.
254    pub text: String,
255}
256
257/// `tool_use`.
258#[derive(Debug, Clone, PartialEq, Deserialize)]
259#[non_exhaustive]
260pub struct ToolUse {
261    /// The call's id.
262    pub id: String,
263    /// The tool's name.
264    pub tool: String,
265    /// The call's arguments; `null` when they were nested too deeply to
266    /// read.
267    pub input: Value,
268}
269
270/// `tool_result`.
271#[derive(Debug, Clone, PartialEq, Deserialize)]
272#[non_exhaustive]
273pub struct ToolResult {
274    /// The call's id.
275    pub id: String,
276    /// The tool's name.
277    pub tool: String,
278    /// Whether the call succeeded.
279    pub status: ToolStatus,
280    /// The output, when the call succeeded.
281    pub output: Option<Value>,
282    /// The error, when it failed.
283    pub error: Option<String>,
284}
285
286/// `approval`: a gate's decision.
287#[derive(Debug, Clone, PartialEq, Deserialize)]
288#[non_exhaustive]
289pub struct Approval {
290    /// The call's id.
291    pub id: String,
292    /// The tool's name.
293    pub tool: String,
294    /// What the gate decided.
295    pub decision: ApprovalDecision,
296    /// The reason given, if any.
297    pub reason: Option<String>,
298    /// A sub-agent's session id.
299    pub session: Option<String>,
300    /// A sub-agent's name.
301    pub agent: Option<String>,
302}
303
304/// `approval_request`: a gate waiting for an answer.
305#[derive(Debug, Clone, PartialEq, Deserialize)]
306#[non_exhaustive]
307pub struct ApprovalRequest {
308    /// The id an answer names.
309    pub id: String,
310    /// The call's `tool_use` id (empty inside a `script`).
311    pub tool_use_id: String,
312    /// The tool's name.
313    pub tool: String,
314    /// Why it asks.
315    pub kind: ApprovalKind,
316    /// The gate's reason, when it gives one.
317    pub reason: Option<String>,
318    /// The escape asked for, when the call asks for one.
319    pub escape: Option<Escape>,
320    /// A one-line summary of the call.
321    pub summary: String,
322    /// The call's arguments; `null` when they were nested too deeply to
323    /// read (`summary` still describes the call).
324    pub input: Value,
325    /// The decisions on offer.
326    pub choices: Vec<Choice>,
327    /// Whether `allow_for_turn` leaves calls of this kind out.
328    pub turn_grant_skips_kind: bool,
329    /// A sub-agent's session id.
330    pub session: Option<String>,
331    /// A sub-agent's name.
332    pub agent: Option<String>,
333    /// The session that answers it.
334    #[serde(skip)]
335    pub(crate) responder: Responder,
336}
337
338/// The session an `approval_request` came from, so the event can answer
339/// itself (R8.3.3). Weak: an event kept after its session ended holds
340/// nothing alive.
341#[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
350/// Two requests are equal by their fields; where they came from is not
351/// one.
352impl PartialEq for Responder {
353    fn eq(&self, _: &Self) -> bool {
354        true
355    }
356}
357
358/// An `approval_request`'s escape.
359#[derive(Debug, Clone, PartialEq, Deserialize)]
360#[non_exhaustive]
361pub struct Escape {
362    /// `network` or `outside`.
363    pub kind: String,
364    /// The model's reason.
365    pub reason: String,
366}
367
368/// `compacted`.
369#[derive(Debug, Clone, PartialEq, Deserialize)]
370#[non_exhaustive]
371pub struct Compacted {
372    /// `automatic` or `manual`.
373    pub trigger: Option<String>,
374    /// The prompt's size before it, in tokens.
375    pub pre_tokens: Option<u64>,
376}
377
378/// `result`: the end of a turn.
379#[derive(Debug, Clone, PartialEq, Deserialize)]
380#[non_exhaustive]
381pub struct TurnResult {
382    /// The session's id.
383    pub session_id: String,
384    /// How the turn ended.
385    pub outcome: Option<Outcome>,
386    /// Whether the turn failed to answer.
387    pub is_error: bool,
388    /// The answer.
389    pub result: Option<String>,
390    /// The error, when the turn failed.
391    pub error: Option<String>,
392    /// The model that answered.
393    pub model: Option<String>,
394    /// The turn's length.
395    pub duration_ms: u64,
396    /// Model calls the turn made.
397    pub model_calls: u64,
398    /// Tokens the turn used.
399    pub usage: Usage,
400    /// Calls the gates denied.
401    pub denied: Vec<Denied>,
402    /// The turn's id.
403    pub turn_id: Option<String>,
404    /// The id of the prompt that opened the turn.
405    pub prompt_id: Option<String>,
406    /// What the turn's model calls cost.
407    pub cost: Option<Cost>,
408}
409
410/// A turn's token counts.
411#[derive(Debug, Clone, PartialEq, Deserialize)]
412#[non_exhaustive]
413pub struct Usage {
414    /// Input tokens.
415    pub input_tokens: u64,
416    /// Output tokens.
417    pub output_tokens: u64,
418    /// Cached input tokens, where the provider reports them.
419    pub cached_input_tokens: Option<u64>,
420    /// Reasoning tokens.
421    pub reasoning_tokens: u64,
422}
423
424/// A denied call.
425#[derive(Debug, Clone, PartialEq, Deserialize)]
426#[non_exhaustive]
427pub struct Denied {
428    /// The tool's name.
429    pub tool: String,
430    /// The call's summary.
431    pub summary: String,
432}
433
434/// A turn's cost.
435#[derive(Debug, Clone, PartialEq, Deserialize)]
436#[non_exhaustive]
437pub struct Cost {
438    /// US dollars, when anything could be priced.
439    pub usd: Option<f64>,
440    /// Calls priced.
441    pub priced: u64,
442    /// Calls not priced.
443    pub unpriced: u64,
444    /// Free calls.
445    pub free: u64,
446    /// `upper` when the figure is an upper bound.
447    pub bound: Option<String>,
448    /// `reported`, `list_price` or `mixed`.
449    pub basis: Option<String>,
450}
451
452/// `get_state`'s data.
453#[derive(Debug, Clone, PartialEq, Deserialize)]
454#[non_exhaustive]
455pub struct State {
456    /// The session's id.
457    pub session_id: String,
458    /// The model.
459    pub model: Option<String>,
460    /// The thinking level.
461    pub thinking: Option<String>,
462    /// The mode.
463    pub mode: Option<String>,
464    /// Whether the running or next task is away.
465    pub away: bool,
466    /// The running turn, if any.
467    pub turn: TurnState,
468    /// The ids of the open approval requests.
469    pub open_requests: Vec<String>,
470    /// Prompts waiting for their turn.
471    pub queued: u64,
472    /// The context's size.
473    pub context: ContextState,
474}
475
476/// `get_state.turn`.
477#[derive(Debug, Clone, PartialEq)]
478#[non_exhaustive]
479pub enum TurnState {
480    /// No turn is running.
481    Idle,
482    /// A turn is running (its ids, once known).
483    Running {
484        /// The turn's id.
485        id: Option<String>,
486        /// The id of the prompt that opened it.
487        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/// `get_state.context`.
518#[derive(Debug, Clone, PartialEq, Deserialize)]
519#[non_exhaustive]
520pub struct ContextState {
521    /// The last turn reply's whole prompt, in tokens.
522    pub last_prompt_tokens: Option<u64>,
523    /// The model's window, in tokens.
524    pub window: Option<u64>,
525    /// The share of the window used, in percent.
526    pub percent: Option<u64>,
527    /// Where automatic compaction starts, in percent.
528    pub auto_compact_at_percent: Option<u64>,
529}
530
531/// An answer to an approval request (R8.3.1): the protocol's decisions.
532#[derive(Debug, Clone, PartialEq, Eq)]
533pub enum Decision {
534    /// Run this call.
535    AllowOnce,
536    /// Run this call and the turn's other calls of the tool.
537    AllowForTurn,
538    /// Do not run it; the reason reaches the model.
539    Deny {
540        /// What the model reads, as `The user said: …`.
541        reason: Option<String>,
542    },
543    /// Do not run it, and end the turn.
544    DenyAndStop,
545}
546
547impl Decision {
548    /// The wire's word for this decision.
549    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/// A `response` line.
560#[derive(Debug, Clone)]
561pub(crate) struct Response {
562    pub id: Option<String>,
563    /// The command answered, as harness echoes it.
564    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/// One line read from the session.
572#[derive(Debug)]
573pub(crate) enum Line {
574    Init(Box<Info>, u64),
575    Event(Box<Event>),
576    Response(Response),
577    /// `client_tool_call` (R3.3).
578    ClientToolCall(ToolCall),
579    /// `client_tool_cancel` (R3.3.5): the call's id.
580    ClientToolCancel(String),
581    /// A line of a type this SDK does not read (R6.3).
582    Other,
583}
584
585/// A `client_tool_call` line.
586#[derive(Debug, Deserialize)]
587pub(crate) struct ToolCall {
588    pub id: String,
589    pub name: String,
590    pub input: Value,
591    /// The arguments were nested past what this SDK parses; `input` is
592    /// null and the call is answered `is_error`.
593    #[serde(skip)]
594    pub too_deep: bool,
595}
596
597/// A line's top-level object with its `input` skipped (as null): the
598/// reading of a line whose `input` is nested past serde_json's recursion
599/// limit. `IgnoredAny` skips a value without recursing.
600struct 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
635/// Parses one line (without its newline).
636///
637/// A model's arguments may nest as deep as harness accepts them, one level
638/// past serde_json's recursion limit once a line wraps them: such a line is
639/// read again with its `input` skipped, so `tool_use` and `approval_request`
640/// carry `input: null` and a `client_tool_call` is answered `is_error`.
641pub(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
722/// A command's line, measured against harness's limit (R8.5.2): the encoded
723/// line with its newline must fit, else `TooLarge`.
724pub(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
733/// `text` cut to at most `limit` bytes, on a character boundary.
734pub(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        // `{"t":"…"}` is 8 bytes around the text, plus the newline.
773        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    /// Review Focus 2: arguments at harness's deepest (127 levels) put the
798    /// lines that carry them one past serde_json's limit; each is still read.
799    #[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        // A shallower one keeps its input.
824        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    /// R7.3: the SDK reads every line harness wrote in every committed
881    /// transcript (`sdk/conformance`, kept equal to the binary's output by
882    /// `scripts/sdk/regen-transcripts.sh --check`) into the type its `type`
883    /// names. The transcripts are read, not replayed through a `Client`:
884    /// their `"invalid"` inbound lines (raw text, unknown commands) are
885    /// ones a `Client` cannot send. Inbound lines are not the SDK's to read.
886    #[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        // The cli crate's `every_outbound_line_type_is_an_event_or_consumed`
934        // holds this set equal to `LINE_TYPES`.
935        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}