waldiez 0.5.8__py3-none-any.whl → 0.5.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of waldiez might be problematic. Click here for more details.

Files changed (88) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +112 -24
  3. waldiez/exporting/agent/exporter.py +3 -0
  4. waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
  5. waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
  6. waldiez/exporting/chats/utils/common.py +25 -23
  7. waldiez/exporting/core/__init__.py +0 -2
  8. waldiez/exporting/core/context.py +13 -13
  9. waldiez/exporting/core/protocols.py +0 -141
  10. waldiez/exporting/core/result.py +5 -5
  11. waldiez/exporting/flow/merger.py +2 -2
  12. waldiez/exporting/flow/orchestrator.py +1 -0
  13. waldiez/exporting/flow/utils/common.py +2 -2
  14. waldiez/exporting/flow/utils/importing.py +1 -0
  15. waldiez/exporting/flow/utils/logging.py +6 -7
  16. waldiez/exporting/tools/exporter.py +5 -0
  17. waldiez/exporting/tools/factory.py +4 -0
  18. waldiez/exporting/tools/processor.py +5 -1
  19. waldiez/io/_ws.py +13 -5
  20. waldiez/io/models/content/image.py +1 -0
  21. waldiez/io/models/user_input.py +4 -4
  22. waldiez/io/models/user_response.py +1 -0
  23. waldiez/io/mqtt.py +1 -1
  24. waldiez/io/structured.py +17 -17
  25. waldiez/io/utils.py +1 -1
  26. waldiez/io/ws.py +9 -11
  27. waldiez/logger.py +180 -63
  28. waldiez/models/agents/agent/update_system_message.py +0 -2
  29. waldiez/models/agents/doc_agent/doc_agent.py +8 -1
  30. waldiez/models/common/dict_utils.py +169 -40
  31. waldiez/models/flow/flow.py +6 -6
  32. waldiez/models/flow/info.py +5 -1
  33. waldiez/models/model/_llm.py +28 -14
  34. waldiez/models/model/model.py +4 -1
  35. waldiez/models/model/model_data.py +18 -5
  36. waldiez/models/tool/predefined/_config.py +5 -1
  37. waldiez/models/tool/predefined/_duckduckgo.py +4 -0
  38. waldiez/models/tool/predefined/_email.py +474 -0
  39. waldiez/models/tool/predefined/_google.py +8 -6
  40. waldiez/models/tool/predefined/_perplexity.py +3 -0
  41. waldiez/models/tool/predefined/_searxng.py +3 -0
  42. waldiez/models/tool/predefined/_tavily.py +4 -1
  43. waldiez/models/tool/predefined/_wikipedia.py +4 -1
  44. waldiez/models/tool/predefined/_youtube.py +4 -1
  45. waldiez/models/tool/predefined/protocol.py +3 -0
  46. waldiez/models/tool/tool.py +22 -4
  47. waldiez/models/waldiez.py +12 -0
  48. waldiez/runner.py +37 -54
  49. waldiez/running/__init__.py +6 -0
  50. waldiez/running/base_runner.py +310 -353
  51. waldiez/running/environment.py +1 -0
  52. waldiez/running/exceptions.py +9 -0
  53. waldiez/running/post_run.py +4 -4
  54. waldiez/running/pre_run.py +51 -40
  55. waldiez/running/protocol.py +21 -101
  56. waldiez/running/run_results.py +1 -1
  57. waldiez/running/standard_runner.py +84 -277
  58. waldiez/running/step_by_step/__init__.py +46 -0
  59. waldiez/running/step_by_step/breakpoints_mixin.py +188 -0
  60. waldiez/running/step_by_step/step_by_step_models.py +224 -0
  61. waldiez/running/step_by_step/step_by_step_runner.py +745 -0
  62. waldiez/running/subprocess_runner/__base__.py +282 -0
  63. waldiez/running/subprocess_runner/__init__.py +16 -0
  64. waldiez/running/subprocess_runner/_async_runner.py +362 -0
  65. waldiez/running/subprocess_runner/_sync_runner.py +455 -0
  66. waldiez/running/subprocess_runner/runner.py +561 -0
  67. waldiez/running/timeline_processor.py +1 -1
  68. waldiez/running/utils.py +376 -1
  69. waldiez/utils/version.py +2 -6
  70. waldiez/ws/__init__.py +70 -0
  71. waldiez/ws/__main__.py +15 -0
  72. waldiez/ws/_file_handler.py +201 -0
  73. waldiez/ws/cli.py +211 -0
  74. waldiez/ws/client_manager.py +835 -0
  75. waldiez/ws/errors.py +416 -0
  76. waldiez/ws/models.py +971 -0
  77. waldiez/ws/reloader.py +342 -0
  78. waldiez/ws/server.py +469 -0
  79. waldiez/ws/session_manager.py +393 -0
  80. waldiez/ws/session_stats.py +83 -0
  81. waldiez/ws/utils.py +385 -0
  82. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/METADATA +74 -74
  83. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/RECORD +87 -65
  84. waldiez/running/patch_io_stream.py +0 -210
  85. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/WHEEL +0 -0
  86. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/entry_points.txt +0 -0
  87. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/LICENSE +0 -0
  88. {waldiez-0.5.8.dist-info → waldiez-0.5.10.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,282 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=no-self-use,unused-argument
4
+ # flake8: noqa: G004
5
+ """Base subprocess runner - shared functionality."""
6
+
7
+ import json
8
+ import logging
9
+ import shlex
10
+ import sys
11
+ import uuid
12
+ from pathlib import Path
13
+ from typing import Any, Literal
14
+
15
+
16
+ class BaseSubprocessRunner:
17
+ """Base class with common logic for subprocess runners."""
18
+
19
+ def __init__(
20
+ self,
21
+ session_id: str | None = None,
22
+ input_timeout: float = 120.0,
23
+ uploads_root: str | Path | None = None,
24
+ dot_env: str | Path | None = None,
25
+ logger: logging.Logger | None = None,
26
+ **kwargs: Any,
27
+ ) -> None:
28
+ """Initialize base subprocess runner.
29
+
30
+ Parameters
31
+ ----------
32
+ session_id : str | None
33
+ Unique session identifier
34
+ input_timeout : float
35
+ Timeout for user input
36
+ output_path : str | Path | None
37
+ Path to output file
38
+ uploads_root : str | Path | None
39
+ Root directory for uploads
40
+ dot_env : str | Path | None
41
+ Path to .env file
42
+ logger : logging.Logger | None
43
+ Logger instance to use
44
+ **kwargs : Any
45
+ Additional arguments
46
+ """
47
+ self.session_id = session_id or f"session_{uuid.uuid4().hex}"
48
+ self.input_timeout = input_timeout
49
+ self.uploads_root = uploads_root
50
+ self.dot_env = dot_env
51
+ self.logger = logger or logging.getLogger(self.__class__.__name__)
52
+ self.waiting_for_input = False
53
+
54
+ def build_command(
55
+ self,
56
+ flow_path: Path,
57
+ output_path: str | Path | None = None,
58
+ mode: Literal["debug", "run"] = "run",
59
+ structured: bool = True,
60
+ force: bool = True,
61
+ ) -> list[str]:
62
+ """Build subprocess command.
63
+
64
+ Parameters
65
+ ----------
66
+ flow_path : Path
67
+ Path to the waldiez flow file
68
+ output_path : str | Path | None
69
+ Path to the output file
70
+ mode : Literal["debug", "run"]
71
+ Execution mode ('debug', 'run', etc.)
72
+ structured : bool
73
+ Whether to use structured I/O
74
+ force : bool
75
+ Whether to force overwrite outputs
76
+
77
+ Returns
78
+ -------
79
+ list[str]
80
+ Command arguments list
81
+ """
82
+ _output_path = (
83
+ str(output_path)
84
+ if output_path
85
+ else str(flow_path.with_suffix(".py"))
86
+ )
87
+ cmd = [
88
+ sys.executable,
89
+ "-m",
90
+ "waldiez",
91
+ "run",
92
+ "--file",
93
+ str(flow_path),
94
+ "--output",
95
+ _output_path,
96
+ ]
97
+
98
+ if mode == "debug":
99
+ cmd.append("--step")
100
+
101
+ if structured:
102
+ cmd.append("--structured")
103
+
104
+ if force:
105
+ cmd.append("--force")
106
+
107
+ if self.uploads_root:
108
+ cmd.extend(["--uploads-root", str(self.uploads_root)])
109
+
110
+ if self.dot_env:
111
+ cmd.extend(["--dot-env", str(self.dot_env)])
112
+
113
+ return cmd
114
+
115
+ def parse_output(
116
+ self,
117
+ line: str,
118
+ stream: Literal["stdout", "stderr"],
119
+ ) -> dict[str, Any]:
120
+ """Parse output from subprocess.
121
+
122
+ Parameters
123
+ ----------
124
+ line : str
125
+ Raw output line from subprocess
126
+ stream : Literal["stdout", "stderr"]
127
+ Stream name ('stdout' or 'stderr')
128
+
129
+ Returns
130
+ -------
131
+ dict[str, Any]
132
+ Parsed JSON data if valid, empty dict otherwise
133
+ """
134
+ line = line.strip()
135
+ if not line:
136
+ return {}
137
+
138
+ try:
139
+ data = json.loads(line)
140
+ if isinstance(data, dict):
141
+ return data # pyright: ignore
142
+ except json.JSONDecodeError:
143
+ return self.create_output_message(stream=stream, content=line)
144
+ return self.create_output_message(
145
+ stream=stream, content=line
146
+ ) # pragma: no cover
147
+
148
+ # noinspection PyMethodMayBeStatic
149
+ def decode_subprocess_line(self, line: Any) -> str:
150
+ """Safely decode subprocess output line.
151
+
152
+ Parameters
153
+ ----------
154
+ line : Any
155
+ Raw bytes from subprocess
156
+
157
+ Returns
158
+ -------
159
+ str
160
+ Decoded string
161
+ """
162
+ if isinstance(line, bytes):
163
+ return line.decode("utf-8", errors="replace").strip()
164
+ if isinstance(line, str):
165
+ return line.strip()
166
+ return str(line).strip()
167
+
168
+ def log_subprocess_start(self, cmd: list[str]) -> None:
169
+ """Log subprocess start with command.
170
+
171
+ Parameters
172
+ ----------
173
+ cmd : list[str]
174
+ Command arguments
175
+ """
176
+ cmd_str = " ".join(shlex.quote(arg) for arg in cmd)
177
+ self.logger.info(f"Starting subprocess: {cmd_str}")
178
+
179
+ def log_subprocess_end(self, exit_code: int) -> None:
180
+ """Log subprocess completion.
181
+
182
+ Parameters
183
+ ----------
184
+ exit_code : int
185
+ Process exit code
186
+ """
187
+ if exit_code == 0:
188
+ self.logger.info(
189
+ f"Subprocess completed successfully (exit code: {exit_code})"
190
+ )
191
+ else:
192
+ self.logger.error(f"Subprocess failed (exit code: {exit_code})")
193
+
194
+ # noinspection PyMethodMayBeStatic
195
+ def create_input_response(
196
+ self,
197
+ response_type: str,
198
+ user_input: str,
199
+ request_id: str | None = None,
200
+ ) -> str:
201
+ """Create input response for subprocess.
202
+
203
+ Parameters
204
+ ----------
205
+ response_type : str
206
+ Type of the response
207
+ user_input : str
208
+ User's input response
209
+ request_id : str | None
210
+ Request ID from the input request
211
+
212
+ Returns
213
+ -------
214
+ str
215
+ JSON-formatted response
216
+ """
217
+ response = {
218
+ "type": response_type,
219
+ "data": user_input,
220
+ }
221
+
222
+ if request_id:
223
+ response["request_id"] = request_id
224
+
225
+ return json.dumps(response) + "\n"
226
+
227
+ def create_output_message(
228
+ self, content: str, stream: str = "stdout", msg_type: str = "output"
229
+ ) -> dict[str, Any]:
230
+ """Create standardized output message.
231
+
232
+ Parameters
233
+ ----------
234
+ content : str
235
+ Message content
236
+ stream : str
237
+ Source stream ('stdout', 'stderr')
238
+ msg_type : str
239
+ Message type ('output', 'error', etc.)
240
+
241
+ Returns
242
+ -------
243
+ dict[str, Any]
244
+ The output message
245
+ """
246
+ return {
247
+ "type": "subprocess_output",
248
+ "session_id": self.session_id,
249
+ "stream": stream,
250
+ "content": content,
251
+ "subprocess_type": msg_type,
252
+ "context": {}, # might add in the future
253
+ }
254
+
255
+ def create_completion_message(
256
+ self, success: bool, exit_code: int, message: str | None = None
257
+ ) -> dict[str, Any]:
258
+ """Create completion message.
259
+
260
+ Parameters
261
+ ----------
262
+ success : bool
263
+ Whether subprocess completed successfully
264
+ exit_code : int
265
+ Process exit code
266
+ message : str | None
267
+ Additional message
268
+
269
+ Returns
270
+ -------
271
+ dict
272
+ Completion message
273
+ """
274
+ data = message or f"Process completed with exit code {exit_code}"
275
+ return {
276
+ "type": "subprocess_completion",
277
+ "session_id": self.session_id,
278
+ "success": success,
279
+ "exit_code": exit_code,
280
+ "message": data,
281
+ "context": {}, # might add in the future
282
+ }
@@ -0,0 +1,16 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ """Subprocess based runner."""
5
+
6
+ from .__base__ import BaseSubprocessRunner
7
+ from ._async_runner import AsyncSubprocessRunner
8
+ from ._sync_runner import SyncSubprocessRunner
9
+ from .runner import WaldiezSubprocessRunner
10
+
11
+ __all__ = [
12
+ "SyncSubprocessRunner",
13
+ "AsyncSubprocessRunner",
14
+ "BaseSubprocessRunner",
15
+ "WaldiezSubprocessRunner",
16
+ ]
@@ -0,0 +1,362 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=no-self-use,unused-argument,logging-fstring-interpolation
4
+ # pylint: disable=too-many-try-statements,broad-exception-caught,duplicate-code
5
+ # flake8: noqa: G004
6
+ """Async subprocess runner for Waldiez workflows."""
7
+
8
+ import asyncio
9
+ import logging
10
+
11
+ # noinspection PyProtectedMember
12
+ from asyncio.subprocess import Process as AsyncProcess
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Coroutine, Literal, Optional
15
+
16
+ from .__base__ import BaseSubprocessRunner
17
+
18
+
19
+ class AsyncSubprocessRunner(BaseSubprocessRunner):
20
+ """Async subprocess runner for Waldiez workflows."""
21
+
22
+ def __init__(
23
+ self,
24
+ on_output: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
25
+ on_input_request: Callable[[str], Coroutine[Any, Any, None]],
26
+ input_timeout: float = 120.0,
27
+ uploads_root: str | Path | None = None,
28
+ dot_env: str | Path | None = None,
29
+ logger: logging.Logger | None = None,
30
+ **kwargs: Any,
31
+ ) -> None:
32
+ """Initialize async subprocess runner.
33
+
34
+ Parameters
35
+ ----------
36
+ on_output : Callable[[dict[str, Any]], Coroutine[Any, Any, None]]
37
+ Callback for handling output messages
38
+ on_input_request : Callable[[str], Coroutine[Any, Any, None]]
39
+ Callback for handling input requests
40
+ input_timeout : float
41
+ Timeout for user input
42
+ uploads_root : str | Path | None
43
+ Root directory for uploads
44
+ dot_env : str | Path | None
45
+ Path to .env file
46
+ logger : logging.Logger | None
47
+ Logger instance
48
+ **kwargs : Any
49
+ Additional arguments passed to base class
50
+ """
51
+ super().__init__(
52
+ input_timeout=input_timeout,
53
+ uploads_root=uploads_root,
54
+ dot_env=dot_env,
55
+ logger=logger,
56
+ **kwargs,
57
+ )
58
+ self.on_output = on_output
59
+ self.on_input_request = on_input_request
60
+ self.process: Optional[AsyncProcess] = None
61
+ self.input_queue: asyncio.Queue[str] = asyncio.Queue()
62
+ self.output_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
63
+ self._monitor_tasks: list[asyncio.Task[Any]] = []
64
+
65
+ async def run_subprocess(
66
+ self,
67
+ flow_path: Path,
68
+ mode: Literal["debug", "run"] = "debug",
69
+ ) -> bool:
70
+ """Run subprocess with the given flow data.
71
+
72
+ Parameters
73
+ ----------
74
+ flow_path : Path
75
+ Path to the waldiez flow file
76
+ mode : Literal["debug", "run"]
77
+ Execution mode ('debug', 'run')
78
+
79
+ Returns
80
+ -------
81
+ bool
82
+ True if subprocess completed successfully, False otherwise
83
+ """
84
+ try:
85
+ # Prepare flow file
86
+ # flow_path = self.prepare_flow_file(flow_data, base_dir, filename)
87
+
88
+ # Build command
89
+ cmd = self.build_command(flow_path, mode=mode)
90
+ self.log_subprocess_start(cmd)
91
+
92
+ # Start subprocess
93
+ self.process = await asyncio.create_subprocess_exec(
94
+ *cmd,
95
+ stdin=asyncio.subprocess.PIPE,
96
+ stdout=asyncio.subprocess.PIPE,
97
+ stderr=asyncio.subprocess.PIPE,
98
+ )
99
+
100
+ # Start monitoring tasks
101
+ await self._start_monitoring()
102
+
103
+ # Wait for completion
104
+ exit_code = await self.process.wait()
105
+ self.log_subprocess_end(exit_code)
106
+
107
+ # Send completion message
108
+ completion_msg = self.create_completion_message(
109
+ success=exit_code == 0, exit_code=exit_code
110
+ )
111
+ await self.on_output(completion_msg)
112
+
113
+ return exit_code == 0
114
+
115
+ except Exception as e:
116
+ self.logger.error(f"Error running subprocess: {e}")
117
+ error_msg = self.create_output_message(
118
+ f"Subprocess error: {str(e)}",
119
+ stream="stderr",
120
+ msg_type="error",
121
+ )
122
+ await self.on_output(error_msg)
123
+ return False
124
+
125
+ finally:
126
+ await self._cleanup()
127
+
128
+ async def provide_user_input(self, user_input: str) -> None:
129
+ """Provide user input response.
130
+
131
+ Parameters
132
+ ----------
133
+ user_input : str
134
+ User's input response
135
+ """
136
+ if self.waiting_for_input:
137
+ try:
138
+ await self.input_queue.put(user_input)
139
+ self.logger.debug(f"User input queued: {user_input}")
140
+ except Exception as e:
141
+ self.logger.error(f"Error queuing user input: {e}")
142
+
143
+ async def stop(self) -> None:
144
+ """Stop the subprocess."""
145
+ await self._cleanup()
146
+ if self.process:
147
+ try:
148
+ self.logger.info("Terminating subprocess")
149
+ self.process.terminate()
150
+
151
+ # Wait a bit for graceful termination
152
+ try:
153
+ await asyncio.wait_for(self.process.wait(), timeout=5.0)
154
+ except asyncio.TimeoutError:
155
+ # Force kill if it doesn't terminate gracefully
156
+ self.logger.warning("Force killing subprocess")
157
+ self.process.kill()
158
+ await self.process.wait()
159
+
160
+ except Exception as e:
161
+ self.logger.error(f"Error stopping subprocess: {e}")
162
+ self.process = None
163
+
164
+ async def _start_monitoring(self) -> None:
165
+ """Start monitoring tasks for subprocess I/O."""
166
+ if not self.process:
167
+ return
168
+
169
+ # Create monitoring tasks
170
+ self._monitor_tasks = [
171
+ asyncio.create_task(self._read_stdout()),
172
+ asyncio.create_task(self._read_stderr()),
173
+ asyncio.create_task(self._send_queued_messages()),
174
+ ]
175
+
176
+ # Wait for all monitoring tasks to complete
177
+ await asyncio.gather(*self._monitor_tasks, return_exceptions=True)
178
+
179
+ async def _read_stdout(self) -> None:
180
+ """Read and handle stdout from subprocess."""
181
+ if not self.process or not self.process.stdout:
182
+ return
183
+
184
+ try:
185
+ while self.process.returncode is None: # pragma: no branch
186
+ try:
187
+ line = await asyncio.wait_for(
188
+ self.process.stdout.readline(), timeout=1.0
189
+ )
190
+
191
+ if not line:
192
+ break
193
+
194
+ line_str = self.decode_subprocess_line(line)
195
+ if line_str:
196
+ await self._handle_stdout_line(line_str)
197
+
198
+ except asyncio.TimeoutError:
199
+ continue
200
+ except Exception as e:
201
+ self.logger.error(f"Error reading stdout: {e}")
202
+ break
203
+
204
+ except Exception as e:
205
+ self.logger.error(f"Error in stdout reader: {e}")
206
+
207
+ async def _read_stderr(self) -> None:
208
+ """Read and handle stderr from subprocess."""
209
+ if not self.process or not self.process.stderr:
210
+ return
211
+
212
+ try:
213
+ while self.process.returncode is None: # pragma: no branch
214
+ try:
215
+ line = await asyncio.wait_for(
216
+ self.process.stderr.readline(), timeout=1.0
217
+ )
218
+
219
+ if not line:
220
+ break
221
+
222
+ line_str = self.decode_subprocess_line(line)
223
+ if line_str:
224
+ error_msg = self.create_output_message(
225
+ line_str, stream="stderr", msg_type="error"
226
+ )
227
+ await self.output_queue.put(error_msg)
228
+
229
+ except asyncio.TimeoutError:
230
+ continue
231
+ except Exception as e:
232
+ self.logger.error(f"Error reading stderr: {e}")
233
+ break
234
+
235
+ except Exception as e:
236
+ self.logger.error(f"Error in stderr reader: {e}")
237
+
238
+ async def _send_queued_messages(self) -> None:
239
+ """Send queued messages to output callback."""
240
+ try:
241
+ while self.process and self.process.returncode is None:
242
+ try:
243
+ message = await asyncio.wait_for(
244
+ self.output_queue.get(), timeout=1.0
245
+ )
246
+
247
+ try:
248
+ await self.on_output(message)
249
+ except Exception as e:
250
+ self.logger.error(f"Error in output callback: {e}")
251
+
252
+ except asyncio.TimeoutError:
253
+ continue
254
+ except Exception as e:
255
+ self.logger.error(f"Error sending queued message: {e}")
256
+ break
257
+
258
+ except Exception as e:
259
+ self.logger.error(f"Error in message sender: {e}")
260
+
261
+ async def _handle_stdout_line(self, line: str) -> None:
262
+ """Handle a line from stdout.
263
+
264
+ Parameters
265
+ ----------
266
+ line : str
267
+ Decoded line from stdout
268
+ """
269
+ # Try to parse as structured JSON first
270
+ parsed_data = self.parse_output(line, stream="stdout")
271
+ if not parsed_data:
272
+ return
273
+ if parsed_data.get("type") in ("input_request", "debug_input_request"):
274
+ self.waiting_for_input = True
275
+ await self.on_input_request(parsed_data.get("prompt", "> "))
276
+ await self._handle_input_request(parsed_data)
277
+ else:
278
+ try:
279
+ await self.output_queue.put(parsed_data)
280
+ except Exception as e:
281
+ self.logger.error(f"Error queuing output message: {e}")
282
+
283
+ async def _handle_input_request(self, data: dict[str, Any]) -> None:
284
+ """Handle input request from subprocess.
285
+
286
+ Parameters
287
+ ----------
288
+ data : dict[str, Any]
289
+ Input request data
290
+ """
291
+ response_type = str(data.get("type", "input_request")).replace(
292
+ "request", "response"
293
+ )
294
+ request_id = data.get("request_id")
295
+ try:
296
+ # Wait for user response
297
+ user_input = await asyncio.wait_for(
298
+ self.input_queue.get(),
299
+ timeout=self.input_timeout,
300
+ )
301
+
302
+ # Create response
303
+ response = self.create_input_response(
304
+ response_type=response_type,
305
+ user_input=user_input,
306
+ request_id=request_id,
307
+ )
308
+
309
+ # Send response to subprocess
310
+ if self.process and self.process.stdin:
311
+ self.process.stdin.write(response.encode())
312
+ await self.process.stdin.drain()
313
+
314
+ self.logger.debug(f"Sent input response: {user_input}")
315
+
316
+ except asyncio.TimeoutError:
317
+ self.logger.warning("Input request timed out")
318
+ # Send empty response on timeout
319
+ if self.process and self.process.stdin:
320
+ response = self.create_input_response(
321
+ response_type=response_type,
322
+ user_input="",
323
+ request_id=request_id,
324
+ )
325
+ self.process.stdin.write(response.encode())
326
+ await self.process.stdin.drain()
327
+
328
+ except Exception as e:
329
+ self.logger.error(f"Error handling input request: {e}")
330
+
331
+ finally:
332
+ self.waiting_for_input = False
333
+
334
+ async def _cleanup(self) -> None:
335
+ """Cleanup resources."""
336
+ # Cancel monitoring tasks
337
+ for task in self._monitor_tasks:
338
+ if not task.done():
339
+ task.cancel()
340
+
341
+ # Wait for tasks to complete
342
+ if self._monitor_tasks:
343
+ await asyncio.gather(*self._monitor_tasks, return_exceptions=True)
344
+
345
+ self._monitor_tasks.clear()
346
+
347
+ # Close process streams
348
+ if self.process:
349
+ if self.process.stdin and not self.process.stdin.is_closing():
350
+ self.process.stdin.close()
351
+ await self.process.stdin.wait_closed()
352
+ self.waiting_for_input = False
353
+
354
+ def is_running(self) -> bool:
355
+ """Check if subprocess is currently running.
356
+
357
+ Returns
358
+ -------
359
+ bool
360
+ True if subprocess is running, False otherwise
361
+ """
362
+ return self.process is not None and self.process.returncode is None