Skip to content

Commit 9dc97b9

Browse files
committed
Release v4.5.13
1 parent 2f2add5 commit 9dc97b9

File tree

21 files changed

+548
-58
lines changed

21 files changed

+548
-58
lines changed

docker/Dockerfile.chat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
1616
# Install Python packages (using latest versions)
1717
RUN pip install --no-cache-dir \
1818
praisonai_tools \
19-
"praisonai>=4.5.12" \
19+
"praisonai>=4.5.13" \
2020
"praisonai[chat]" \
2121
"embedchain[github,youtube]"
2222

docker/Dockerfile.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
2020
# Install Python packages (using latest versions)
2121
RUN pip install --no-cache-dir \
2222
praisonai_tools \
23-
"praisonai>=4.5.12" \
23+
"praisonai>=4.5.13" \
2424
"praisonai[ui]" \
2525
"praisonai[chat]" \
2626
"praisonai[realtime]" \

docker/Dockerfile.ui

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
1616
# Install Python packages (using latest versions)
1717
RUN pip install --no-cache-dir \
1818
praisonai_tools \
19-
"praisonai>=4.5.12" \
19+
"praisonai>=4.5.13" \
2020
"praisonai[ui]" \
2121
"praisonai[crewai]"
2222

src/praisonai-agents/praisonaiagents/agent/agent.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,6 +1918,9 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19181918
self.autonomy_config = {}
19191919
self._autonomy_trigger = None
19201920
self._doom_loop_tracker = None
1921+
self._file_snapshot = None
1922+
self._snapshot_stack = []
1923+
self._redo_stack = []
19211924
return
19221925

19231926
self.autonomy_enabled = True
@@ -1942,6 +1945,9 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19421945
self.autonomy_config = {}
19431946
self._autonomy_trigger = None
19441947
self._doom_loop_tracker = None
1948+
self._file_snapshot = None
1949+
self._snapshot_stack = []
1950+
self._redo_stack = []
19451951
return
19461952

19471953
# Preserve ALL AutonomyConfig fields in the dict (G14 fix: no lossy extraction)
@@ -1954,6 +1960,8 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19541960
"observe": config.observe,
19551961
"completion_promise": config.completion_promise,
19561962
"clear_context": config.clear_context,
1963+
"track_changes": config.effective_track_changes,
1964+
"snapshot_dir": config.snapshot_dir,
19571965
}
19581966
# Also preserve any extra user-provided keys from dict input
19591967
if isinstance(autonomy, dict):
@@ -1967,6 +1975,21 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19671975
self._autonomy_trigger = AutonomyTrigger()
19681976
self._doom_loop_tracker = DoomLoopTracker(threshold=config.doom_loop_threshold)
19691977

1978+
# Initialize FileSnapshot for filesystem tracking (lazy import)
1979+
self._file_snapshot = None
1980+
self._snapshot_stack = [] # Stack of snapshot hashes for undo/redo
1981+
self._redo_stack = [] # Redo stack
1982+
if config.effective_track_changes:
1983+
try:
1984+
from ..snapshot import FileSnapshot
1985+
import os
1986+
self._file_snapshot = FileSnapshot(
1987+
project_path=os.getcwd(),
1988+
snapshot_dir=config.snapshot_dir,
1989+
)
1990+
except Exception as e:
1991+
logger.debug(f"FileSnapshot init failed (git may not be available): {e}")
1992+
19701993
# Wire ObservabilityHooks when observe=True (G-UNUSED-2 fix)
19711994
if config.observe:
19721995
from ..escalation.observability import ObservabilityHooks
@@ -1977,6 +2000,93 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19772000
# Wire level → approval bridge (G3 fix)
19782001
self._bridge_autonomy_level(config.level)
19792002

