Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cecli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "0.99.0.dev"
__version__ = "0.99.1.dev"
safe_version = __version__

try:
Expand Down
47 changes: 27 additions & 20 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,37 +1009,44 @@ def _generate_tool_context(self, repetitive_tools):
repetition_warning = None

if repetitive_tools:
default_temp = (
float(self.get_active_model().use_temperature)
if isinstance(self.get_active_model().use_temperature, (int, float, str))
else 1
)
default_fp = 0

if not self.model_kwargs:
self.model_kwargs = {
"temperature": (
1
if isinstance(self.get_active_model().use_temperature, bool)
else float(self.get_active_model().use_temperature)
) + 0.1,
"frequency_penalty": 0.2,
"temperature": default_temp + 0.1,
"frequency_penalty": default_fp + 0.2,
"presence_penalty": 0.1,
}
else:
temperature = nested.getter(self.model_kwargs, "temperature")
freq_penalty = nested.getter(self.model_kwargs, "frequency_penalty")
if temperature and freq_penalty:
self.model_kwargs["temperature"] = min(temperature + 0.1, 2)
self.model_kwargs["frequency_penalty"] = min(freq_penalty + 0.1, 1)
temperature = nested.getter(self.model_kwargs, "temperature", default_temp)
freq_penalty = nested.getter(self.model_kwargs, "frequency_penalty", default_fp)

self.model_kwargs["temperature"] = temperature + 0.1
self.model_kwargs["frequency_penalty"] = freq_penalty + 0.1

if random.random() < 0.2:
self.model_kwargs["temperature"] = min(
(
1
if isinstance(self.get_active_model().use_temperature, bool)
else float(self.get_active_model().use_temperature)
),
max(temperature - 0.15, 1),
self.model_kwargs["temperature"] = max(
default_temp,
temperature - 0.15,
)
self.model_kwargs["frequency_penalty"] = max(
default_fp,
freq_penalty - 0.15,
)
self.model_kwargs["frequency_penalty"] = min(0, max(freq_penalty - 0.15, 0))

self.model_kwargs["temperature"] = max(
0, min(nested.getter(self.model_kwargs, "temperature", 1), 1)
0, min(nested.getter(self.model_kwargs, "temperature", default_temp), 1)
)

self.model_kwargs["frequency_penalty"] = max(
0, min(nested.getter(self.model_kwargs, "frequency_penalty", default_fp), 1)
)

# One twentieth of the time, just straight reset the randomness
if random.random() < 0.05:
self.model_kwargs = {}
Expand Down
2 changes: 1 addition & 1 deletion cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3065,7 +3065,7 @@ async def send(self, messages, model=None, functions=None, tools=None):
self.temperature,
# This could include any tools, but for now it is just MCP tools
tools=tools,
override_kwargs=self.model_kwargs,
override_kwargs=self.model_kwargs.copy(),
)
self.chat_completion_call_hashes.append(hash_object.hexdigest())

Expand Down
190 changes: 175 additions & 15 deletions cecli/helpers/background_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@
in the background and capturing their output for injection into chat streams.
"""

import codecs
import os
import platform
import subprocess
import threading
from collections import deque
from typing import Dict, Optional, Tuple

try:
import pty
import termios

HAS_PTY = True
except ImportError:
HAS_PTY = False


class CircularBuffer:
"""
Expand Down Expand Up @@ -89,14 +100,48 @@ def size(self) -> int:
return sum(len(chunk) for chunk in self.buffer)


class InputBuffer:
"""
Thread-safe buffer for queuing input to be sent to a process.
"""

def __init__(self):
self.queue = deque()
self.lock = threading.Lock()

def append(self, text: str) -> None:
"""Add text to the input queue."""
with self.lock:
self.queue.append(text)

def pop_all(self) -> str:
"""Get and clear all queued input."""
with self.lock:
result = "".join(self.queue)
self.queue.clear()
return result

def has_input(self) -> bool:
"""Check if there is queued input."""
with self.lock:
return len(self.queue) > 0


class BackgroundProcess:
"""
Represents a background process with output capture.
"""

