Single-session stdio transport — the hermes route's local link. Pumps newline-delimited JSON between this process's stdin/stdout and the in-process agent, reusing ``make_spawn_agent``/``AgentHandle`` exactly as the WS pump does. There is no socket, so nothing can idle-time-out. ``stdout
(workspace: str, agent_config: AgentServerConfig)
| 118 | |
| 119 | |
| 120 | async def _serve_stdio(workspace: str, agent_config: AgentServerConfig) -> int: |
| 121 | """Single-session stdio transport — the hermes route's local link. |
| 122 | |
| 123 | Pumps newline-delimited JSON between this process's stdin/stdout and the |
| 124 | in-process agent, reusing ``make_spawn_agent``/``AgentHandle`` exactly as the |
| 125 | WS pump does. There is no socket, so nothing can idle-time-out. ``stdout`` is |
| 126 | RESERVED for JSON frames (diagnostics go to ``stderr``); ``stdin`` EOF — the |
| 127 | parent TUI going away — ends the session. |
| 128 | """ |
| 129 | index_path = Path.home() / ".clawcodex" / "server-sessions.json" |
| 130 | index_path.parent.mkdir(parents=True, exist_ok=True) |
| 131 | manager = SessionManager(workspace=workspace, index_path=index_path) |
| 132 | info = manager.create_session(cwd=workspace) |
| 133 | spawn = make_spawn_agent(agent_config) |
| 134 | agent = await spawn(info.id, workspace, None) # emits system/init as its first frame |
| 135 | manager.mark_running(info.id) |
| 136 | |
| 137 | loop = asyncio.get_running_loop() |
| 138 | out = sys.stdout |
| 139 | |
| 140 | async def outbound() -> None: |
| 141 | """Agent → stdout: one JSON object per line, flushed.""" |
| 142 | async for msg in agent.messages_from_agent(): |
| 143 | try: |
| 144 | out.write(json.dumps(msg) + "\n") |
| 145 | out.flush() |
| 146 | except (BrokenPipeError, OSError): |
| 147 | return |
| 148 | |
| 149 | # Inbound: a daemon thread reads stdin lines and dispatches them onto the |
| 150 | # loop (mirrors the existing threaded pattern). stdin EOF resolves `closed`. |
| 151 | closed: asyncio.Future[None] = loop.create_future() |
| 152 | |
| 153 | def _resolve_closed() -> None: |
| 154 | if not closed.done(): |
| 155 | closed.set_result(None) |
| 156 | |
| 157 | def _read_stdin() -> None: |
| 158 | try: |
| 159 | for raw in sys.stdin: |
| 160 | line = raw.strip() |
| 161 | if not line: |
| 162 | continue |
| 163 | try: |
| 164 | parsed = json.loads(line) |
| 165 | except json.JSONDecodeError: |
| 166 | continue |
| 167 | if isinstance(parsed, dict): |
| 168 | asyncio.run_coroutine_threadsafe(agent.send_to_agent(parsed), loop) |
| 169 | except Exception: # noqa: BLE001 - any stdin error ends the session cleanly |
| 170 | pass |
| 171 | finally: |
| 172 | loop.call_soon_threadsafe(_resolve_closed) |
| 173 | |
| 174 | threading.Thread(target=_read_stdin, name="agent-server-stdin", daemon=True).start() |
| 175 | |
| 176 | out_task = loop.create_task(outbound()) |
| 177 | try: |
no test coverage detected