Compare commits

...
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison 76e390fc75 fix: harden batch-2 fixes per reviewer feedback
- runner.py: isolate child process groups (start_new_session) on POSIX so
  timeout killpg cannot target the suite runner; propagate TimeoutError
  metadata + exit_code=-1 in the streaming timeout path
- agent_tools.py: route run_sync via _wrapper_bridge (C7 gate + standalone
  install hint); surface delete apply_plan failures instead of masking them;
  drop unused cwd param on acp_execute_command; B904 chaining; RUF013 Optional
- execution_mixin.py: reset _server_started[port] in finally so a failed or
  exited server can be retried by later launch() calls
- __init__.py: rebind package global db to the callable _LazyDbModule proxy so
  repeated praisonaiagents.db access returns the proxy, not the raw module
- compression_demo.py: derive target_tokens from each ratio so iterations differ
- multi-provider-agent.py: rebind each Task.agent to its RouterAgent wrapper

Co-authored-by: Mervin Praison <[email protected]>
2026-07-22 11:13:58 +00:00
praisonai-triage-agent[bot]andMervin Praison 4447dacb03 fix(code): route run_sync via wrapper bridge to satisfy C7 import gate
agent_tools.py imported `from praisonai._async_bridge import run_sync`
directly, tripping the C7 allowlist enforcement in check_c7_imports.sh
(non-allowlisted wrapper import in praisonai_code/). Load it lazily via
_wrapper_bridge.import_wrapper_module so standalone praisonai-code installs
get a clear install hint and the import-direction gate stays green.

Co-authored-by: Mervin Praison <[email protected]>
2026-07-22 11:05:32 +00:00
6 changed files with 61 additions and 16 deletions

No files matched your search