def __init__(
self, command: str, process: subprocess.Popen, buffer: CircularBuffer, persist: bool = False
self,
command: str,
process: subprocess.Popen,
buffer: CircularBuffer,
persist: bool = False,
input_buffer: Optional[InputBuffer] = None,
master_fd: Optional[int] = None,
):
self.master_fd = master_fd
"""
Initialize background process wrapper.

Expand All @@ -116,7 +161,11 @@ def __init__(
self.start_time = time.time()
self.end_time = None
self.persist = persist
self.input_buffer = input_buffer or InputBuffer()
self.writer_thread = None
self._stop_event = threading.Event()
self._start_output_reader()
self._start_input_writer()

def _start_output_reader(self) -> None:
"""Start thread to read process output."""
Expand All @@ -128,22 +177,71 @@ def reader():
# we're in a separate thread and the buffer will capture
# output as soon as it's available

# Read stdout
for line in iter(self.process.stdout.readline, ""):
if line:
self.buffer.append(line)
if self.master_fd is not None:
while not self._stop_event.is_set():
try:
data = os.read(self.master_fd, 4096).decode(errors="replace")
if not data:
break
self.buffer.append(data)
except (OSError, EOFError):
break
else:
# Read stdout
for line in iter(self.process.stdout.readline, ""):
if line:
self.buffer.append(line)

# Read stderr
for line in iter(self.process.stderr.readline, ""):
if line:
self.buffer.append(line)
# Read stderr
for line in iter(self.process.stderr.readline, ""):
if line:
self.buffer.append(line)

except Exception as e:
self.buffer.append(f"\n[Error reading process output: {str(e)}]\n")

self.reader_thread = threading.Thread(target=reader, daemon=True)
self.reader_thread.start()

def _start_input_writer(self) -> None:
"""Start thread to write input to process stdin."""

def writer():
try:
while not self._stop_event.is_set() and self.is_alive():
if self.input_buffer.has_input():
text = self.input_buffer.pop_all()
if text:
if self.master_fd is not None:
os.write(self.master_fd, text.encode("latin-1"))
else:
try:
# Try to write to the binary buffer for lossless propagation
self.process.stdin.buffer.write(text.encode("latin-1"))
self.process.stdin.buffer.flush()
except (AttributeError, ValueError):
# Fallback to text mode if buffer is not available
self.process.stdin.write(text)
self.process.stdin.flush()
import time

time.sleep(0.1)
except (BrokenPipeError, OSError):
pass
except Exception as e:
self.buffer.append(f"\n[Error writing to process input: {str(e)}]\n")
finally:
try:
if self.master_fd is not None:
os.close(self.master_fd)
else:
self.process.stdin.close()
except Exception:
pass

self.writer_thread = threading.Thread(target=writer, daemon=True)
self.writer_thread.start()

def get_output(self, clear: bool = False) -> str:
"""
Get current output buffer.
Expand Down Expand Up @@ -171,6 +269,11 @@ def is_alive(self) -> bool:
"""Check if process is running."""
return self.process.poll() is None

def send_input(self, text: str) -> None:
"""Queue input to be sent to the process."""
if self.input_buffer:
self.input_buffer.append(text)

def stop(self, timeout: float = 5.0) -> Tuple[bool, str, Optional[int]]:
"""
Stop the process gracefully.
Expand All @@ -184,6 +287,9 @@ def stop(self, timeout: float = 5.0) -> Tuple[bool, str, Optional[int]]:
import time

try:
# Signal threads to stop
self._stop_event.set()

# Try SIGTERM first
self.process.terminate()
self.process.wait(timeout=timeout)
Expand All @@ -192,7 +298,6 @@ def stop(self, timeout: float = 5.0) -> Tuple[bool, str, Optional[int]]:
output = self.get_output(clear=True)
exit_code = self.process.returncode
self.end_time = time.time()

return True, output, exit_code

except subprocess.TimeoutExpired:
Expand Down Expand Up @@ -262,6 +367,8 @@ def start_background_command(
existing_process: Optional[subprocess.Popen] = None,
existing_buffer: Optional[CircularBuffer] = None,
persist: bool = False,
existing_input_buffer: Optional[InputBuffer] = None,
use_pty: bool = False,
) -> str:
"""
Start a command in background.
Expand All @@ -283,23 +390,52 @@ def start_background_command(
buffer = existing_buffer or CircularBuffer(max_size=max_buffer_size)

# Use existing process or start new one
if existing_process:
master_fd = None
if use_pty and HAS_PTY and platform.system() != "Windows":
master_fd, slave_fd = pty.openpty()

# Disable echo on the slave PTY
attr = termios.tcgetattr(slave_fd)
attr[3] = attr[3] & ~termios.ECHO
termios.tcsetattr(slave_fd, termios.TCSANOW, attr)

process = subprocess.Popen(
command,
shell=True,
stdout=slave_fd,
stderr=slave_fd,
stdin=slave_fd,
cwd=cwd,
close_fds=True,
text=True,
bufsize=1,
universal_newlines=True,
)
os.close(slave_fd)
elif existing_process:
process = existing_process
else:
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.DEVNULL, # No stdin for background commands
stdin=subprocess.PIPE,
cwd=cwd,
text=True, # Use text mode for easier handling
bufsize=1, # Line buffered
text=True,
bufsize=1,
universal_newlines=True,
)

# Create background process wrapper
bg_process = BackgroundProcess(command, process, buffer, persist=persist)
bg_process = BackgroundProcess(
command,
process,
buffer,
persist=persist,
input_buffer=existing_input_buffer,
master_fd=master_fd,
)

# Generate unique key and store
command_key = cls._generate_command_key(command)
Expand Down Expand Up @@ -367,6 +503,30 @@ def get_new_command_output(cls, command_key: str) -> str:
return f"[Error] No background command found with key: {command_key}"
return bg_process.get_new_output()

@classmethod
def send_command_input(cls, command_key: str, text: str) -> bool:
"""
Send input to a background command.

Args:
command_key: Command key returned by start_background_command
text: Text to send to the command's stdin

Returns:
True if input was queued, False if command not found
"""
with cls._lock:
bg_process = cls._background_commands.get(command_key)
if not bg_process:
return False
# Decode escape sequences (like \x1b) if present in the string
try:
text = codecs.decode(text, "unicode_escape")
except Exception:
pass
bg_process.send_input(text)
return True

@classmethod
def get_all_command_outputs(cls, clear: bool = False) -> Dict[str, str]:
"""
Expand Down
Loading
Loading