Skip to main content

megacode_agent_sdk/
client.rs

1//! A session (spec slice-ar R8.1, R8.5, R8.6): one process, one reader task,
2//! one writer task, and the handle a program holds.
3
4use std::collections::{HashMap, HashSet, VecDeque};
5use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
6use std::sync::{Arc, Mutex};
7use std::time::Duration;
8
9use serde_json::{Value, json};
10use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
11use tokio::process::{ChildStdin, ChildStdout};
12use tokio::sync::{mpsc, oneshot, watch};
13
14use crate::error::Error;
15use crate::options::{ApprovalCallback, Options};
16use crate::process::{self, Exit, OptionsFile, Signal};
17use crate::tools::{self, CancelSignal, Tool, ToolContext};
18use crate::wire::{
19    self, ApprovalRequest, Compacted, Decision, Disposition, Event, Info, Line, REASON_LIMIT,
20    Responder, Response, State, ToolCall, UserKind,
21};
22
23/// How long a start waits for `init` (R6.2).
24const INIT_WAIT: Duration = Duration::from_secs(60);
25/// A turn's unread events past this end the session (R8.5.5).
26const UNREAD_LIMIT: usize = 10_000;
27/// How long `close` waits for the exit after `quit` (R8.6.2).
28const QUIT_WAIT: Duration = Duration::from_secs(5);
29/// How long `close` waits for handler and callback tasks before aborting
30/// them (R8.6.2).
31const TASK_WAIT: Duration = Duration::from_secs(5);
32
33/// What `compact` did (R8.1.2).
34#[derive(Debug, Clone, PartialEq)]
35pub enum Compaction {
36    /// A turn is running; the compaction runs after it.
37    Queued,
38    /// The compaction ran: `Some` when one was written.
39    Done(Option<Compacted>),
40}
41
42/// A queue of events that never blocks its writer (R8.5.3).
43struct Queue {
44    unread: AtomicUsize,
45    overflowed: AtomicBool,
46}
47
48struct QueueTx {
49    tx: mpsc::UnboundedSender<Result<Event, Error>>,
50    shared: Arc<Queue>,
51}
52
53impl QueueTx {
54    /// Queues `event`; `false` once the reader left more than
55    /// [`UNREAD_LIMIT`] unread. A dropped reader discards it.
56    fn push(&self, event: Event) -> bool {
57        if self.tx.is_closed() {
58            return true;
59        }
60        let unread = self.shared.unread.fetch_add(1, Ordering::SeqCst) + 1;
61        let _ = self.tx.send(Ok(event));
62        if unread > UNREAD_LIMIT {
63            self.shared.overflowed.store(true, Ordering::SeqCst);
64            return false;
65        }
66        true
67    }
68
69    fn fail(&self, error: Error) {
70        let _ = self.tx.send(Err(error));
71    }
72}
73
74fn queue() -> (QueueTx, Events) {
75    let (tx, rx) = mpsc::unbounded_channel();
76    let shared = Arc::new(Queue {
77        unread: AtomicUsize::new(0),
78        overflowed: AtomicBool::new(false),
79    });
80    (
81        QueueTx {
82            tx,
83            shared: shared.clone(),
84        },
85        Events {
86            rx,
87            shared,
88            done: false,
89        },
90    )
91}
92
93/// A stream of events: a turn's, or the session's between turns.
94pub struct Events {
95    rx: mpsc::UnboundedReceiver<Result<Event, Error>>,
96    shared: Arc<Queue>,
97    done: bool,
98}
99
100impl Events {
101    /// The next event; `None` once the stream ended. An error ends it.
102    pub async fn next(&mut self) -> Option<Result<Event, Error>> {
103        if self.done {
104            return None;
105        }
106        if self.shared.overflowed.load(Ordering::SeqCst) {
107            self.done = true;
108            return Some(Err(Error::SlowConsumer));
109        }
110        match self.rx.recv().await {
111            Some(Ok(event)) => {
112                self.shared.unread.fetch_sub(1, Ordering::SeqCst);
113                Some(Ok(event))
114            }
115            Some(Err(e)) => {
116                self.done = true;
117                Some(Err(e))
118            }
119            None => {
120                self.done = true;
121                None
122            }
123        }
124    }
125}
126
127/// One turn's events, `user` to `result` (from `query`, `init` first).
128pub struct TurnStream {
129    init: Option<Info>,
130    events: Events,
131    /// `query`'s session, closed once its turn ends.
132    owner: Option<Client>,
133    /// The session the turn runs in, alive while its events are read.
134    _session: Option<Client>,
135    closing: Option<tokio::task::JoinHandle<()>>,
136}
137
138impl TurnStream {
139    /// The next event; `None` after the turn's `result`, once an abort took
140    /// its queued prompt back (`Disposition::Dequeued`: the prompt never
141    /// ran), and, from `query`, once the session was closed.
142    pub async fn next(&mut self) -> Option<Result<Event, Error>> {
143        if let Some(info) = self.init.take() {
144            return Some(Ok(Event::Init(info)));
145        }
146        if let Some(closing) = self.closing.take() {
147            let _ = closing.await;
148            return None;
149        }
150        let next = self.events.next().await;
151        let ended = matches!(next, None | Some(Ok(Event::Result(_))) | Some(Err(_)));
152        if ended && let Some(client) = self.owner.take() {
153            let closing = tokio::spawn(client.close());
154            if next.is_none() {
155                let _ = closing.await;
156            } else {
157                self.closing = Some(closing);
158            }
159        }
160        next
161    }
162}
163
164/// The turn the session is running for this program (R8.5.4).
165struct Turn {
166    prompt_id: String,
167    /// The prompt's `user` line arrived: the lines after it are the turn's.
168    /// Before it, whether the prompt was answered `started` or `queued`,
169    /// the lines are another turn's (one a background agent's result
170    /// opened) or between turns (a compaction), and the session's.
171    opened: bool,
172    /// The `id` of every `abort` that named its prompt, registered as the
173    /// abort is queued: any one answered `dequeued` took the prompt back,
174    /// so it never runs and the turn ends, whatever aborts follow it and
175    /// whether or not its caller still waits.
176    aborts: HashSet<String>,
177    events: QueueTx,
178}
179
180/// A waiting command's key: the command and its `id`, as its `response`
181/// echoes both, so an `approval` and a `client_tool_result` that share an
182/// id never answer each other.
183type Key = (String, String);
184
185#[derive(Default)]
186struct Session {
187    /// Each waiting command's reply.
188    pending: HashMap<Key, oneshot::Sender<Response>>,
189    turn: Option<Turn>,
190    /// `session_events()`'s sender, once taken.
191    loose: Option<QueueTx>,
192    /// `session_events()` was called.
193    loose_given: bool,
194    /// Why the session is over, once it is.
195    ended: Option<Error>,
196    /// Each running tool call's cancellation trigger, by call id; a call's
197    /// task removes its own.
198    calls: HashMap<String, watch::Sender<bool>>,
199}
200
201/// What the writer task takes.
202enum Write {
203    Line(String),
204    /// Close stdin: nothing more is sent.
205    Close,
206}
207
208/// What the reader, the writer and every handle share.
209pub(crate) struct Shared {
210    /// The writer task's queue: a line is queued whole, under the session
211    /// lock, so lines keep their order and a dropped caller never leaves a
212    /// part of one (R8.5.3).
213    writer: mpsc::UnboundedSender<Write>,
214    session: Mutex<Session>,
215    ctl: mpsc::UnboundedSender<Signal>,
216    exit: watch::Receiver<Option<Exit>>,
217    next_id: AtomicU64,
218    tools: HashMap<String, Tool>,
219    on_approval: Option<ApprovalCallback>,
220    tasks: Mutex<Tasks>,
221}
222
223/// The handler and callback tasks `close` waits for, then aborts: each
224/// call's or request's own task, and the task its handler or callback runs
225/// in (so a panic is caught there).
226#[derive(Default)]
227struct Tasks {
228    own: Vec<tokio::task::JoinHandle<()>>,
229    inner: Vec<tokio::task::AbortHandle>,
230}
231
232impl Shared {
233    fn id(&self) -> String {
234        format!("sdk-{}", self.next_id.fetch_add(1, Ordering::SeqCst))
235    }
236
237    fn ended(&self) -> Option<Error> {
238        self.session.lock().expect("session lock").ended.clone()
239    }
240
241    /// Ends the session with `cause`: every stream and waiting command
242    /// gets it. The first cause wins.
243    fn end(&self, cause: Error) {
244        let mut s = self.session.lock().expect("session lock");
245        if s.ended.is_some() {
246            return;
247        }
248        s.ended = Some(cause.clone());
249        if let Some(turn) = s.turn.take() {
250            turn.events.fail(cause.clone());
251        }
252        if let Some(loose) = s.loose.take() {
253            loose.fail(cause.clone());
254        }
255        s.pending.clear();
256        for fire in s.calls.values() {
257            fire.send_replace(true);
258        }
259    }
260
261    /// Fires every running call's cancellation signal.
262    fn cancel_calls(&self) {
263        let s = self.session.lock().expect("session lock");
264        for fire in s.calls.values() {
265            fire.send_replace(true);
266        }
267    }
268
269    fn track(&self, task: tokio::task::JoinHandle<()>) {
270        let mut t = self.tasks.lock().expect("tasks lock");
271        t.own.retain(|h| !h.is_finished());
272        t.own.push(task);
273    }
274
275    fn track_inner(&self, task: tokio::task::AbortHandle) {
276        let mut t = self.tasks.lock().expect("tasks lock");
277        t.inner.retain(|h| !h.is_finished());
278        t.inner.push(task);
279    }
280
281    /// Waits up to [`TASK_WAIT`] for the tracked tasks, then aborts every
282    /// one, those started meanwhile included. The tasks stay tracked while
283    /// it waits, and a guard aborts them, so a dropped `close` leaves none
284    /// running on.
285    async fn finish_tasks(&self) {
286        struct AbortAll<'a>(&'a Shared);
287        impl Drop for AbortAll<'_> {
288            fn drop(&mut self) {
289                if let Ok(mut t) = self.0.tasks.lock() {
290                    let t = std::mem::take(&mut *t);
291                    for task in t.own {
292                        task.abort();
293                    }
294                    for task in t.inner {
295                        task.abort();
296                    }
297                }
298            }
299        }
300        let _abort = AbortAll(self);
301        let deadline = tokio::time::Instant::now() + TASK_WAIT;
302        while tokio::time::Instant::now() < deadline && !self.tasks_finished() {
303            tokio::time::sleep(Duration::from_millis(20)).await;
304        }
305    }
306
307    fn tasks_finished(&self) -> bool {
308        let t = self.tasks.lock().expect("tasks lock");
309        t.own.iter().all(|h| h.is_finished())
310    }
311
312    /// Writes `command` with `body` and waits for the `response` whose `id`
313    /// is `id`.
314    async fn command_with_id(
315        &self,
316        command: &str,
317        mut body: Value,
318        id: String,
319    ) -> Result<Response, Error> {
320        body["type"] = json!(command);
321        let line = wire::encode(&body)?;
322        self.send_line(command, id, line).await
323    }
324
325    /// Queues an encoded `command` line and waits for its `response`.
326    async fn send_line(&self, command: &str, id: String, line: String) -> Result<Response, Error> {
327        let key = (command.to_string(), id);
328        let reply = {
329            let mut s = self.session.lock().expect("session lock");
330            if let Some(e) = &s.ended {
331                return Err(e.clone());
332            }
333            if s.pending.contains_key(&key) {
334                let id = &key.1;
335                return Err(Error::Refused {
336                    code: "bad_argument".into(),
337                    message: format!("an answer to {id} is already waiting for its response"),
338                });
339            }
340            if self.writer.send(Write::Line(line)).is_err() {
341                return Err(Error::SessionEnded { diagnostics: None });
342            }
343            let (reply, rx) = oneshot::channel();
344            s.pending.insert(key, reply);
345            rx
346        };
347        self.answer(reply).await
348    }
349
350    /// Waits for a queued command's `response`: `Refused` when it is an
351    /// error, the session's end when it never comes.
352    async fn answer(&self, reply: oneshot::Receiver<Response>) -> Result<Response, Error> {
353        match reply.await {
354            Ok(r) if r.ok => Ok(r),
355            Ok(r) => {
356                let (code, message) = r.error.unwrap_or_default();
357                Err(Error::Refused { code, message })
358            }
359            Err(_) => Err(self
360                .ended()
361                .unwrap_or(Error::SessionEnded { diagnostics: None })),
362        }
363    }
364
365    /// Answers the approval request `request_id` (R8.3.3).
366    async fn respond(&self, request_id: &str, decision: Decision) -> Result<(), Error> {
367        let mut body = json!({ "id": request_id, "decision": decision.as_wire() });
368        if let Decision::Deny {
369            reason: Some(reason),
370        } = &decision
371        {
372            body["reason"] = json!(wire::cut(reason, REASON_LIMIT));
373        }
374        // `approval`'s `id` names the request, so the SDK's own id cannot
375        // ride on it: the answer is matched by the request's id, and a
376        // second answer while one is in flight is refused before it is sent.
377        self.command_with_id("approval", body, request_id.to_string())
378            .await
379            .map(|_| ())
380    }
381
382    /// The stop of R8.6.2: `quit` and the end of stdin are queued, then
383    /// the exit is awaited, escalating to signals whether or not the writer
384    /// got them through (a harness not reading stdin is stopped all the same).
385    async fn shut_down(self: &Arc<Self>) {
386        let quit = wire::encode(&json!({ "type": "quit", "id": self.id() })).expect("fits");
387        let _ = self.writer.send(Write::Line(quit));
388        let _ = self.writer.send(Write::Close);
389        let mut exit = self.exit.clone();
390        process::stop(&self.ctl, &mut exit, QUIT_WAIT).await;
391    }
392}
393
394/// Owned by every `Client` clone; the last one dropped kills the process
395/// (R8.6.3: a best-effort backstop that never blocks).
396struct Handle {
397    shared: Arc<Shared>,
398}
399
400impl Drop for Handle {
401    fn drop(&mut self) {
402        let _ = self.shared.ctl.send(Signal::Kill);
403    }
404}
405
406/// A session: cheap to clone, every method on `&self`, usable while a
407/// turn's stream is read (R8.1.2).
408#[derive(Clone)]
409pub struct Client {
410    handle: Arc<Handle>,
411    info: Arc<Info>,
412}
413
414impl Client {
415    /// Starts a session and reads its `init`.
416    pub async fn start(options: Options) -> Result<Client, Error> {
417        Self::open(None, options).await
418    }
419
420    /// Continues the session `session_id` (R2.5): the options send only
421    /// what a resume may give, and the same tools.
422    pub async fn resume(session_id: &str, options: Options) -> Result<Client, Error> {
423        Self::open(Some(session_id), options).await
424    }
425
426    async fn open(resume: Option<&str>, options: Options) -> Result<Client, Error> {
427        Self::open_with(resume, options, INIT_WAIT).await
428    }
429
430    /// [`Client::open`] waiting `init_wait` for `init`.
431    async fn open_with(
432        resume: Option<&str>,
433        options: Options,
434        init_wait: Duration,
435    ) -> Result<Client, Error> {
436        let binary = crate::binary::locate(&options).await?;
437        let body = serde_json::to_vec(&options.file_json(resume.is_some()))
438            .map_err(|e| Error::Protocol(e.to_string()))?;
439        let file = Arc::new(
440            OptionsFile::write(&body)
441                .map_err(|e| Error::Handshake(format!("cannot write the options file: {e}")))?,
442        );
443        let mut proc = process::spawn(process::Launch {
444            binary: &binary,
445            resume,
446            mock: options.mock.as_deref(),
447            cwd: options.cwd.as_deref(),
448            env: &options.env,
449            options: file.clone(),
450        })
451        .await?;
452        let failure = match tokio::time::timeout(init_wait, read_init(&mut proc.stdout)).await {
453            Err(_) => Error::Handshake(format!("no init within {} s", init_wait.as_secs())),
454            Ok(Err(e)) => e,
455            Ok(Ok(None)) => {
456                let d =
457                    process::diagnostics(&mut proc.exit, &proc.stderr, &mut proc.stderr_done).await;
458                if d.exit_code == Some(2) {
459                    // harness refused its flags or options, in one stderr
460                    // line naming the field (R2.1).
461                    Error::Refused {
462                        code: "bad_argument".into(),
463                        message: d.stderr_tail.last().cloned().unwrap_or_default(),
464                    }
465                } else {
466                    Error::SessionEnded {
467                        diagnostics: Some(d),
468                    }
469                }
470            }
471            Ok(Ok(Some((info, v)))) => match check(&info, v, options.tools.len()) {
472                Err(why) => Error::Handshake(why),
473                Ok(()) => {
474                    file.remove();
475                    return Ok(Self::run(info, proc, options));
476                }
477            },
478        };
479        // R8.6.4: stdin closed, the process stopped and reaped, the options
480        // file removed, then the error.
481        let process::Process {
482            stdin,
483            ctl,
484            mut exit,
485            ..
486        } = proc;
487        drop(stdin);
488        process::stop(&ctl, &mut exit, QUIT_WAIT).await;
489        file.remove();
490        Err(failure)
491    }
492
493    /// The session after a good `init`: the reader task started.
494    fn run(info: Info, proc: process::Process, options: Options) -> Client {
495        let (writer, lines) = mpsc::unbounded_channel();
496        let shared = Arc::new(Shared {
497            writer,
498            session: Mutex::new(Session::default()),
499            ctl: proc.ctl,
500            exit: proc.exit.clone(),
501            next_id: AtomicU64::new(1),
502            tools: options
503                .tools
504                .into_iter()
505                .map(|t| (t.name.clone(), t))
506                .collect(),
507            on_approval: options.on_approval,
508            tasks: Mutex::new(Tasks::default()),
509        });
510        tokio::spawn(write(proc.stdin, lines, Arc::downgrade(&shared)));
511        tokio::spawn(read(
512            shared.clone(),
513            proc.stdout,
514            proc.exit,
515            proc.stderr,
516            proc.stderr_done,
517        ));
518        Client {
519            handle: Arc::new(Handle { shared }),
520            info: Arc::new(info),
521        }
522    }
523
524    fn shared(&self) -> &Arc<Shared> {
525        &self.handle.shared
526    }
527
528    /// `init`'s data.
529    pub fn info(&self) -> &Info {
530        &self.info
531    }
532
533    /// The session's id.
534    pub fn id(&self) -> &str {
535        &self.info.session_id
536    }
537
538    /// Starts a turn with `prompt` and returns its events, `user` to
539    /// `result`. One turn at a time: until the wire's `result` (whether or
540    /// not the stream is read), or an [`Client::abort`] that took the
541    /// prompt back, another `send` is `TurnInProgress`.
542    pub async fn send(&self, prompt: &str) -> Result<TurnStream, Error> {
543        let shared = self.shared();
544        let id = shared.id();
545        let line = wire::encode(&json!({ "type": "prompt", "id": id, "text": prompt }))?;
546        let (events, reply) = {
547            let mut s = shared.session.lock().expect("session lock");
548            if let Some(e) = &s.ended {
549                return Err(e.clone());
550            }
551            if s.turn.is_some() {
552                return Err(Error::TurnInProgress);
553            }
554            let (tx, events) = queue();
555            s.turn = Some(Turn {
556                prompt_id: id.clone(),
557                opened: false,
558                aborts: HashSet::new(),
559                events: tx,
560            });
561            let (reply, rx) = oneshot::channel();
562            let key = ("prompt".to_string(), id.clone());
563            s.pending.insert(key.clone(), reply);
564            if shared.writer.send(Write::Line(line)).is_err() {
565                s.turn = None;
566                s.pending.remove(&key);
567                return Err(Error::SessionEnded { diagnostics: None });
568            }
569            (events, rx)
570        };
571        shared.answer(reply).await?;
572        Ok(TurnStream {
573            init: None,
574            events,
575            owner: None,
576            _session: Some(self.clone()),
577            closing: None,
578        })
579    }
580
581    /// Joins the running turn at its next landing point.
582    pub async fn steer(&self, text: &str) -> Result<Disposition, Error> {
583        self.disposition("steer", json!({ "text": text })).await
584    }
585
586    /// Interrupts the program's turn, naming its prompt: a running one is
587    /// `Interrupting`; one still `queued` (behind a compaction) is taken
588    /// back, `Dequeued`, never runs, and its stream ends with no `result`.
589    /// With no turn of the program's, whatever runs is interrupted.
590    pub async fn abort(&self) -> Result<Disposition, Error> {
591        let shared = self.shared();
592        let id = shared.id();
593        // The turn read, the abort registered with it and the line queued
594        // under one lock: the abort names the prompt that precedes it on
595        // the wire.
596        let reply = {
597            let mut s = shared.session.lock().expect("session lock");
598            if let Some(e) = &s.ended {
599                return Err(e.clone());
600            }
601            let mut body = json!({ "type": "abort", "id": id });
602            if let Some(turn) = s.turn.as_ref() {
603                body["prompt_id"] = json!(turn.prompt_id);
604            }
605            let line = wire::encode(&body)?;
606            if shared.writer.send(Write::Line(line)).is_err() {
607                return Err(Error::SessionEnded { diagnostics: None });
608            }
609            if let Some(turn) = s.turn.as_mut() {
610                turn.aborts.insert(id.clone());
611            }
612            let (reply, rx) = oneshot::channel();
613            s.pending.insert(("abort".to_string(), id), reply);
614            rx
615        };
616        let r = shared.answer(reply).await?;
617        Ok(Disposition::from_wire(
618            r.disposition.as_deref().unwrap_or(""),
619        ))
620    }
621
622    /// Compacts the context: at once when idle, after the turn when one
623    /// runs. `Done` carries the compaction its own `response` names
624    /// (`data.compacted`); every `compacted` line goes to the running turn's
625    /// stream or [`Client::session_events`].
626    pub async fn compact(&self) -> Result<Compaction, Error> {
627        let r = self.command("compact", json!({})).await?;
628        Ok(match r.disposition.as_deref() {
629            Some("queued") => Compaction::Queued,
630            _ => Compaction::Done(
631                r.data
632                    .and_then(|mut d| d.get_mut("compacted").map(Value::take))
633                    .and_then(|c| serde_json::from_value::<Compacted>(c).ok()),
634            ),
635        })
636    }
637
638    /// Puts the running or next task away, or ends away.
639    pub async fn away(&self, on: bool) -> Result<Disposition, Error> {
640        self.disposition("away", json!({ "on": on })).await
641    }
642
643    /// The session's state.
644    pub async fn state(&self) -> Result<State, Error> {
645        let r = self.command("get_state", json!({})).await?;
646        let data = r
647            .data
648            .ok_or_else(|| Error::Protocol("a `get_state` response with no `data`".into()))?;
649        serde_json::from_value(data)
650            .map_err(|e| Error::Protocol(format!("a `get_state` response: {e}")))
651    }
652
653    /// Answers the approval request `request_id` (R8.3.3).
654    pub async fn respond(&self, request_id: &str, decision: Decision) -> Result<(), Error> {
655        self.shared().respond(request_id, decision).await
656    }
657
658    /// The events that arrive while no turn's stream is open; the first
659    /// call takes the stream, later calls get `None`. Until it is taken,
660    /// those events are dropped.
661    pub fn session_events(&self) -> Option<Events> {
662        let mut s = self.shared().session.lock().expect("session lock");
663        if std::mem::replace(&mut s.loose_given, true) {
664            return None;
665        }
666        let (tx, events) = queue();
667        match &s.ended {
668            Some(e) => tx.fail(e.clone()),
669            None => s.loose = Some(tx),
670        }
671        Some(events)
672    }
673
674    /// Ends the session (R8.6.2): every running call's cancellation signal
675    /// fires; `quit`, up to 5 s for the exit, SIGTERM, up to 5 s more,
676    /// SIGKILL, the process reaped; meanwhile up to 5 s for handler and
677    /// callback tasks, then they are aborted. Returns within about 15 s.
678    pub async fn close(self) {
679        /// Kills the process when `close` is dropped before the stop
680        /// finished, whatever clones are still held.
681        struct KillOnDrop<'a>(Option<&'a Shared>);
682        impl Drop for KillOnDrop<'_> {
683            fn drop(&mut self) {
684                if let Some(shared) = self.0 {
685                    let _ = shared.ctl.send(Signal::Kill);
686                }
687            }
688        }
689        let shared = self.shared().clone();
690        shared.cancel_calls();
691        let mut kill = KillOnDrop(Some(&shared));
692        tokio::join!(shared.shut_down(), shared.finish_tasks());
693        kill.0 = None;
694        // Tasks started while the process stopped.
695        shared.finish_tasks().await;
696    }
697
698    async fn disposition(&self, command: &str, body: Value) -> Result<Disposition, Error> {
699        let r = self.command(command, body).await?;
700        Ok(Disposition::from_wire(
701            r.disposition.as_deref().unwrap_or(""),
702        ))
703    }
704
705    async fn command(&self, command: &str, body: Value) -> Result<Response, Error> {
706        let id = self.shared().id();
707        let mut body = body;
708        body["id"] = json!(id);
709        self.shared().command_with_id(command, body, id).await
710    }
711}
712
713impl ApprovalRequest {
714    /// Answers this request (R8.3.3), as [`Client::respond`] does by id;
715    /// `SessionEnded` once its session is gone.
716    pub async fn respond(&self, decision: Decision) -> Result<(), Error> {
717        let shared = self
718            .responder
719            .0
720            .as_ref()
721            .and_then(std::sync::Weak::upgrade)
722            .ok_or(Error::SessionEnded { diagnostics: None })?;
723        shared.respond(&self.id, decision).await
724    }
725}
726
727/// Starts a session, sends `prompt`, and yields `init` and then that turn's
728/// events up to its `result`; then the session is closed (R8.1.1).
729pub async fn query(prompt: &str, options: Options) -> Result<TurnStream, Error> {
730    let client = Client::start(options).await?;
731    let mut stream = match client.send(prompt).await {
732        Ok(stream) => stream,
733        Err(e) => {
734            client.close().await;
735            return Err(e);
736        }
737    };
738    stream.init = Some((*client.info).clone());
739    stream._session = None;
740    stream.owner = Some(client);
741    Ok(stream)
742}
743
744/// The writer task: writes each queued line whole, in order, until `Close`,
745/// the queue's end or a failed write; then stdin is closed. A failed write
746/// ends the session at once (every waiting command settled, every running
747/// call's signal fired) and kills the process, whether or not its stdout
748/// ever ends (a descendant can hold it open). It holds the session weakly:
749/// the queue ends when the last handle goes.
750async fn write(
751    mut stdin: ChildStdin,
752    mut lines: mpsc::UnboundedReceiver<Write>,
753    shared: std::sync::Weak<Shared>,
754) {
755    while let Some(Write::Line(line)) = lines.recv().await {
756        if stdin.write_all(line.as_bytes()).await.is_err() || stdin.flush().await.is_err() {
757            if let Some(shared) = shared.upgrade() {
758                shared.end(Error::SessionEnded { diagnostics: None });
759                let _ = shared.ctl.send(Signal::Kill);
760            }
761            break;
762        }
763    }
764    let _ = stdin.shutdown().await;
765}
766
767/// Reads up to the first `init`, skipping lines of unknown type; `None` at
768/// the end of stdout.
769async fn read_init(stdout: &mut BufReader<ChildStdout>) -> Result<Option<(Info, u64)>, Error> {
770    let mut buf = Vec::new();
771    loop {
772        buf.clear();
773        if stdout.read_until(b'\n', &mut buf).await.unwrap_or(0) == 0 {
774            return Ok(None);
775        }
776        let line = trim_newline(&buf);
777        match wire::parse(line).map_err(|e| Error::Handshake(e.to_string()))? {
778            Line::Init(info, v) => return Ok(Some((*info, v))),
779            Line::Other => continue,
780            _ => {
781                return Err(Error::Handshake(format!(
782                    "the first line was not init: {}",
783                    String::from_utf8_lossy(&line[..line.len().min(200)])
784                )));
785            }
786        }
787    }
788}
789
790fn trim_newline(buf: &[u8]) -> &[u8] {
791    let buf = buf.strip_suffix(b"\n").unwrap_or(buf);
792    buf.strip_suffix(b"\r").unwrap_or(buf)
793}
794
795/// R6.2: protocol `v` 1, a harness of this SDK's major.minor, and
796/// `client_tool_result` among the commands when the session has tools.
797fn check(info: &Info, v: u64, tools: usize) -> Result<(), String> {
798    if v != 1 {
799        return Err(format!(
800            "the session speaks protocol v{v}; this SDK speaks v1"
801        ));
802    }
803    let ours = env!("CARGO_PKG_VERSION");
804    let minor = |version: &str| version.splitn(3, '.').take(2).collect::<Vec<_>>().join(".");
805    if minor(&info.harness_version) != minor(ours) {
806        return Err(format!(
807            "harness {} does not match this SDK ({ours}); it needs harness {}.x",
808            info.harness_version,
809            minor(ours)
810        ));
811    }
812    if tools > 0 && !info.commands.iter().any(|c| c == "client_tool_result") {
813        return Err(format!(
814            "harness {} does not take client_tool_result, and the session has tools",
815            info.harness_version
816        ));
817    }
818    Ok(())
819}
820
821/// How long the reader keeps reading stdout once the process has exited:
822/// its last lines are already in the pipe, while a descendant holding the
823/// pipe open may never end it.
824const DRAIN_AFTER_EXIT: Duration = Duration::from_millis(500);
825
826/// The reader (R8.5.3): reads stdout until its end, or until the process
827/// has exited and `DRAIN_AFTER_EXIT` has passed, never waiting on the
828/// program; settles responses, routes events, ends the session on a
829/// protocol error or a slow consumer.
830async fn read(
831    shared: Arc<Shared>,
832    mut stdout: BufReader<ChildStdout>,
833    mut exit: watch::Receiver<Option<Exit>>,
834    stderr: Arc<Mutex<VecDeque<String>>>,
835    mut stderr_done: watch::Receiver<bool>,
836) {
837    // A read cut by the exit or the drain's end leaves what it took in
838    // `buf`, which is cleared only after a whole line.
839    let mut buf = Vec::new();
840    // Once the SDK ended the session, the rest of stdout is read and
841    // dropped, so the process is never held writing while it stops.
842    let mut stopped = false;
843    let mut drain_until: Option<tokio::time::Instant> = None;
844    loop {
845        let read = match drain_until {
846            Some(until) => {
847                match tokio::time::timeout_at(until, stdout.read_until(b'\n', &mut buf)).await {
848                    Ok(read) => read,
849                    Err(_) => break,
850                }
851            }
852            None => tokio::select! {
853                read = stdout.read_until(b'\n', &mut buf) => read,
854                _ = exit.wait_for(Option::is_some) => {
855                    drain_until = Some(tokio::time::Instant::now() + DRAIN_AFTER_EXIT);
856                    continue;
857                }
858            },
859        };
860        if read.unwrap_or(0) == 0 {
861            break;
862        }
863        if stopped {
864            buf.clear();
865            continue;
866        }
867        let routed = wire::parse(trim_newline(&buf)).and_then(|line| route(&shared, line));
868        buf.clear();
869        if let Err(cause) = routed {
870            stop_with(&shared, cause);
871            stopped = true;
872        }
873    }
874    let diagnostics = process::diagnostics(&mut exit, &stderr, &mut stderr_done).await;
875    shared.end(Error::SessionEnded {
876        diagnostics: Some(diagnostics),
877    });
878}
879
880/// Ends the session for `cause` and stops the process (R8.6).
881fn stop_with(shared: &Arc<Shared>, cause: Error) {
882    shared.end(cause);
883    let shared = shared.clone();
884    tokio::spawn(async move { shared.shut_down().await });
885}
886
887/// One line's effect.
888fn route(shared: &Arc<Shared>, line: Line) -> Result<(), Error> {
889    let mut s = shared.session.lock().expect("session lock");
890    match line {
891        Line::ClientToolCall(call) => start_call(shared, &mut s, call),
892        Line::ClientToolCancel(id) => {
893            if let Some(fire) = s.calls.get(&id) {
894                fire.send_replace(true);
895            }
896        }
897        Line::Response(r) => {
898            let Some(id) = r.id.clone() else {
899                return Ok(());
900            };
901            // A refused prompt takes no turn, whether or not the caller of
902            // `send` still waits for its answer.
903            if r.command == "prompt" && !r.ok && s.turn.as_ref().is_some_and(|t| t.prompt_id == id)
904            {
905                s.turn = None;
906            }
907            // A prompt an abort took back never runs: its turn ends here.
908            if r.command == "abort"
909                && r.disposition.as_deref() == Some("dequeued")
910                && s.turn.as_ref().is_some_and(|t| t.aborts.contains(&id))
911            {
912                s.turn = None;
913            }
914            if let Some(reply) = s.pending.remove(&(r.command.clone(), id)) {
915                let _ = reply.send(r);
916            }
917        }
918        Line::Event(event) => {
919            let mut event = *event;
920            if let Event::ApprovalRequest(request) = &mut event {
921                request.responder = Responder(Some(Arc::downgrade(shared)));
922                if let Some(callback) = &shared.on_approval {
923                    start_approval(shared, callback.clone(), request.clone());
924                    return Ok(());
925                }
926            }
927            // The program's turn opens at its prompt's `user` line: the one
928            // prompt in flight is the program's, so the first prompt-kind
929            // `user` line is its own, and the next `result` ends it. A turn
930            // a background agent's result opens has no such line.
931            if let Some(turn) = s.turn.as_mut() {
932                turn.opened =
933                    turn.opened || matches!(&event, Event::User(u) if u.kind == UserKind::Message);
934            }
935            if let Some(turn) = s.turn.as_ref().filter(|t| t.opened) {
936                let end = matches!(event, Event::Result(_));
937                let kept = turn.events.push(event);
938                if end {
939                    s.turn = None;
940                }
941                if !kept {
942                    return Err(Error::SlowConsumer);
943                }
944                return Ok(());
945            }
946            if let Some(loose) = &s.loose
947                && !loose.push(event)
948            {
949                return Err(Error::SlowConsumer);
950            }
951        }
952        // A second `init`, and lines of types this SDK does not read.
953        Line::Init(..) | Line::Other => {}
954    }
955    Ok(())
956}
957
958/// Runs a `client_tool_call` (R8.4) in its own task: the handler in a task
959/// of its own (a panic is the call's error), then the result, unless the
960/// call's signal fired first (R8.4.4). A `bad_argument` refusal is followed
961/// by an `is_error` result with its message (R8.4.5).
962fn start_call(shared: &Arc<Shared>, s: &mut Session, call: ToolCall) {
963    let (fire, signal) = tools::signal();
964    if s.ended.is_some() {
965        fire.send_replace(true);
966    }
967    s.calls.insert(call.id.clone(), fire);
968    let tool = shared.tools.get(&call.name).cloned();
969    let calling = shared.clone();
970    shared.track(tokio::spawn(async move {
971        let ToolCall {
972            id,
973            name,
974            input,
975            too_deep,
976        } = call;
977        let outcome = match tool {
978            _ if too_deep => Err("the arguments are nested too deeply".to_string()),
979            None => Err(format!("this program has no tool named {name:?}")),
980            Some(tool) => {
981                let ctx = ToolContext::new(id.clone(), signal.clone());
982                let handler = tool.handler.clone();
983                let running = tokio::spawn(async move { handler(input, ctx).await });
984                calling.track_inner(running.abort_handle());
985                match running.await {
986                    Ok(outcome) => outcome,
987                    Err(e) => Err(format!("the tool's handler {}", tools::failure_text(e))),
988                }
989            }
990        };
991        answer_call(&calling, &id, &outcome, &signal).await;
992        calling
993            .session
994            .lock()
995            .expect("session lock")
996            .calls
997            .remove(&id);
998    }));
999}
1000
1001async fn answer_call(
1002    shared: &Shared,
1003    id: &str,
1004    outcome: &Result<tools::ToolOutput, String>,
1005    signal: &CancelSignal,
1006) {
1007    if signal.is_fired() {
1008        return;
1009    }
1010    const COMMAND: &str = "client_tool_result";
1011    let line = tools::result_line(id, outcome);
1012    if let Err(Error::Refused { code, message }) =
1013        shared.send_line(COMMAND, id.to_string(), line).await
1014        && code == "bad_argument"
1015        && !signal.is_fired()
1016    {
1017        let line = tools::error_line(id, &message);
1018        let _ = shared.send_line(COMMAND, id.to_string(), line).await;
1019    }
1020}
1021
1022/// Runs the approval callback on `request` in its own task (R8.3.2) and
1023/// answers with its decision; a panic, or a decision not among the
1024/// request's choices, answers `Deny` with the failure as the reason.
1025fn start_approval(shared: &Arc<Shared>, callback: ApprovalCallback, request: ApprovalRequest) {
1026    let answering = shared.clone();
1027    shared.track(tokio::spawn(async move {
1028        let id = request.id.clone();
1029        let choices = request.choices.clone();
1030        let running = tokio::spawn(async move { callback(request).await });
1031        answering.track_inner(running.abort_handle());
1032        let decision = match running.await {
1033            Ok(d) if choices.iter().any(|c| c.as_str() == d.as_wire()) => d,
1034            Ok(d) => Decision::Deny {
1035                reason: Some(format!(
1036                    "the approval callback chose {}, which is not among the choices offered ({})",
1037                    d.as_wire(),
1038                    choices
1039                        .iter()
1040                        .map(|c| c.as_str())
1041                        .collect::<Vec<_>>()
1042                        .join(", ")
1043                )),
1044            },
1045            Err(e) => Decision::Deny {
1046                reason: Some(format!("the approval callback {}", tools::failure_text(e))),
1047            },
1048        };
1049        let _ = answering.respond(&id, decision).await;
1050    }));
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055    use super::*;
1056
1057    #[test]
1058    fn a_start_waits_60_s_for_init() {
1059        assert_eq!(INIT_WAIT, Duration::from_secs(60));
1060    }
1061
1062    /// The test's own folder: on every path out, success or panic, the
1063    /// files it names are removed, then the (by then empty) folder.
1064    struct Scratch {
1065        dir: std::path::PathBuf,
1066        files: Vec<std::path::PathBuf>,
1067    }
1068
1069    impl Scratch {
1070        fn new(tag: u32) -> Self {
1071            let dir = std::env::temp_dir().join(format!(
1072                "megacode-agent-sdk-test-{}-{tag}",
1073                std::process::id()
1074            ));
1075            std::fs::create_dir(&dir).unwrap();
1076            Self {
1077                dir,
1078                files: Vec::new(),
1079            }
1080        }
1081
1082        /// A path in the folder, removed with it.
1083        fn file(&mut self, name: &str) -> std::path::PathBuf {
1084            let path = self.dir.join(name);
1085            self.files.push(path.clone());
1086            path
1087        }
1088    }
1089
1090    impl Drop for Scratch {
1091        fn drop(&mut self) {
1092            for file in &self.files {
1093                let _ = std::fs::remove_file(file);
1094            }
1095            let _ = std::fs::remove_dir(&self.dir);
1096        }
1097    }
1098
1099    #[tokio::test]
1100    async fn no_init_in_time_is_a_handshake_error_leaving_nothing_behind() {
1101        use std::os::unix::fs::PermissionsExt;
1102        let mut scratch = Scratch::new(line!());
1103        let script = scratch.file("harness-silent");
1104        let pid = scratch.file("pid");
1105        let args = scratch.file("args");
1106        std::fs::write(
1107            &script,
1108            format!(
1109                "#!/bin/sh\necho $$ > '{}'\nfor a in \"$@\"; do printf '%s\\n' \"$a\"; done > '{}'\n\
1110                 while read -r _; do :; done\n",
1111                pid.display(),
1112                args.display()
1113            ),
1114        )
1115        .unwrap();
1116        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
1117
1118        let started = std::time::Instant::now();
1119        let options = Options::new().binary(&script);
1120        match Client::open_with(None, options, Duration::from_secs(1)).await {
1121            Err(Error::Handshake(why)) => assert!(why.contains("no init within 1 s"), "{why}"),
1122            Err(e) => panic!("{e:?}"),
1123            Ok(_) => panic!("started"),
1124        }
1125        let took = started.elapsed();
1126        assert!(took >= Duration::from_secs(1), "{took:?}");
1127        assert!(
1128            took < Duration::from_secs(5),
1129            "stdin closed ends the fake: {took:?}"
1130        );
1131        let pid: i32 = std::fs::read_to_string(&pid)
1132            .unwrap()
1133            .trim()
1134            .parse()
1135            .unwrap();
1136        // SAFETY: signal 0 checks for the process; nothing is sent.
1137        assert!(unsafe { libc::kill(pid, 0) } != 0, "the process was reaped");
1138        let sent = std::fs::read_to_string(&args).unwrap();
1139        let sent: Vec<&str> = sent.lines().collect();
1140        let file = std::path::PathBuf::from(
1141            sent[sent.iter().position(|a| *a == "--options").unwrap() + 1],
1142        );
1143        assert!(!file.exists() && !file.parent().unwrap().exists());
1144    }
1145}