+5 -1
View File
@@ -150,10 +150,14 @@ def main():
print(f"\nOriginal document length: {len(content)} characters")
chunks = [{"text": content, "metadata": {"source": doc_path}}]
target_tokens = 2000
# Estimate original tokens (~4 chars/token) so each ratio maps to a
# distinct token budget — compress() truncates to target_tokens.
original_tokens = max(1, len(content) // 4)
# Compress with different ratios
for ratio in [0.3, 0.5, 0.7]:
target_tokens = max(1, int(original_tokens * ratio))
compressor = ContextCompressor(verbose=False)
result = compressor.compress(
chunks,
@@ -144,6 +144,7 @@ def example_auto_agents_multi_provider():
# After agents are created, upgrade them to multi-model agents
multi_model_agents = []
wrapper_by_original = {}
for agent in auto_agents.agents:
# Convert regular agents to multi-model agents
multi_agent = RouterAgent(
@@ -155,9 +156,18 @@ def example_auto_agents_multi_provider():
models=["gpt-4o-mini", "gemini/gemini-1.5-flash", "claude-3-haiku-20240307", "gpt-4o-mini"],
routing_strategy="auto")
multi_model_agents.append(multi_agent)
wrapper_by_original[id(agent)] = multi_agent
# Update the agents in the AutoAgents instance
auto_agents.agents = multi_model_agents
# Rebind each task's agent to its wrapper — AutoAgents stores a direct agent
# reference on every Task and execution reads task.agent, so replacing only
# auto_agents.agents would leave the workflow bound to the original agents.
for task in getattr(auto_agents, "tasks", []) or []:
wrapper = wrapper_by_original.get(id(task.agent))
if wrapper is not None:
task.agent = wrapper
# Run the auto-generated workflow
results = auto_agents.start()
@@ -710,8 +710,14 @@ def _custom_handler(name, cache):
return mod.obs
if name == 'db':
import importlib
import sys
mod = importlib.import_module('.db', 'praisonaiagents')
# importlib registers the submodule as praisonaiagents.db in the package
# namespace, so plain attribute lookup would bypass __getattr__ and return
# the raw module on repeat access. Rebind the package global to the callable
# _LazyDbModule proxy so `praisonaiagents.db(...)` keeps working every time.
cache['db'] = mod.db # Return the _LazyDbModule instance, not the module
setattr(sys.modules['praisonaiagents'], 'db', mod.db)
return mod.db
if name == 'toolsets':
import importlib
@@ -1573,6 +1573,11 @@ Write the complete compiled report:"""
except Exception as e:
logging.error(f"Error starting server: {str(e)}", exc_info=True)
print(f"❌ Error starting server: {str(e)}")
finally:
# Clear the started marker so a later launch() on this port
# can retry after a bind/startup failure or server exit.
with _server_lock:
_server_started[port] = False
# Run server in a background thread
server_thread = threading.Thread(target=run_server, daemon=True)
@@ -49,10 +49,10 @@ def _sanitize_filepath(filepath: str, workspace: Optional[str] = None) -> str:
ws_resolved = Path(os.path.realpath(workspace))
try:
resolved.relative_to(ws_resolved)
except ValueError:
except ValueError as exc:
raise ValueError(
f"Path {filepath!r} resolves outside workspace {workspace!r}"
)
) from exc
return normalized
@@ -88,8 +88,8 @@ def _sanitize_command(command: str) -> str:
def create_agent_centric_tools(
runtime: "InteractiveRuntime",
router: "CodeIntelligenceRouter" = None,
orchestrator: "ActionOrchestrator" = None
router: "Optional[CodeIntelligenceRouter]" = None,
orchestrator: "Optional[ActionOrchestrator]" = None
) -> List[Callable]:
"""
Create tools that route through LSP/ACP for agent-centric architecture.
@@ -110,8 +110,12 @@ def create_agent_centric_tools(
if orchestrator is None:
orchestrator = ActionOrchestrator(runtime)
# Helper to run async functions synchronously using the shared bridge
from praisonai._async_bridge import run_sync
# Helper to run async functions synchronously using the shared bridge.
# Loaded via the wrapper bridge so standalone ``praisonai-code`` installs
# fail with a clear install hint instead of an ImportError, and so the C7
# import-direction gate stays satisfied (no direct wrapper import here).
from praisonai_code._wrapper_bridge import import_wrapper_module
run_sync = import_wrapper_module("praisonai._async_bridge").run_sync
# =========================================================================
# ACP-Powered File Tools
@@ -291,7 +295,15 @@ def create_agent_centric_tools(
"requires_approval": True
})
result = await orchestrator.apply_plan(plan)
apply_result = await orchestrator.apply_plan(plan)
if not apply_result.success:
return json.dumps({
"success": False,
"file_deleted": filepath,
"verified": False,
"error": apply_result.error
})
result = await orchestrator.verify_plan(plan)
return json.dumps({
@@ -302,14 +314,16 @@ def create_agent_centric_tools(
return run_sync(_delete())
def acp_execute_command(command: str, cwd: str = None) -> str:
def acp_execute_command(command: str) -> str:
"""
Execute a shell command through ACP with tracking.
Commands run in the configured workspace directory. Per-call working
directories are not currently supported.
Args:
command: The command to execute
cwd: Working directory (optional)
Returns:
JSON string with command output
"""
@@ -393,7 +407,7 @@ def create_agent_centric_tools(
return run_sync(_list())
def lsp_find_definition(symbol: str, file_path: str = None) -> str:
def lsp_find_definition(symbol: str, file_path: Optional[str] = None) -> str:
"""
Find where a symbol is defined using LSP.
@@ -421,7 +435,7 @@ def create_agent_centric_tools(
return run_sync(_find())
def lsp_find_references(symbol: str, file_path: str = None) -> str:
def lsp_find_references(symbol: str, file_path: Optional[str] = None) -> str:
"""
Find all references to a symbol using LSP.
@@ -449,7 +463,7 @@ def create_agent_centric_tools(
return run_sync(_find())
def lsp_get_diagnostics(file_path: str = None) -> str:
def lsp_get_diagnostics(file_path: Optional[str] = None) -> str:
"""
Get diagnostics (errors, warnings) for a file using LSP.
@@ -193,6 +193,7 @@ class ScriptRunner:
encoding="utf-8",
errors="replace",
bufsize=1,
start_new_session=(os.name != "nt"),
)
import selectors
@@ -211,6 +212,9 @@ class ScriptRunner:
except subprocess.TimeoutExpired:
process.kill()
status = "timeout"
error_type = "TimeoutError"
error_message = f"Exceeded {timeout}s timeout"
exit_code = -1
break
events = sel.select(timeout=min(0.1, remaining))
@@ -236,7 +240,8 @@ class ScriptRunner:
if remaining_stderr:
stderr_data.append(remaining_stderr)
exit_code = process.returncode or 0
if status != "timeout":
exit_code = process.returncode or 0
else:
# Capture output without streaming (Popen + communicate so we
@@ -250,6 +255,7 @@ class ScriptRunner:
errors="replace",
env=env,
cwd=cwd,
start_new_session=(os.name != "nt"),
)
try:
stdout, stderr = process.communicate(timeout=timeout)