Skip to content

process_manager

Class info

Classes

Name Children Inherits
ProcessManager
llmling_agent.agent.process_manager
Manages background processes for an agent pool.
    ProcessOutput
    llmling_agent.agent.process_manager
    Output from a running process.
      RunningProcess
      llmling_agent.agent.process_manager
      Represents a running background process.

        🛈 DocStrings

        Process management for background command execution.

        ProcessManager

        Manages background processes for an agent pool.

        Source code in src/llmling_agent/agent/process_manager.py
        136
        137
        138
        139
        140
        141
        142
        143
        144
        145
        146
        147
        148
        149
        150
        151
        152
        153
        154
        155
        156
        157
        158
        159
        160
        161
        162
        163
        164
        165
        166
        167
        168
        169
        170
        171
        172
        173
        174
        175
        176
        177
        178
        179
        180
        181
        182
        183
        184
        185
        186
        187
        188
        189
        190
        191
        192
        193
        194
        195
        196
        197
        198
        199
        200
        201
        202
        203
        204
        205
        206
        207
        208
        209
        210
        211
        212
        213
        214
        215
        216
        217
        218
        219
        220
        221
        222
        223
        224
        225
        226
        227
        228
        229
        230
        231
        232
        233
        234
        235
        236
        237
        238
        239
        240
        241
        242
        243
        244
        245
        246
        247
        248
        249
        250
        251
        252
        253
        254
        255
        256
        257
        258
        259
        260
        261
        262
        263
        264
        265
        266
        267
        268
        269
        270
        271
        272
        273
        274
        275
        276
        277
        278
        279
        280
        281
        282
        283
        284
        285
        286
        287
        288
        289
        290
        291
        292
        293
        294
        295
        296
        297
        298
        299
        300
        301
        302
        303
        304
        305
        306
        307
        308
        309
        310
        311
        312
        313
        314
        315
        316
        317
        318
        319
        320
        321
        322
        323
        324
        325
        326
        327
        328
        329
        330
        331
        332
        333
        334
        335
        336
        337
        338
        339
        340
        341
        342
        343
        344
        345
        346
        347
        348
        349
        350
        351
        352
        353
        354
        355
        356
        357
        358
        359
        360
        361
        362
        363
        364
        365
        366
        367
        368
        369
        370
        371
        372
        373
        374
        375
        376
        377
        378
        379
        380
        381
        382
        383
        384
        385
        386
        387
        388
        389
        390
        391
        392
        393
        394
        395
        396
        397
        398
        399
        400
        401
        402
        403
        404
        405
        406
        407
        408
        409
        410
        411
        412
        413
        414
        415
        416
        417
        418
        419
        420
        421
        422
        423
        424
        425
        426
        427
        428
        429
        430
        431
        432
        433
        434
        435
        436
        437
        438
        439
        class ProcessManager:
            """Manages background processes for an agent pool."""
        
            def __init__(self):
                """Initialize process manager."""
                self._processes: dict[str, RunningProcess] = {}
                self._output_tasks: dict[str, asyncio.Task[None]] = {}
        
            async def start_process(
                self,
                command: str,
                args: list[str] | None = None,
                cwd: str | Path | None = None,
                env: dict[str, str] | None = None,
                output_limit: int | None = None,
            ) -> str:
                """Start a background process.
        
                Args:
                    command: Command to execute
                    args: Command arguments
                    cwd: Working directory
                    env: Environment variables (added to current env)
                    output_limit: Maximum bytes of output to retain
        
                Returns:
                    Process ID for tracking
        
                Raises:
                    OSError: If process creation fails
                """
                process_id = f"proc_{uuid.uuid4().hex[:8]}"
                args = args or []
        
                # Prepare environment
                proc_env = dict(os.environ)
                if env:
                    proc_env.update(env)
        
                # Convert cwd to Path if provided
                work_dir = Path(cwd) if cwd else None
        
                try:
                    # Start process
                    process = await asyncio.create_subprocess_exec(
                        command,
                        *args,
                        cwd=work_dir,
                        env=proc_env,
                        stdout=asyncio.subprocess.PIPE,
                        stderr=asyncio.subprocess.PIPE,
                    )
        
                    # Create tracking object
                    running_proc = RunningProcess(
                        process_id=process_id,
                        command=command,
                        args=args,
                        cwd=work_dir,
                        env=env or {},
                        process=process,
                        output_limit=output_limit,
                    )
        
                    self._processes[process_id] = running_proc
        
                    # Start output collection task
                    self._output_tasks[process_id] = asyncio.create_task(
                        self._collect_output(running_proc)
                    )
        
                    logger.info("Started process %s: %s %s", process_id, command, " ".join(args))
                except Exception as e:
                    msg = f"Failed to start process: {command} {' '.join(args)}"
                    logger.exception(msg, exc_info=e)
                    raise OSError(msg) from e
                else:
                    return process_id
        
            async def _collect_output(self, proc: RunningProcess) -> None:
                """Collect output from process in background."""
                try:
                    # Read output streams concurrently
                    stdout_task = asyncio.create_task(self._read_stream(proc.process.stdout))
                    stderr_task = asyncio.create_task(self._read_stream(proc.process.stderr))
        
                    stdout_chunks = []
                    stderr_chunks = []
        
                    # Collect output until both streams close
                    stdout_done = False
                    stderr_done = False
        
                    while not (stdout_done and stderr_done):
                        done, pending = await asyncio.wait(
                            [stdout_task, stderr_task],
                            return_when=asyncio.FIRST_COMPLETED,
                            timeout=0.1,  # Check every 100ms
                        )
        
                        for task in done:
                            if task == stdout_task and not stdout_done:
                                chunk = task.result()
                                if chunk is None:
                                    stdout_done = True
                                else:
                                    stdout_chunks.append(chunk)
                                    proc.add_output(stdout=chunk)
                                    # Restart task for next chunk
                                    stdout_task = asyncio.create_task(
                                        self._read_stream(proc.process.stdout)
                                    )
        
                            elif task == stderr_task and not stderr_done:
                                chunk = task.result()
                                if chunk is None:
                                    stderr_done = True
                                else:
                                    stderr_chunks.append(chunk)
                                    proc.add_output(stderr=chunk)
                                    # Restart task for next chunk
                                    stderr_task = asyncio.create_task(
                                        self._read_stream(proc.process.stderr)
                                    )
        
                    # Cancel any remaining tasks
                    for task in pending:
                        task.cancel()
        
                except Exception:
                    logger.exception("Error collecting output for %s", proc.process_id)
        
            async def _read_stream(self, stream: asyncio.StreamReader | None) -> str | None:
                """Read a chunk from a stream."""
                if not stream:
                    return None
                try:
                    data = await stream.read(8192)  # Read in 8KB chunks
                    return data.decode("utf-8", errors="replace") if data else None
                except Exception:  # noqa: BLE001
                    return None
        
            async def get_output(self, process_id: str) -> ProcessOutput:
                """Get current output from a process.
        
                Args:
                    process_id: Process identifier
        
                Returns:
                    Current process output
        
                Raises:
                    ValueError: If process not found
                """
                if process_id not in self._processes:
                    msg = f"Process {process_id} not found"
                    raise ValueError(msg)
        
                proc = self._processes[process_id]
                return proc.get_output()
        
            async def wait_for_exit(self, process_id: str) -> int:
                """Wait for process to complete.
        
                Args:
                    process_id: Process identifier
        
                Returns:
                    Exit code
        
                Raises:
                    ValueError: If process not found
                """
                if process_id not in self._processes:
                    msg = f"Process {process_id} not found"
                    raise ValueError(msg)
        
                proc = self._processes[process_id]
                exit_code = await proc.wait()
        
                # Wait for output collection to finish
                if process_id in self._output_tasks:
                    await self._output_tasks[process_id]
        
                return exit_code
        
            async def kill_process(self, process_id: str) -> None:
                """Kill a running process.
        
                Args:
                    process_id: Process identifier
        
                Raises:
                    ValueError: If process not found
                """
                if process_id not in self._processes:
                    msg = f"Process {process_id} not found"
                    raise ValueError(msg)
        
                proc = self._processes[process_id]
                await proc.kill()
        
                # Cancel output collection task
                if process_id in self._output_tasks:
                    self._output_tasks[process_id].cancel()
                    with contextlib.suppress(asyncio.CancelledError):
                        await self._output_tasks[process_id]
        
                logger.info("Killed process %s", process_id)
        
            async def release_process(self, process_id: str) -> None:
                """Release resources for a process.
        
                Args:
                    process_id: Process identifier
        
                Raises:
                    ValueError: If process not found
                """
                if process_id not in self._processes:
                    msg = f"Process {process_id} not found"
                    raise ValueError(msg)
        
                # Kill if still running
                proc = self._processes[process_id]
                if await proc.is_running():
                    await proc.kill()
        
                # Clean up tasks
                if process_id in self._output_tasks:
                    self._output_tasks[process_id].cancel()
                    with contextlib.suppress(asyncio.CancelledError):
                        await self._output_tasks[process_id]
                    del self._output_tasks[process_id]
        
                # Remove from tracking
                del self._processes[process_id]
                logger.info("Released process %s", process_id)
        
            def list_processes(self) -> list[str]:
                """List all tracked process IDs."""
                return list(self._processes.keys())
        
            async def get_process_info(self, process_id: str) -> dict[str, Any]:
                """Get information about a process.
        
                Args:
                    process_id: Process identifier
        
                Returns:
                    Process information dict
        
                Raises:
                    ValueError: If process not found
                """
                if process_id not in self._processes:
                    msg = f"Process {process_id} not found"
                    raise ValueError(msg)
        
                proc = self._processes[process_id]
                return {
                    "process_id": process_id,
                    "command": proc.command,
                    "args": proc.args,
                    "cwd": str(proc.cwd) if proc.cwd else None,
                    "created_at": proc.created_at.isoformat(),
                    "is_running": await proc.is_running(),
                    "exit_code": proc.process.returncode,
                    "output_limit": proc.output_limit,
                }
        
            async def cleanup(self) -> None:
                """Clean up all processes."""
                logger.info("Cleaning up %s processes", len(self._processes))
        
                # Try graceful termination first
                termination_tasks = []
                for proc in self._processes.values():
                    if await proc.is_running():
                        proc.process.terminate()
                        termination_tasks.append(proc.wait())
        
                if termination_tasks:
                    try:
                        future = asyncio.gather(*termination_tasks, return_exceptions=True)
                        await asyncio.wait_for(future, timeout=5.0)  # Wait up to 5 seconds
                    except TimeoutError:
                        msg = "Some processes didn't terminate gracefully, force killing"
                        logger.warning(msg)
                        # Force kill remaining processes
                        for proc in self._processes.values():
                            if await proc.is_running():
                                proc.process.kill()
        
                if self._output_tasks:
                    for task in self._output_tasks.values():
                        task.cancel()
                    await asyncio.gather(*self._output_tasks.values(), return_exceptions=True)
        
                # Clear all tracking
                self._processes.clear()
                self._output_tasks.clear()
        
                logger.info("Process cleanup completed")
        

        __init__

        __init__()
        

        Initialize process manager.

        Source code in src/llmling_agent/agent/process_manager.py
        139
        140
        141
        142
        def __init__(self):
            """Initialize process manager."""
            self._processes: dict[str, RunningProcess] = {}
            self._output_tasks: dict[str, asyncio.Task[None]] = {}
        

        cleanup async

        cleanup() -> None
        

        Clean up all processes.

        Source code in src/llmling_agent/agent/process_manager.py
        407
        408
        409
        410
        411
        412
        413
        414
        415
        416
        417
        418
        419
        420
        421
        422
        423
        424
        425
        426
        427
        428
        429
        430
        431
        432
        433
        434
        435
        436
        437
        438
        439
        async def cleanup(self) -> None:
            """Clean up all processes."""
            logger.info("Cleaning up %s processes", len(self._processes))
        
            # Try graceful termination first
            termination_tasks = []
            for proc in self._processes.values():
                if await proc.is_running():
                    proc.process.terminate()
                    termination_tasks.append(proc.wait())
        
            if termination_tasks:
                try:
                    future = asyncio.gather(*termination_tasks, return_exceptions=True)
                    await asyncio.wait_for(future, timeout=5.0)  # Wait up to 5 seconds
                except TimeoutError:
                    msg = "Some processes didn't terminate gracefully, force killing"
                    logger.warning(msg)
                    # Force kill remaining processes
                    for proc in self._processes.values():
                        if await proc.is_running():
                            proc.process.kill()
        
            if self._output_tasks:
                for task in self._output_tasks.values():
                    task.cancel()
                await asyncio.gather(*self._output_tasks.values(), return_exceptions=True)
        
            # Clear all tracking
            self._processes.clear()
            self._output_tasks.clear()
        
            logger.info("Process cleanup completed")
        

        get_output async

        get_output(process_id: str) -> ProcessOutput
        

        Get current output from a process.

        Parameters:

        Name Type Description Default
        process_id str

        Process identifier

        required

        Returns:

        Type Description
        ProcessOutput

        Current process output

        Raises:

        Type Description
        ValueError

        If process not found

        Source code in src/llmling_agent/agent/process_manager.py
        278
        279
        280
        281
        282
        283
        284
        285
        286
        287
        288
        289
        290
        291
        292
        293
        294
        295
        async def get_output(self, process_id: str) -> ProcessOutput:
            """Get current output from a process.
        
            Args:
                process_id: Process identifier
        
            Returns:
                Current process output
        
            Raises:
                ValueError: If process not found
            """
            if process_id not in self._processes:
                msg = f"Process {process_id} not found"
                raise ValueError(msg)
        
            proc = self._processes[process_id]
            return proc.get_output()
        

        get_process_info async

        get_process_info(process_id: str) -> dict[str, Any]
        

        Get information about a process.

        Parameters:

        Name Type Description Default
        process_id str

        Process identifier

        required

        Returns:

        Type Description
        dict[str, Any]

        Process information dict

        Raises:

        Type Description
        ValueError

        If process not found

        Source code in src/llmling_agent/agent/process_manager.py
        379
        380
        381
        382
        383
        384
        385
        386
        387
        388
        389
        390
        391
        392
        393
        394
        395
        396
        397
        398
        399
        400
        401
        402
        403
        404
        405
        async def get_process_info(self, process_id: str) -> dict[str, Any]:
            """Get information about a process.
        
            Args:
                process_id: Process identifier
        
            Returns:
                Process information dict
        
            Raises:
                ValueError: If process not found
            """
            if process_id not in self._processes:
                msg = f"Process {process_id} not found"
                raise ValueError(msg)
        
            proc = self._processes[process_id]
            return {
                "process_id": process_id,
                "command": proc.command,
                "args": proc.args,
                "cwd": str(proc.cwd) if proc.cwd else None,
                "created_at": proc.created_at.isoformat(),
                "is_running": await proc.is_running(),
                "exit_code": proc.process.returncode,
                "output_limit": proc.output_limit,
            }
        

        kill_process async

        kill_process(process_id: str) -> None
        

        Kill a running process.

        Parameters:

        Name Type Description Default
        process_id str

        Process identifier

        required

        Raises:

        Type Description
        ValueError

        If process not found

        Source code in src/llmling_agent/agent/process_manager.py
        322
        323
        324
        325
        326
        327
        328
        329
        330
        331
        332
        333
        334
        335
        336
        337
        338
        339
        340
        341
        342
        343
        344
        async def kill_process(self, process_id: str) -> None:
            """Kill a running process.
        
            Args:
                process_id: Process identifier
        
            Raises:
                ValueError: If process not found
            """
            if process_id not in self._processes:
                msg = f"Process {process_id} not found"
                raise ValueError(msg)
        
            proc = self._processes[process_id]
            await proc.kill()
        
            # Cancel output collection task
            if process_id in self._output_tasks:
                self._output_tasks[process_id].cancel()
                with contextlib.suppress(asyncio.CancelledError):
                    await self._output_tasks[process_id]
        
            logger.info("Killed process %s", process_id)
        

        list_processes

        list_processes() -> list[str]
        

        List all tracked process IDs.

        Source code in src/llmling_agent/agent/process_manager.py
        375
        376
        377
        def list_processes(self) -> list[str]:
            """List all tracked process IDs."""
            return list(self._processes.keys())
        

        release_process async

        release_process(process_id: str) -> None
        

        Release resources for a process.

        Parameters:

        Name Type Description Default
        process_id str

        Process identifier

        required

        Raises:

        Type Description
        ValueError

        If process not found

        Source code in src/llmling_agent/agent/process_manager.py
        346
        347
        348
        349
        350
        351
        352
        353
        354
        355
        356
        357
        358
        359
        360
        361
        362
        363
        364
        365
        366
        367
        368
        369
        370
        371
        372
        373
        async def release_process(self, process_id: str) -> None:
            """Release resources for a process.
        
            Args:
                process_id: Process identifier
        
            Raises:
                ValueError: If process not found
            """
            if process_id not in self._processes:
                msg = f"Process {process_id} not found"
                raise ValueError(msg)
        
            # Kill if still running
            proc = self._processes[process_id]
            if await proc.is_running():
                await proc.kill()
        
            # Clean up tasks
            if process_id in self._output_tasks:
                self._output_tasks[process_id].cancel()
                with contextlib.suppress(asyncio.CancelledError):
                    await self._output_tasks[process_id]
                del self._output_tasks[process_id]
        
            # Remove from tracking
            del self._processes[process_id]
            logger.info("Released process %s", process_id)
        

        start_process async

        start_process(
            command: str,
            args: list[str] | None = None,
            cwd: str | Path | None = None,
            env: dict[str, str] | None = None,
            output_limit: int | None = None,
        ) -> str
        

        Start a background process.

        Parameters:

        Name Type Description Default
        command str

        Command to execute

        required
        args list[str] | None

        Command arguments

        None
        cwd str | Path | None

        Working directory

        None
        env dict[str, str] | None

        Environment variables (added to current env)

        None
        output_limit int | None

        Maximum bytes of output to retain

        None

        Returns:

        Type Description
        str

        Process ID for tracking

        Raises:

        Type Description
        OSError

        If process creation fails

        Source code in src/llmling_agent/agent/process_manager.py
        144
        145
        146
        147
        148
        149
        150
        151
        152
        153
        154
        155
        156
        157
        158
        159
        160
        161
        162
        163
        164
        165
        166
        167
        168
        169
        170
        171
        172
        173
        174
        175
        176
        177
        178
        179
        180
        181
        182
        183
        184
        185
        186
        187
        188
        189
        190
        191
        192
        193
        194
        195
        196
        197
        198
        199
        200
        201
        202
        203
        204
        205
        206
        207
        208
        209
        210
        211
        212
        213
        async def start_process(
            self,
            command: str,
            args: list[str] | None = None,
            cwd: str | Path | None = None,
            env: dict[str, str] | None = None,
            output_limit: int | None = None,
        ) -> str:
            """Start a background process.
        
            Args:
                command: Command to execute
                args: Command arguments
                cwd: Working directory
                env: Environment variables (added to current env)
                output_limit: Maximum bytes of output to retain
        
            Returns:
                Process ID for tracking
        
            Raises:
                OSError: If process creation fails
            """
            process_id = f"proc_{uuid.uuid4().hex[:8]}"
            args = args or []
        
            # Prepare environment
            proc_env = dict(os.environ)
            if env:
                proc_env.update(env)
        
            # Convert cwd to Path if provided
            work_dir = Path(cwd) if cwd else None
        
            try:
                # Start process
                process = await asyncio.create_subprocess_exec(
                    command,
                    *args,
                    cwd=work_dir,
                    env=proc_env,
                    stdout=asyncio.subprocess.PIPE,
                    stderr=asyncio.subprocess.PIPE,
                )
        
                # Create tracking object
                running_proc = RunningProcess(
                    process_id=process_id,
                    command=command,
                    args=args,
                    cwd=work_dir,
                    env=env or {},
                    process=process,
                    output_limit=output_limit,
                )
        
                self._processes[process_id] = running_proc
        
                # Start output collection task
                self._output_tasks[process_id] = asyncio.create_task(
                    self._collect_output(running_proc)
                )
        
                logger.info("Started process %s: %s %s", process_id, command, " ".join(args))
            except Exception as e:
                msg = f"Failed to start process: {command} {' '.join(args)}"
                logger.exception(msg, exc_info=e)
                raise OSError(msg) from e
            else:
                return process_id
        

        wait_for_exit async

        wait_for_exit(process_id: str) -> int
        

        Wait for process to complete.

        Parameters:

        Name Type Description Default
        process_id str

        Process identifier

        required

        Returns:

        Type Description
        int

        Exit code

        Raises:

        Type Description
        ValueError

        If process not found

        Source code in src/llmling_agent/agent/process_manager.py
        297
        298
        299
        300
        301
        302
        303
        304
        305
        306
        307
        308
        309
        310
        311
        312
        313
        314
        315
        316
        317
        318
        319
        320
        async def wait_for_exit(self, process_id: str) -> int:
            """Wait for process to complete.
        
            Args:
                process_id: Process identifier
        
            Returns:
                Exit code
        
            Raises:
                ValueError: If process not found
            """
            if process_id not in self._processes:
                msg = f"Process {process_id} not found"
                raise ValueError(msg)
        
            proc = self._processes[process_id]
            exit_code = await proc.wait()
        
            # Wait for output collection to finish
            if process_id in self._output_tasks:
                await self._output_tasks[process_id]
        
            return exit_code
        

        ProcessOutput dataclass

        Output from a running process.

        Source code in src/llmling_agent/agent/process_manager.py
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        @dataclass
        class ProcessOutput:
            """Output from a running process."""
        
            stdout: str
            stderr: str
            combined: str
            truncated: bool = False
            exit_code: int | None = None
            signal: str | None = None
        

        RunningProcess dataclass

        Represents a running background process.

        Source code in src/llmling_agent/agent/process_manager.py
         32
         33
         34
         35
         36
         37
         38
         39
         40
         41
         42
         43
         44
         45
         46
         47
         48
         49
         50
         51
         52
         53
         54
         55
         56
         57
         58
         59
         60
         61
         62
         63
         64
         65
         66
         67
         68
         69
         70
         71
         72
         73
         74
         75
         76
         77
         78
         79
         80
         81
         82
         83
         84
         85
         86
         87
         88
         89
         90
         91
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        110
        111
        112
        113
        114
        115
        116
        117
        118
        119
        120
        121
        122
        123
        124
        125
        126
        127
        128
        129
        130
        131
        132
        133
        @dataclass
        class RunningProcess:
            """Represents a running background process."""
        
            process_id: str
            command: str
            args: list[str]
            cwd: Path | None
            env: dict[str, str]
            process: asyncio.subprocess.Process
            created_at: datetime = field(default_factory=datetime.now)
            output_limit: int | None = None
            _stdout_buffer: list[str] = field(default_factory=list)
            _stderr_buffer: list[str] = field(default_factory=list)
            _output_size: int = 0
            _truncated: bool = False
        
            def add_output(self, stdout: str = "", stderr: str = "") -> None:
                """Add output to buffers, applying size limits."""
                if stdout:
                    self._stdout_buffer.append(stdout)
                    self._output_size += len(stdout.encode())
                if stderr:
                    self._stderr_buffer.append(stderr)
                    self._output_size += len(stderr.encode())
        
                # Apply truncation if limit exceeded
                if self.output_limit and self._output_size > self.output_limit:
                    self._truncate_output()
                    self._truncated = True
        
            def _truncate_output(self) -> None:
                """Truncate output from beginning to stay within limit."""
                if not self.output_limit:
                    return
        
                # Combine all output to measure total size
                all_stdout = "".join(self._stdout_buffer)
                all_stderr = "".join(self._stderr_buffer)
        
                # Calculate how much to keep
                target_size = int(self.output_limit * 0.9)  # Keep 90% of limit
        
                # Truncate stdout first, then stderr if needed
                if len(all_stdout.encode()) > target_size:
                    # Find character boundary for truncation
                    truncated_stdout = all_stdout[-target_size:].lstrip()
                    self._stdout_buffer = [truncated_stdout]
                    self._stderr_buffer = [all_stderr]
                else:
                    remaining = target_size - len(all_stdout.encode())
                    truncated_stderr = all_stderr[-remaining:].lstrip()
                    self._stdout_buffer = [all_stdout]
                    self._stderr_buffer = [truncated_stderr]
        
                # Update size counter
                self._output_size = sum(
                    len(chunk.encode()) for chunk in self._stdout_buffer + self._stderr_buffer
                )
        
            def get_output(self) -> ProcessOutput:
                """Get current process output."""
                stdout = "".join(self._stdout_buffer)
                stderr = "".join(self._stderr_buffer)
                combined = stdout + stderr
        
                # Check if process has exited
                exit_code = self.process.returncode
                signal = None  # TODO: Extract signal info if available
        
                return ProcessOutput(
                    stdout=stdout,
                    stderr=stderr,
                    combined=combined,
                    truncated=self._truncated,
                    exit_code=exit_code,
                    signal=signal,
                )
        
            async def is_running(self) -> bool:
                """Check if process is still running."""
                return self.process.returncode is None
        
            async def wait(self) -> int:
                """Wait for process to complete and return exit code."""
                return await self.process.wait()
        
            async def kill(self) -> None:
                """Terminate the process."""
                if await self.is_running():
                    try:
                        self.process.terminate()
                        # Give it a moment to terminate gracefully
                        try:
                            await asyncio.wait_for(self.process.wait(), timeout=5.0)
                        except TimeoutError:
                            # Force kill if it doesn't terminate
                            self.process.kill()
                            await self.process.wait()
                    except ProcessLookupError:
                        # Process already dead
                        pass
        

        add_output

        add_output(stdout: str = '', stderr: str = '') -> None
        

        Add output to buffers, applying size limits.

        Source code in src/llmling_agent/agent/process_manager.py
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        def add_output(self, stdout: str = "", stderr: str = "") -> None:
            """Add output to buffers, applying size limits."""
            if stdout:
                self._stdout_buffer.append(stdout)
                self._output_size += len(stdout.encode())
            if stderr:
                self._stderr_buffer.append(stderr)
                self._output_size += len(stderr.encode())
        
            # Apply truncation if limit exceeded
            if self.output_limit and self._output_size > self.output_limit:
                self._truncate_output()
                self._truncated = True
        

        get_output

        get_output() -> ProcessOutput
        

        Get current process output.

        Source code in src/llmling_agent/agent/process_manager.py
         92
         93
         94
         95
         96
         97
         98
         99
        100
        101
        102
        103
        104
        105
        106
        107
        108
        109
        def get_output(self) -> ProcessOutput:
            """Get current process output."""
            stdout = "".join(self._stdout_buffer)
            stderr = "".join(self._stderr_buffer)
            combined = stdout + stderr
        
            # Check if process has exited
            exit_code = self.process.returncode
            signal = None  # TODO: Extract signal info if available
        
            return ProcessOutput(
                stdout=stdout,
                stderr=stderr,
                combined=combined,
                truncated=self._truncated,
                exit_code=exit_code,
                signal=signal,
            )
        

        is_running async

        is_running() -> bool
        

        Check if process is still running.

        Source code in src/llmling_agent/agent/process_manager.py
        111
        112
        113
        async def is_running(self) -> bool:
            """Check if process is still running."""
            return self.process.returncode is None
        

        kill async

        kill() -> None
        

        Terminate the process.

        Source code in src/llmling_agent/agent/process_manager.py
        119
        120
        121
        122
        123
        124
        125
        126
        127
        128
        129
        130
        131
        132
        133
        async def kill(self) -> None:
            """Terminate the process."""
            if await self.is_running():
                try:
                    self.process.terminate()
                    # Give it a moment to terminate gracefully
                    try:
                        await asyncio.wait_for(self.process.wait(), timeout=5.0)
                    except TimeoutError:
                        # Force kill if it doesn't terminate
                        self.process.kill()
                        await self.process.wait()
                except ProcessLookupError:
                    # Process already dead
                    pass
        

        wait async

        wait() -> int
        

        Wait for process to complete and return exit code.

        Source code in src/llmling_agent/agent/process_manager.py
        115
        116
        117
        async def wait(self) -> int:
            """Wait for process to complete and return exit code."""
            return await self.process.wait()