Skip to content

Commit f088bf3

Browse files
committed
cli-5: fixed merge conflcits
2 parents 3ce8727 + 5873909 commit f088bf3

15 files changed

Lines changed: 363 additions & 83 deletions

cecli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from packaging import version
22

3-
__version__ = "0.99.0.dev"
3+
__version__ = "0.99.1.dev"
44
safe_version = __version__
55

66
try:

cecli/coders/agent_coder.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,38 +1009,44 @@ def _generate_tool_context(self, repetitive_tools):
10091009
repetition_warning = None
10101010

10111011
if repetitive_tools:
1012+
default_temp = (
1013+
float(self.get_active_model().use_temperature)
1014+
if isinstance(self.get_active_model().use_temperature, (int, float, str))
1015+
else 1
1016+
)
1017+
default_fp = 0
1018+
10121019
if not self.model_kwargs:
10131020
self.model_kwargs = {
1014-
"temperature": (
1015-
1
1016-
if isinstance(self.get_active_model().use_temperature, bool)
1017-
else float(self.get_active_model().use_temperature)
1018-
)
1019-
+ 0.1,
1020-
"frequency_penalty": 0.2,
1021+
"temperature": default_temp + 0.1,
1022+
"frequency_penalty": default_fp + 0.2,
10211023
"presence_penalty": 0.1,
10221024
}
10231025
else:
1024-
temperature = nested.getter(self.model_kwargs, "temperature")
1025-
freq_penalty = nested.getter(self.model_kwargs, "frequency_penalty")
1026-
if temperature and freq_penalty:
1027-
self.model_kwargs["temperature"] = min(temperature + 0.1, 2)
1028-
self.model_kwargs["frequency_penalty"] = min(freq_penalty + 0.1, 1)
1026+
temperature = nested.getter(self.model_kwargs, "temperature", default_temp)
1027+
freq_penalty = nested.getter(self.model_kwargs, "frequency_penalty", default_fp)
1028+
1029+
self.model_kwargs["temperature"] = temperature + 0.1
1030+
self.model_kwargs["frequency_penalty"] = freq_penalty + 0.1
10291031

10301032
if random.random() < 0.2:
1031-
self.model_kwargs["temperature"] = min(
1032-
(
1033-
1
1034-
if isinstance(self.get_active_model().use_temperature, bool)
1035-
else float(self.get_active_model().use_temperature)
1036-
),
1037-
max(temperature - 0.15, 1),
1033+
self.model_kwargs["temperature"] = max(
1034+
default_temp,
1035+
temperature - 0.15,
1036+
)
1037+
self.model_kwargs["frequency_penalty"] = max(
1038+
default_fp,
1039+
freq_penalty - 0.15,
10381040
)
1039-
self.model_kwargs["frequency_penalty"] = min(0, max(freq_penalty - 0.15, 0))
10401041

10411042
self.model_kwargs["temperature"] = max(
1042-
0, min(nested.getter(self.model_kwargs, "temperature", 1), 1)
1043+
0, min(nested.getter(self.model_kwargs, "temperature", default_temp), 1)
1044+
)
1045+
1046+
self.model_kwargs["frequency_penalty"] = max(
1047+
0, min(nested.getter(self.model_kwargs, "frequency_penalty", default_fp), 1)
10431048
)
1049+
10441050
# One twentieth of the time, just straight reset the randomness
10451051
if random.random() < 0.05:
10461052
self.model_kwargs = {}

cecli/coders/base_coder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3065,7 +3065,7 @@ async def send(self, messages, model=None, functions=None, tools=None):
30653065
self.temperature,
30663066
# This could include any tools, but for now it is just MCP tools
30673067
tools=tools,
3068-
override_kwargs=self.model_kwargs,
3068+
override_kwargs=self.model_kwargs.copy(),
30693069
)
30703070
self.chat_completion_call_hashes.append(hash_object.hexdigest())
30713071

cecli/helpers/background_commands.py

Lines changed: 175 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,22 @@
55
in the background and capturing their output for injection into chat streams.
66
"""
77

8+
import codecs
9+
import os
10+
import platform
811
import subprocess
912
import threading
1013
from collections import deque
1114
from typing import Dict, Optional, Tuple
1215

16+
try:
17+
import pty
18+
import termios
19+
20+
HAS_PTY = True
21+
except ImportError:
22+
HAS_PTY = False
23+
1324

1425
class CircularBuffer:
1526
"""
@@ -89,14 +100,48 @@ def size(self) -> int:
89100
return sum(len(chunk) for chunk in self.buffer)
90101