2003+
# ================================================================
2004+
# Filesystem tracking convenience methods (powered by FileSnapshot)
2005+
# ================================================================
2006+
2007+
def undo(self) -> bool:
2008+
"""Undo the last set of file changes.
2009+
2010+
Restores files to the state before the last autonomous iteration.
2011+
Requires ``autonomy=AutonomyConfig(track_changes=True)``.
2012+
2013+
Returns:
2014+
True if undo was successful, False if nothing to undo.
2015+
2016+
Example::
2017+
2018+
agent = Agent(autonomy="full_auto")
2019+
result = agent.start("Refactor utils.py")
2020+
agent.undo() # Restore original files
2021+
"""
2022+
if self._file_snapshot is None or not self._snapshot_stack:
2023+
return False
2024+
try:
2025+
target_hash = self._snapshot_stack.pop()
2026+
# Get current hash before restore (for redo)
2027+
current_hash = self._file_snapshot.get_current_hash()
2028+
if current_hash:
2029+
self._redo_stack.append(current_hash)
2030+
self._file_snapshot.restore(target_hash)
2031+
return True
2032+
except Exception as e:
2033+
logger.debug(f"Undo failed: {e}")
2034+
return False
2035+
2036+
def redo(self) -> bool:
2037+
"""Redo a previously undone set of file changes.
2038+
2039+
Re-applies file changes that were reverted by :meth:`undo`.
2040+
2041+
Returns:
2042+
True if redo was successful, False if nothing to redo.
2043+
"""
2044+
if self._file_snapshot is None or not self._redo_stack:
2045+
return False
2046+
try:
2047+
target_hash = self._redo_stack.pop()
2048+
current_hash = self._file_snapshot.get_current_hash()
2049+
if current_hash:
2050+
self._snapshot_stack.append(current_hash)
2051+
self._file_snapshot.restore(target_hash)
2052+
return True
2053+
except Exception as e:
2054+
logger.debug(f"Redo failed: {e}")
2055+
return False
2056+
2057+
def diff(self, from_hash: Optional[str] = None):
2058+
"""Get file diffs from autonomous execution.
2059+
2060+
Returns a list of :class:`FileDiff` objects showing what files
2061+
were modified, with additions/deletions counts.
2062+
2063+
Args:
2064+
from_hash: Base commit hash to diff from. If None, uses the
2065+
first snapshot (pre-autonomous state).
2066+
2067+
Returns:
2068+
List of FileDiff objects, or empty list if tracking not enabled.
2069+
2070+
Example::
2071+
2072+
agent = Agent(autonomy="full_auto")
2073+
result = agent.start("Refactor utils.py")
2074+
for d in agent.diff():
2075+
print(f"{d.status}: {d.path} (+{d.additions}/-{d.deletions})")
2076+
"""
2077+
if self._file_snapshot is None:
2078+
return []
2079+
try:
2080+
base = from_hash
2081+
if base is None and self._snapshot_stack:
2082+
base = self._snapshot_stack[0]
2083+
if base is None:
2084+
return []
2085+
return self._file_snapshot.diff(base)
2086+
except Exception as e:
2087+
logger.debug(f"Diff failed: {e}")
2088+
return []
2089+
19802090
def analyze_prompt(self, prompt: str) -> set:
19812091
"""Analyze prompt for autonomy signals.
19822092
@@ -2159,6 +2269,15 @@ def run_autonomous(
21592269
"Create agent with autonomy=True or autonomy={...}"
21602270
)
21612271

2272+
# Take initial snapshot before autonomous execution starts
2273+
if self._file_snapshot is not None:
2274+
try:
2275+
snap_info = self._file_snapshot.track(message="pre-autonomous")
2276+
self._snapshot_stack.append(snap_info.commit_hash)
2277+
self._redo_stack.clear()
2278+
except Exception as e:
2279+
logger.debug(f"Pre-autonomous snapshot failed: {e}")
2280+
21622281
start_time = time_module.time()
21632282
started_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
21642283
iterations = 0

src/praisonai-agents/praisonaiagents/agent/autonomy.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
class AutonomyConfig:
7878
"""Configuration for Agent autonomy features.
7979
80+
Autonomy is the safety center: doom loops, escalation, sandbox,
81+
filesystem tracking, and verification hooks all live here.
82+
8083
Attributes:
8184
enabled: Whether autonomy is enabled
8285
level: Autonomy level (suggest, auto_edit, full_auto)
@@ -87,6 +90,24 @@ class AutonomyConfig:
8790
completion_promise: Optional string that signals completion when wrapped in <promise>TEXT</promise>
8891
clear_context: Whether to clear chat history between iterations
8992
verification_hooks: List of VerificationHook instances for output verification
93+
track_changes: Whether to track filesystem changes via shadow git.
94+
Defaults to True when level="full_auto", False otherwise.
95+
When enabled, agent.undo()/redo()/diff() become available.
96+
sandbox: Optional SandboxConfig for execution isolation.
97+
When level="full_auto" and sandbox is None, subprocess sandbox is auto-enabled.
98+
snapshot_dir: Directory for snapshot storage. Defaults to ~/.praisonai/snapshots.
99+
100+
Usage::
101+
102+
# Simple — full_auto auto-enables tracking + sandbox
103+
Agent(autonomy="full_auto")
104+
105+
# Advanced — explicit config
106+
Agent(autonomy=AutonomyConfig(
107+
level="full_auto",
108+
track_changes=True,
109+
sandbox=SandboxConfig.native(writable_paths=["./src"]),
110+
))
90111
"""
91112
enabled: bool = True
92113
level: str = "suggest"
@@ -97,13 +118,31 @@ class AutonomyConfig:
97118
completion_promise: Optional[str] = None
98119
clear_context: bool = False
99120
verification_hooks: Optional[List[Any]] = None
121+
# Safety features — autonomy owns the safety model
122+
track_changes: Optional[bool] = None # None = auto (True for full_auto)
123+
sandbox: Optional[Any] = None # SandboxConfig (lazy import to avoid circular)
124+
snapshot_dir: Optional[str] = None # Defaults to ~/.praisonai/snapshots
100125

101126
def __post_init__(self):
102127
if self.level not in VALID_AUTONOMY_LEVELS:
103128
raise ValueError(
104129
f"Invalid autonomy level: {self.level!r}. "
105130
f"Must be one of {sorted(VALID_AUTONOMY_LEVELS)}"
106131
)
132+
# Auto-enable track_changes for full_auto if not explicitly set
133+
if self.track_changes is None:
134+
self.track_changes = (self.level == "full_auto")
135+
# Default snapshot directory
136+
if self.snapshot_dir is None:
137+
import os
138+
self.snapshot_dir = os.path.join(
139+
os.path.expanduser("~"), ".praisonai", "snapshots"
140+
)
141+
142+
@property
143+
def effective_track_changes(self) -> bool:
144+
"""Whether track_changes is effectively enabled."""
145+
return bool(self.track_changes)
107146

108147
@classmethod
109148
def from_dict(cls, data: Dict[str, Any]) -> "AutonomyConfig":
@@ -124,6 +163,9 @@ def from_dict(cls, data: Dict[str, Any]) -> "AutonomyConfig":
124163
completion_promise=data.get("completion_promise"),
125164
clear_context=data.get("clear_context", False),
126165
verification_hooks=data.get("verification_hooks"),
166+
track_changes=data.get("track_changes"),
167+
sandbox=data.get("sandbox"),
168+
snapshot_dir=data.get("snapshot_dir"),
127169
)
128170

129171

0 commit comments

Comments
 (0)