Private instance vars, public get properties

This commit is contained in:
2024-11-01 15:45:54 +01:00
parent d80171de15
commit a95c9676b4
18 changed files with 429 additions and 427 deletions

View File

@@ -1,6 +1,6 @@
from datetime import datetime
import subprocess
import xml.etree.ElementTree as ET
import xml.etree.ElementTree as ET
from typing import Optional
from .entry import Entry
@@ -10,13 +10,11 @@ class BackgroundEntry(Entry):
"""
Entry type for long-running background processes.
Attributes:
script: The script/command to execute
stdout: Captured standard output since last update
stderr: Captured standard error since last update
process: The running subprocess.Popen instance
_accumulated_stdout: Total stdout collected so far
_accumulated_stderr: Total stderr collected so far
Private Attributes:
_script: The script/command to execute
_stdout: Captured standard output
_stderr: Captured standard error
_process: The running subprocess.Popen instance
_exit_code: Exit code when process completes
"""
@@ -30,58 +28,73 @@ class BackgroundEntry(Entry):
timestamp: Creation timestamp for this entry
"""
super().__init__(id, timestamp)
self.script = script
self.stdout = ""
self.stderr = ""
self.process: Optional[subprocess.Popen] = None
self._accumulated_stdout = ""
self._accumulated_stderr = ""
self._script = script
self._stdout = ""
self._stderr = ""
self._process: Optional[subprocess.Popen] = None
self._exit_code: Optional[int] = None
@property
def script(self) -> str:
"""Get the script/command being executed."""
return self._script
@property
def stdout(self) -> str:
"""Get the captured standard output."""
return self._stdout
@property
def stderr(self) -> str:
"""Get the captured standard error."""
return self._stderr
@property
def exit_code(self) -> Optional[int]:
"""Get the exit code of the process (None if still running)."""
return self._exit_code
@property
def pid(self) -> Optional[int]:
"""Get the process ID (None if not running)."""
return self._process.pid if self._process is not None else None
def cleanup(self) -> None:
"""
Clean up the background process if it's still running.
Ensures process is terminated and file handles are closed.
"""
self._cleanup_process()
def _cleanup_process(self):
"""Clean up process and file handles"""
if self.process is not None:
if self._process is not None:
try:
# Close file handles if they're open
if self.process.stdout:
self.process.stdout.close()
if self.process.stderr:
self.process.stderr.close()
if self._process.stdout:
self._process.stdout.close()
if self._process.stderr:
self._process.stderr.close()
# Terminate process if it's still running
if self.process.poll() is None:
self.process.terminate()
if self._process.poll() is None:
self._process.terminate()
try:
self.process.wait(timeout=1.0)
self._process.wait(timeout=1.0)
except subprocess.TimeoutExpired:
self.process.kill()
self.process.wait()
self._process.kill()
self._process.wait()
except:
pass # Ignore cleanup errors
finally:
self.process = None
self._process = None
def update(self) -> None:
"""
Start the process if not running and collect any new output.
Updates stdout and stderr with any new output since last update.
Updates stdout and stderr with any new output.
"""
try:
# Reset current output buffers
self.stdout = ""
self.stderr = ""
# Start process if not running
if self.process is None and self._exit_code is None:
self.process = subprocess.Popen(
self.script,
if self._process is None and self._exit_code is None:
self._process = subprocess.Popen(
self._script,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -90,57 +103,52 @@ class BackgroundEntry(Entry):
)
return # Return after starting to allow process to generate output
if self.process is None:
if self._process is None:
return # Process already completed
# Check if process has finished
exit_code = self.process.poll()
exit_code = self._process.poll()
if exit_code is not None:
# Process has terminated, collect remaining output
try:
remaining_out, remaining_err = self.process.communicate(timeout=0.1)
self.stdout = remaining_out
self.stderr = remaining_err
self._accumulated_stdout += remaining_out
self._accumulated_stderr += remaining_err
remaining_out, remaining_err = self._process.communicate(timeout=0.1)
self._stdout += remaining_out
self._stderr += remaining_err
except subprocess.TimeoutExpired:
pass # Process didn't finish communicating, try again next update
self._exit_code = exit_code
self._cleanup_process()
self.cleanup()
return
# Read stdout if available
if self.process.stdout:
if self._process.stdout:
while True:
try:
line = self.process.stdout.readline()
line = self._process.stdout.readline()
if not line:
break
self.stdout += line
self._accumulated_stdout += line
self._stdout += line
except:
break
# Read stderr if available
if self.process.stderr:
if self._process.stderr:
while True:
try:
line = self.process.stderr.readline()
line = self._process.stderr.readline()
if not line:
break
self.stderr += line
self._accumulated_stderr += line
self._stderr += line
except:
break
except Exception as e:
# Handle any errors
error_msg = f"Error handling background process: {str(e)}"
self.stderr = error_msg
self._accumulated_stderr += f"\n{error_msg}"
self._stderr += f"\n{error_msg}"
self._exit_code = -1
self._cleanup_process()
self.cleanup()
def generate_context(self) -> ET.Element:
"""
@@ -150,26 +158,22 @@ class BackgroundEntry(Entry):
ET.Element: XML element containing the entry's data
"""
# Create root element with appropriate status
element = ET.Element("background", {"id": self.id})
element = ET.Element("background", {"id": self._id})
if self.process is not None:
element.set("pid", str(self.process.pid))
if self._process is not None:
element.set("pid", str(self._process.pid))
elif self._exit_code is not None:
element.set("exit_code", str(self._exit_code))
# Add script as CDATA or escaped text
element.text = escape_text_for_xml(self.script)
element.text = escape_text_for_xml(self._script)
# Add stdout element with accumulated output
stdout_elem = ET.SubElement(element, "stdout")
stdout_elem.text = escape_text_for_xml(self._accumulated_stdout)
stdout_elem.text = escape_text_for_xml(self._stdout)
# Add stderr element with accumulated output
stderr_elem = ET.SubElement(element, "stderr")
stderr_elem.text = escape_text_for_xml(self._accumulated_stderr)
stderr_elem.text = escape_text_for_xml(self._stderr)
return element
def __del__(self):
"""Ensure process is terminated and file handles are closed when entry is destroyed"""
self._cleanup_process()
return element

View File

@@ -5,10 +5,6 @@ import xml.etree.ElementTree as ET
class Entry(ABC):
"""
Abstract base class for all entry types in the working memory.
Attributes:
id: Unique identifier for the entry
timestamp: When the entry was created
"""
def __init__(self, id: str, timestamp: datetime):
@@ -19,9 +15,19 @@ class Entry(ABC):
id: Unique identifier for this entry
timestamp: Creation timestamp for this entry
"""
self.id = id
self.timestamp = timestamp
self._id = id
self._timestamp = timestamp
@property
def id(self) -> str:
"""Get entry's unique identifier."""
return self._id
@property
def timestamp(self) -> datetime:
"""Get entry's creation timestamp."""
return self._timestamp
@abstractmethod
def update(self) -> None:
"""

View File

@@ -7,10 +7,6 @@ from .util import escape_text_for_xml
class ParseErrorEntry(Entry):
"""
Entry type for parse and validation errors.
Attributes:
content: The original content that failed to parse
error: The error message describing what went wrong
"""
def __init__(self, content: str, error: str, id: str, timestamp: datetime):
@@ -24,11 +20,20 @@ class ParseErrorEntry(Entry):
timestamp: Creation timestamp for this entry
"""
super().__init__(id, timestamp)
self.content = content
self.error = error
self._content = content
self._error = error
@property
def content(self) -> str:
"""Get the original content that failed to parse."""
return self._content
@property
def error(self) -> str:
"""Get the error message describing the failure."""
return self._error
def update(self) -> None:
"""No update needed for parse error entries."""
pass
def generate_context(self) -> ET.Element:
@@ -39,14 +44,14 @@ class ParseErrorEntry(Entry):
ET.Element: XML element containing the entry's data
"""
# Create root element
element = ET.Element("parse_error", {"id": self.id})
element = ET.Element("parse_error", {"id": self._id})
# Add error subelement
error_elem = ET.SubElement(element, "error")
error_elem.text = escape_text_for_xml(self.error)
error_elem.text = escape_text_for_xml(self._error)
# Add content subelement
content_elem = ET.SubElement(element, "content")
content_elem.text = escape_text_for_xml(self.content)
content_elem.text = escape_text_for_xml(self._content)
return element

View File

@@ -9,8 +9,8 @@ class ReadEntry(Entry):
Entry type for reading content from standard input.
Attributes:
content: Content read from stdin, empty until first update
io_buffer: Buffer to use for IO operations
_content: Content read from stdin, empty until first update
_io_buffer: Buffer to use for IO operations
_read: Whether content has been read
"""
@@ -24,9 +24,16 @@ class ReadEntry(Entry):
timestamp: Creation timestamp for this entry
"""
super().__init__(id, timestamp)
self.content = ""
self.io_buffer = io_buffer
self._content = ""
self._io_buffer = io_buffer
self._read = False
@property
def content(self) -> str:
"""
Get the content read from stdin.
"""
return self._content
def update(self) -> None:
"""
@@ -34,7 +41,7 @@ class ReadEntry(Entry):
Uses the provided IO buffer for the actual read operation.
"""
if not self._read:
self.content = self.io_buffer.read()
self._content = self._io_buffer.read()
self._read = True
def generate_context(self) -> ET.Element:
@@ -45,10 +52,10 @@ class ReadEntry(Entry):
ET.Element: XML element containing the entry's data
"""
# Create root element
element = ET.Element("read_stdin", {"id": self.id})
element = ET.Element("read_stdin", {"id": self._id})
# Add content as CDATA or escaped text if content has been read
if self._read:
element.text = escape_text_for_xml(self.content)
element.text = escape_text_for_xml(self._content)
return element