91102

103+
class InputBuffer:
104+
"""
105+
Thread-safe buffer for queuing input to be sent to a process.
106+
"""
107+
108+
def __init__(self):
109+
self.queue = deque()
110+
self.lock = threading.Lock()
111+
112+
def append(self, text: str) -> None:
113+
"""Add text to the input queue."""
114+
with self.lock:
115+
self.queue.append(text)
116+
117+
def pop_all(self) -> str:
118+
"""Get and clear all queued input."""
119+
with self.lock:
120+
result = "".join(self.queue)
121+
self.queue.clear()
122+
return result
123+
124+
def has_input(self) -> bool:
125+
"""Check if there is queued input."""
126+
with self.lock:
127+
return len(self.queue) > 0
128+
129+
92130
class BackgroundProcess:
93131
"""
94132
Represents a background process with output capture.
95133
"""
96134

97135
def __init__(
98-
self, command: str, process: subprocess.Popen, buffer: CircularBuffer, persist: bool = False
136+
self,
137+
command: str,
138+
process: subprocess.Popen,
139+
buffer: CircularBuffer,
140+
persist: bool = False,
141+
input_buffer: Optional[InputBuffer] = None,
142+
master_fd: Optional[int] = None,
99143
):
144+
self.master_fd = master_fd
100145
"""
101146
Initialize background process wrapper.
102147
@@ -116,7 +161,11 @@ def __init__(
116161
self.start_time = time.time()
117162
self.end_time = None
118163
self.persist = persist
164+
self.input_buffer = input_buffer or InputBuffer()
165+
self.writer_thread = None
166+
self._stop_event = threading.Event()
119167
self._start_output_reader()
168+
self._start_input_writer()
120169

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

131-
# Read stdout
132-
for line in iter(self.process.stdout.readline, ""):
133-
if line:
134-
self.buffer.append(line)
180+
if self.master_fd is not None:
181+
while not self._stop_event.is_set():
182+
try:
183+
data = os.read(self.master_fd, 4096).decode(errors="replace")
184+
if not data:
185+
break
186+
self.buffer.append(data)
187+
except (OSError, EOFError):
188+
break
189+
else:
190+
# Read stdout
191+
for line in iter(self.process.stdout.readline, ""):
192+
if line:
193+
self.buffer.append(line)
135194

136-
# Read stderr
137-
for line in iter(self.process.stderr.readline, ""):
138-
if line:
139-
self.buffer.append(line)
195+
# Read stderr
196+
for line in iter(self.process.stderr.readline, ""):
197+
if line:
198+
self.buffer.append(line)
140199

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

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

206+
def _start_input_writer(self) -> None:
207+
"""Start thread to write input to process stdin."""
208+
209+
def writer():
210+
try:
211+
while not self._stop_event.is_set() and self.is_alive():
212+
if self.input_buffer.has_input():
213+
text = self.input_buffer.pop_all()
214+
if text:
215+
if self.master_fd is not None:
216+
os.write(self.master_fd, text.encode("latin-1"))
217+
else:
218+
try:
219+
# Try to write to the binary buffer for lossless propagation
220+
self.process.stdin.buffer.write(text.encode("latin-1"))
221+
self.process.stdin.buffer.flush()
222+
except (AttributeError, ValueError):
223+
# Fallback to text mode if buffer is not available
224+
self.process.stdin.write(text)
225+
self.process.stdin.flush()
226+
import time
227+
228+
time.sleep(0.1)
229+
except (BrokenPipeError, OSError):
230+
pass
231+
except Exception as e:
232+
self.buffer.append(f"\n[Error writing to process input: {str(e)}]\n")
233+
finally:
234+
try:
235+
if self.master_fd is not None:
236+
os.close(self.master_fd)
237+
else:
238+
self.process.stdin.close()
239+
except Exception:
240+
pass
241+
242+
self.writer_thread = threading.Thread(target=writer, daemon=True)
243+
self.writer_thread.start()
244+
147245
def get_output(self, clear: bool = False) -> str:
148246
"""
149247
Get current output buffer.
@@ -171,6 +269,11 @@ def is_alive(self) -> bool:
171269
"""Check if process is running."""
172270
return self.process.poll() is None
173271

