1use 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
23const INIT_WAIT: Duration = Duration::from_secs(60);
25const UNREAD_LIMIT: usize = 10_000;
27const QUIT_WAIT: Duration = Duration::from_secs(5);
29const TASK_WAIT: Duration = Duration::from_secs(5);
32
33#[derive(Debug, Clone, PartialEq)]
35pub enum Compaction {
36 Queued,
38 Done(Option<Compacted>),
40}
41
42struct 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 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
93pub struct Events {
95 rx: mpsc::UnboundedReceiver<Result<Event, Error>>,
96 shared: Arc<Queue>,
97 done: bool,
98}
99
100impl Events {
101 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
127pub struct TurnStream {
129 init: Option<Info>,
130 events: Events,
131 owner: Option<Client>,
133 _session: Option<Client>,
135 closing: Option<tokio::task::JoinHandle<()>>,
136}
137
138impl TurnStream {
139 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
164struct Turn {
166 prompt_id: String,
167 opened: bool,
172 aborts: HashSet<String>,
177 events: QueueTx,
178}
179
180type Key = (String, String);
184
185#[derive(Default)]
186struct Session {
187 pending: HashMap<Key, oneshot::Sender<Response>>,
189 turn: Option<Turn>,
190 loose: Option<QueueTx>,
192 loose_given: bool,
194 ended: Option<Error>,
196 calls: HashMap<String, watch::Sender<bool>>,
199}
200
201enum Write {
203 Line(String),
204 Close,
206}
207
208pub(crate) struct Shared {
210 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#[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 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 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 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 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 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 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 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 self.command_with_id("approval", body, request_id.to_string())
378 .await
379 .map(|_| ())
380 }
381
382 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
394struct 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#[derive(Clone)]
409pub struct Client {
410 handle: Arc<Handle>,
411 info: Arc<Info>,
412}
413
414impl Client {
415 pub async fn start(options: Options) -> Result<Client, Error> {
417 Self::open(None, options).await
418 }
419
420 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 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 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 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 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 pub fn info(&self) -> &Info {
530 &self.info
531 }
532
533 pub fn id(&self) -> &str {
535 &self.info.session_id
536 }
537
538 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 pub async fn steer(&self, text: &str) -> Result<Disposition, Error> {
583 self.disposition("steer", json!({ "text": text })).await
584 }
585
586 pub async fn abort(&self) -> Result<Disposition, Error> {
591 let shared = self.shared();
592 let id = shared.id();
593 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 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 pub async fn away(&self, on: bool) -> Result<Disposition, Error> {
640 self.disposition("away", json!({ "on": on })).await
641 }
642
643 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 pub async fn respond(&self, request_id: &str, decision: Decision) -> Result<(), Error> {
655 self.shared().respond(request_id, decision).await
656 }
657
658 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 pub async fn close(self) {
679 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 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 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
727pub 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
744async 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
767async 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
795fn 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
821const DRAIN_AFTER_EXIT: Duration = Duration::from_millis(500);
825
826async 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 let mut buf = Vec::new();
840 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
880fn 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
887fn 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 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 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 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 Line::Init(..) | Line::Other => {}
954 }
955 Ok(())
956}
957
958fn 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
1022fn 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 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 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 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}