3030 RequestPermissionResponse ,
3131 SessionNotification ,
3232 SetSessionModeRequest ,
33- stdio_streams ,
3433)
3534from acp .schema import (
3635 ContentBlock1 ,
4544 ToolCallContent1 ,
4645 ToolCallUpdate ,
4746)
47+ from acp .stdio import _WritePipeProtocol
4848
4949
5050MODE = Literal ["confirm" , "yolo" , "human" ]
@@ -324,7 +324,7 @@ def _ask_initial_task(self) -> None:
324324 task = self .input_container .request_input ("Enter your task for mini-swe-agent:" )
325325 blocks = [ContentBlock1 (type = "text" , text = task )]
326326 self ._outbox .put (blocks )
327- self ._start_backend ()
327+ self ._start_connection_thread ()
328328
329329 def on_unmount (self ) -> None :
330330 if self ._bg_loop :
@@ -335,57 +335,58 @@ def on_unmount(self) -> None:
335335
336336 # --- Backend comms ---
337337
338- def _start_backend (self ) -> None :
339- def _runner ():
338+ def _start_connection_thread (self ) -> None :
339+ """Start a background thread running the ACP connection event loop."""
340+
341+ def _runner () -> None :
340342 loop = asyncio .new_event_loop ()
341343 self ._bg_loop = loop
342344 asyncio .set_event_loop (loop )
343- loop .run_until_complete (self ._backend_main ())
345+ loop .run_until_complete (self ._run_connection ())
344346
345347 t = threading .Thread (target = _runner , daemon = True )
346348 t .start ()
347349 self ._bg_thread = t
348350
349- async def _backend_main (self ) -> None :
350- # Spawn the agent as a child process and talk over its stdio pipes
351- import sys as _sys
352-
353- agent_path = str (Path (__file__ ).parent / "agent.py" )
354- env = os .environ .copy ()
355- src_dir = str ((Path (__file__ ).resolve ().parents [2 ] / "src" ).resolve ())
356- env ["PYTHONPATH" ] = src_dir + os .pathsep + env .get ("PYTHONPATH" , "" )
357- # Load .env into environment for child agent
358- try :
359- from pathlib import Path as _P
360-
361- dotenv_path = _P (__file__ ).resolve ().parents [2 ] / ".env"
362- if dotenv_path .is_file ():
363- for line in dotenv_path .read_text ().splitlines ():
364- s = line .strip ()
365- if not s or s .startswith ("#" ):
366- continue
367- if s .startswith ("export " ):
368- s = s [len ("export " ) :]
369- if "=" not in s :
370- continue
371- k , v = s .split ("=" , 1 )
372- k = k .strip ()
373- v = v .strip ().strip ('"' ).strip ("'" )
374- env .setdefault (k , v )
375- except Exception :
376- pass
377- proc = await asyncio .create_subprocess_exec (
378- _sys .executable ,
379- agent_path ,
380- stdin = asyncio .subprocess .PIPE ,
381- stdout = asyncio .subprocess .PIPE ,
382- stderr = _sys .stderr ,
383- env = env ,
384- )
385- assert proc .stdout and proc .stdin
386- # Use subprocess-provided streams directly
387- reader = proc .stdout
388- writer = proc .stdin
351+ async def _open_acp_streams_from_env (self ) -> tuple [Optional [asyncio .StreamReader ], Optional [asyncio .StreamWriter ]]:
352+ """If launched via duet, open ACP streams from inherited FDs; else return (None, None)."""
353+ read_fd_s = os .environ .get ("MSWEA_READ_FD" )
354+ write_fd_s = os .environ .get ("MSWEA_WRITE_FD" )
355+ if not read_fd_s or not write_fd_s :
356+ return None , None
357+ read_fd = int (read_fd_s )
358+ write_fd = int (write_fd_s )
359+ loop = asyncio .get_running_loop ()
360+ # Reader
361+ reader = asyncio .StreamReader ()
362+ reader_proto = asyncio .StreamReaderProtocol (reader )
363+ r_file = os .fdopen (read_fd , "rb" , buffering = 0 )
364+ await loop .connect_read_pipe (lambda : reader_proto , r_file )
365+ # Writer
366+ write_proto = _WritePipeProtocol ()
367+ w_file = os .fdopen (write_fd , "wb" , buffering = 0 )
368+ transport , _ = await loop .connect_write_pipe (lambda : write_proto , w_file )
369+ writer = asyncio .StreamWriter (transport , write_proto , None , loop )
370+ return reader , writer
371+
372+ async def _run_connection (self ) -> None :
373+ """Run the ACP client connection using FDs provided by duet; do not fallback."""
374+ reader , writer = await self ._open_acp_streams_from_env ()
375+ if reader is None or writer is None : # type: ignore[truthy-bool]
376+ # Do not fallback; inform user and stop
377+ self .call_from_thread (
378+ lambda : (
379+ self .enqueue_message (
380+ UIMessage (
381+ "assistant" ,
382+ "Communication endpoints not provided. Please launch via examples/mini_swe_agent/duet.py" ,
383+ )
384+ ),
385+ self .on_message_added (),
386+ )
387+ )
388+ self .agent_state = "STOPPED"
389+ return
389390
390391 self ._conn = ClientSideConnection (lambda _agent : MiniSweClientImpl (self ), writer , reader )
391392 try :
@@ -411,7 +412,9 @@ async def _backend_main(self) -> None:
411412 self .on_message_added (),
412413 )
413414 )
415+ self .agent_state = "STOPPED"
414416 return
417+
415418 # Autostep loop: take queued prompts and send; if none and mode != human, keep stepping
416419 while True :
417420 blocks : list [ContentBlock1 ]
0 commit comments