272+
def send_input(self, text: str) -> None:
273+
"""Queue input to be sent to the process."""
274+
if self.input_buffer:
275+
self.input_buffer.append(text)
276+
174277
def stop(self, timeout: float = 5.0) -> Tuple[bool, str, Optional[int]]:
175278
"""
176279
Stop the process gracefully.
@@ -184,6 +287,9 @@ def stop(self, timeout: float = 5.0) -> Tuple[bool, str, Optional[int]]:
184287
import time
185288

186289
try:
290+
# Signal threads to stop
291+
self._stop_event.set()
292+
187293
# Try SIGTERM first
188294
self.process.terminate()
189295
self.process.wait(timeout=timeout)
@@ -192,7 +298,6 @@ def stop(self, timeout: float = 5.0) -> Tuple[bool, str, Optional[int]]:
192298
output = self.get_output(clear=True)
193299
exit_code = self.process.returncode
194300
self.end_time = time.time()
195-
196301
return True, output, exit_code
197302

198303
except subprocess.TimeoutExpired:
@@ -262,6 +367,8 @@ def start_background_command(
262367
existing_process: Optional[subprocess.Popen] = None,
263368
existing_buffer: Optional[CircularBuffer] = None,
264369
persist: bool = False,
370+
existing_input_buffer: Optional[InputBuffer] = None,
371+
use_pty: bool = False,
265372
) -> str:
266373
"""
267374
Start a command in background.
@@ -283,23 +390,52 @@ def start_background_command(
283390
buffer = existing_buffer or CircularBuffer(max_size=max_buffer_size)
284391

285392
# Use existing process or start new one
286-
if existing_process:
393+
master_fd = None
394+
if use_pty and HAS_PTY and platform.system() != "Windows":
395+
master_fd, slave_fd = pty.openpty()
396+
397+
# Disable echo on the slave PTY
398+
attr = termios.tcgetattr(slave_fd)
399+
attr[3] = attr[3] & ~termios.ECHO
400+
termios.tcsetattr(slave_fd, termios.TCSANOW, attr)
401+
402+
process = subprocess.Popen(
403+
command,
404+
shell=True,
405+
stdout=slave_fd,
406+
stderr=slave_fd,
407+
stdin=slave_fd,
408+
cwd=cwd,
409+
close_fds=True,
410+
text=True,
411+
bufsize=1,
412+
universal_newlines=True,
413+
)
414+
os.close(slave_fd)
415+
elif existing_process:
287416
process = existing_process
288417
else:
289418
process = subprocess.Popen(
290419
command,
291420
shell=True,
292421
stdout=subprocess.PIPE,
293422
stderr=subprocess.PIPE,
294-
stdin=subprocess.DEVNULL, # No stdin for background commands
423+
stdin=subprocess.PIPE,
295424
cwd=cwd,
296-
text=True, # Use text mode for easier handling
297-
bufsize=1, # Line buffered
425+
text=True,
426+
bufsize=1,
298427
universal_newlines=True,
299428
)
300429

301430
# Create background process wrapper
302-
bg_process = BackgroundProcess(command, process, buffer, persist=persist)
431+
bg_process = BackgroundProcess(
432+
command,
433+
process,
434+
buffer,
435+
persist=persist,
436+
input_buffer=existing_input_buffer,
437+
master_fd=master_fd,
438+
)
303439

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

506+
@classmethod
507+
def send_command_input(cls, command_key: str, text: str) -> bool:
508+
"""
509+
Send input to a background command.
510+
511+
Args:
512+
command_key: Command key returned by start_background_command
513+
text: Text to send to the command's stdin
514+
515+
Returns:
516+
True if input was queued, False if command not found
517+
"""
518+
with cls._lock:
519+
bg_process = cls._background_commands.get(command_key)
520+
if not bg_process:
521+
return False
522+
# Decode escape sequences (like \x1b) if present in the string
523+
try:
524+
text = codecs.decode(text, "unicode_escape")
525+
except Exception:
526+
pass
527+
bg_process.send_input(text)
528+
return True
529+
370530
@classmethod
371531
def get_all_command_outputs(cls, clear: bool = False) -> Dict[str, str]:
372532
"""

0 commit comments

Comments
 (0)