View File

@@ -9,7 +9,7 @@ class ReasoningEntry(Entry):
Entry type for agent reasoning steps.
Attributes:
content: The reasoning text
_content: The reasoning text
"""
def __init__(self, content: str, id: str, timestamp: datetime):
@@ -22,7 +22,14 @@ class ReasoningEntry(Entry):
timestamp: Creation timestamp for this entry
"""
super().__init__(id, timestamp)
self.content = content
self._content = content
@property
def content(self) -> str:
"""
Get the reasoning text.
"""
return self._content
def update(self) -> None:
"""No update needed for reasoning entries."""
@@ -36,9 +43,9 @@ class ReasoningEntry(Entry):
ET.Element: XML element containing the entry's data
"""
# Create root element
element = ET.Element("reasoning", {"id": self.id})
element = ET.Element("reasoning", {"id": self._id})
# Add content as CDATA or escaped text
element.text = escape_text_for_xml(self.content)
element.text = escape_text_for_xml(self._content)
return element

View File

@@ -9,12 +9,6 @@ from .util import escape_text_for_xml
class RepeatEntry(Entry):
"""
Entry type for scripts that are executed on every update.
Attributes:
script: The script/command to execute
stdout: Captured standard output from most recent execution
stderr: Captured standard error from most recent execution
exit_code: Process exit code from most recent execution
"""
def __init__(self, script: str, id: str, timestamp: datetime):
@@ -27,10 +21,30 @@ class RepeatEntry(Entry):
timestamp: Creation timestamp for this entry
"""
super().__init__(id, timestamp)
self.script = script
self.stdout = ""
self.stderr = ""
self.exit_code: Optional[int] = None
self._script = script
self._stdout = ""
self._stderr = ""
self._exit_code: Optional[int] = None
@property
def script(self) -> str:
"""Get the script/command being executed."""
return self._script
@property
def stdout(self) -> str:
"""Get the captured standard output."""
return self._stdout
@property
def stderr(self) -> str:
"""Get the captured standard error."""
return self._stderr
@property
def exit_code(self) -> Optional[int]:
"""Get the exit code of the process (None if still running)."""
return self._exit_code
def update(self) -> None:
"""
@@ -40,28 +54,28 @@ class RepeatEntry(Entry):
try:
# Run script and capture output
process = subprocess.run(
self.script,
self._script,
shell=True,
capture_output=True,
text=True
)
# Store results
self.stdout = process.stdout
self.stderr = process.stderr
self.exit_code = process.returncode
self._stdout = process.stdout
self._stderr = process.stderr
self._exit_code = process.returncode
except subprocess.SubprocessError as e:
# Handle subprocess errors by capturing the error message
self.stdout = ""
self.stderr = str(e)
self.exit_code = -1
self._stdout = ""
self._stderr = str(e)
self._exit_code = -1
except Exception as e:
# Handle any other errors
self.stdout = ""
self.stderr = f"Error executing script: {str(e)}"
self.exit_code = -1
self._stdout = ""
self._stderr = f"Error executing script: {str(e)}"
self._exit_code = -1
def generate_context(self) -> ET.Element:
"""
@@ -73,7 +87,7 @@ class RepeatEntry(Entry):
# Create root element
element = ET.Element("repeat", {
"id": self.id,
"exit_code": str(self.exit_code) if self.exit_code is not None else "-1"
"exit_code": str(self._exit_code) if self._exit_code is not None else "-1"
})
# Add script as CDATA or escaped text
@@ -81,10 +95,10 @@ class RepeatEntry(Entry):
# Add stdout element
stdout_elem = ET.SubElement(element, "stdout")
stdout_elem.text = escape_text_for_xml(self.stdout)
stdout_elem.text = escape_text_for_xml(self._stdout)
# Add stderr element
stderr_elem = ET.SubElement(element, "stderr")
stderr_elem.text = escape_text_for_xml(self.stderr)
stderr_elem.text = escape_text_for_xml(self._stderr)
return element

View File

@@ -28,11 +28,31 @@ class SingleShotEntry(Entry):
timestamp: Creation timestamp for this entry
"""
super().__init__(id, timestamp)
self.script = script
self.stdout = ""
self.stderr = ""
self.exit_code: Optional[int] = None
self._script = script
self._stdout = ""
self._stderr = ""
self._exit_code: Optional[int] = None
self._executed = False
@property
def script(self) -> str:
"""Get the script/command being executed."""
return self._script
@property
def stdout(self) -> str:
"""Get the captured standard output."""
return self._stdout
@property
def stderr(self) -> str:
"""Get the captured standard error."""
return self._stderr
@property
def exit_code(self) -> Optional[int]:
"""Get the exit code of the process (None if still running)."""
return self._exit_code
def update(self) -> None:
"""
@@ -52,21 +72,21 @@ class SingleShotEntry(Entry):
)
# Store results
self.stdout = process.stdout
self.stderr = process.stderr
self.exit_code = process.returncode
self._stdout = process.stdout
self._stderr = process.stderr
self._exit_code = process.returncode
except subprocess.SubprocessError as e:
# Handle subprocess errors by capturing the error message
self.stdout = ""
self.stderr = str(e)
self.exit_code = -1
self._stdout = ""
self._stderr = str(e)
self._exit_code = -1
except Exception as e:
# Handle any other errors
self.stdout = ""
self.stderr = f"Error executing script: {str(e)}"
self.exit_code = -1
self._stdout = ""
self._stderr = f"Error executing script: {str(e)}"
self._exit_code = -1
self._executed = True
@@ -89,13 +109,13 @@ class SingleShotEntry(Entry):
if self._executed:
# Add stdout element if there is output
stdout_elem = ET.SubElement(element, "stdout")
stdout_elem.text = escape_text_for_xml(self.stdout)
stdout_elem.text = escape_text_for_xml(self._stdout)
# Add stderr element if there are errors
stderr_elem = ET.SubElement(element, "stderr")
stderr_elem.text = escape_text_for_xml(self.stderr)
stderr_elem.text = escape_text_for_xml(self._stderr)
# Add exit code attribute after execution
element.set("exit_code", str(self.exit_code))
element.set("exit_code", str(self._exit_code))
return element