fix: make interactive chat/code resident in praisonai-code - #3818
fix: make interactive chat/code resident in praisonai-code#3818praisonai-triage-agent[bot] wants to merge 2 commits into
Conversation
|
@coderabbitai review |
|
/review |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
✅ Action performedReview finished.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe interactive TUI and session core now run from ChangesResident interactive CLI
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant CLI
participant AsyncTUI
participant InteractiveCore
participant AgentRuntime
CLI->>AsyncTUI: start interactive or single-prompt session
AsyncTUI->>InteractiveCore: submit prompt and session context
InteractiveCore->>AgentRuntime: execute prompt with tools and project context
AgentRuntime-->>InteractiveCore: return assistant response
InteractiveCore-->>AsyncTUI: emit completion and response
AsyncTUI-->>CLI: render session or print response
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThe PR relocates the interactive TUI into
Confidence Score: 3/5The PR is not yet safe to merge because standalone The attempted fix places the PLAN backend in a global registry, but the resident TUI constructs a separate Agent without that approval configuration, leaving its permission mode unset and allowing write, edit, and shell tools to reach ConsoleBackend approval. Files Needing Attention: src/praisonai-code/praisonai_code/cli/commands/code.py; src/praisonai-code/praisonai_code/cli/interactive/async_tui.py
|
| Filename | Overview |
|---|---|
| src/praisonai-code/praisonai_code/cli/commands/code.py | Adds standalone resident code dispatch and flag mapping, but its global PLAN registration does not reach the executing Agent. |
| src/praisonai-code/praisonai_code/cli/interactive/async_tui.py | Relocates the full resident TUI and honors ACP/LSP configuration, while Agent construction remains disconnected from the registered approval policy. |
| src/praisonai-code/praisonai_code/cli/commands/chat.py | Removes the wrapper-only gate and dispatches interactive chat to the resident TUI. |
| src/praisonai/praisonai/cli/interactive/async_tui.py | Replaces the wrapper implementation with an identity-preserving compatibility shim. |
| scripts/check_c7_imports.sh | Extends the package-boundary import check to the newly resident interactive modules. |
Sequence Diagram
sequenceDiagram
participant User
participant Code as code command
participant Registry as Approval registry
participant TUI as Resident AsyncTUI
participant Agent
User->>Code: code --plan
Code->>Registry: set_backend(PLAN)
Code->>TUI: "AsyncTUIConfig(plan_mode=true)"
TUI->>Agent: Agent(...), no approval argument
Agent->>Agent: Create separate ConsoleBackend
Note over Registry,Agent: Registered PLAN backend is not used by Agent
Agent-->>User: Mutating tool follows ordinary approval path
Reviews (3): Last reviewed commit: "fix: honor plan/no-acp/no-lsp in standal..." | Re-trigger Greptile
|
@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding. Phase 1: Review per AGENTS.md
Phase 2: FIX Valid Issues Phase 3: Final Verdict |
|
Claude finished @MervinPraison's task —— View job Final Architecture Review — PR #3818 ✅ Fixes pushedVerdict: Architecture is sound; the three reviewer-flagged defects on the new standalone Phase 1 — Architecture (AGENTS.md)
Phase 2 — Fixes implemented
Verification
Files modified: All blocking issues resolved — recommend merge once CI is green. |
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
scripts/check_c7_imports.sh (1)
16-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd an existence check so a missing hot-path file fails the gate.
grepreturns 2 when the file does not exist.2>/dev/nullhides the error, and theiftreats the non-zero status as "no match". The gate then passes silently. This risk grows now that the list includes the newly relocatedinteractive/async_tui.pyandinteractive/core.py; a later move or rename would disable the check without any signal.🛡️ Proposed fix to fail on missing files
do + if [ ! -f "$f" ]; then + echo "FAIL: hot-path file not found: $f" + exit 1 + fi if grep -E "$HOT_PATH_RE" "$f" 2>/dev/null; then echo "FAIL: module-level wrapper import in $f" exit 1 fi done🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check_c7_imports.sh` around lines 16 - 29, Update the file loop in the hot-path import check to explicitly verify each path exists before running grep. If any listed file is missing, print a failure message and exit nonzero; otherwise preserve the existing HOT_PATH_RE matching behavior and failure handling.
🧹 Nitpick comments (3)
src/praisonai-code/praisonai_code/cli/interactive/async_tui.py (2)
400-402: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueChain the original
ImportError.
raise RuntimeError(...)inside anexceptblock hides the original traceback. Ruff reports this as B904.♻️ Proposed fix
except ImportError as e: logger.error(f"Failed to import praisonaiagents: {e}") - raise RuntimeError(f"Failed to import praisonaiagents: {e}") + raise RuntimeError(f"Failed to import praisonaiagents: {e}") from e🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/praisonai-code/praisonai_code/cli/interactive/async_tui.py` around lines 400 - 402, Update the ImportError handler in the interactive TUI import flow to explicitly chain the caught exception when raising RuntimeError, preserving the original traceback while retaining the existing log message and error context.Source: Linters/SAST tools
280-288: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winConstrain
@fileexpansion to the workspace.
os.path.join(workspace, match)accepts a traversal token.@../../secrets.envresolves outside the workspace and its contents are injected into the prompt. The surrounding comments state that@fileexpansion must not read outside the workspace, so enforce that with a containment check.🛡️ Proposed fix
+ workspace_root = os.path.realpath(workspace) file_contents = [] for match in matches: - file_path = os.path.join(workspace, match) + file_path = os.path.realpath(os.path.join(workspace, match)) + if os.path.commonpath([workspace_root, file_path]) != workspace_root: + file_contents.append(f"\n[Skipped {match}: outside the workspace]\n") + continue if os.path.isfile(file_path):🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/praisonai-code/praisonai_code/cli/interactive/async_tui.py` around lines 280 - 288, Validate the resolved path in the `@file` expansion loop before checking or reading it, ensuring it remains contained within workspace and rejecting traversal or absolute paths that escape it. Apply this check around file_path and preserve the existing file reading and error behavior for safe matches.Source: Linters/SAST tools
src/praisonai-code/praisonai_code/cli/interactive/core.py (1)
122-133: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winIterate over a copy of the handler list in
_emit.A handler can call the unsubscribe function returned by
subscribe. That mutatesself._event_handlerswhile_emititerates it, so later handlers are skipped for that event.♻️ Proposed fix
- for handler in self._event_handlers: + for handler in list(self._event_handlers):🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/praisonai-code/praisonai_code/cli/interactive/core.py` around lines 122 - 133, Update `_emit` to iterate over a snapshot or shallow copy of `self._event_handlers` rather than the mutable list directly, so handlers unsubscribing during callback execution do not cause subsequent handlers to be skipped. Preserve the existing filtering and exception-logging behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/praisonai-code/praisonai_code/cli/commands/code.py`:
- Around line 342-349: Update _run_resident_code and its AsyncTUIConfig
construction to apply args.agent_approval to the approval registry, set
plan_mode, and forward continue-session state from args.resume_session == "last"
plus args.autonomy. Correct the nearby docstring to remove the claim that
no-acp/no-lsp are mapped. Apply these changes at
src/praisonai-code/praisonai_code/cli/commands/code.py lines 342-349 (anchor)
and 320-348 (sibling); both sites require updates as described.
- Around line 320-348: Update _run_resident_code and its docstring to accurately
describe and implement the resident code options. Forward no-acp and no-lsp
through AsyncTUIConfig and ensure AsyncTUI._load_tools honors them, forward
args.autonomy to autonomy_mode, and translate code_main’s resume_session='last'
into the session value expected by the TUI so --continue resumes the latest
session.
- Around line 304-306: Fix the wrapper-absent branch in code_main so it no
longer references the undefined compact variable: either add a matching
--compact parameter to code_main and preserve passing it to _run_resident_code,
or remove the compact argument at that call site while maintaining the
standalone code flow.
In `@src/praisonai-code/praisonai_code/cli/interactive/async_tui.py`:
- Around line 1151-1201: Protect the stdout/stderr redirection and root logger
handler setup in _execute_prompt with a shared synchronization guard so
overlapping or abandoned turns cannot alter another turn’s capture. Hold the
guard for the entire agent execution and cleanup, ensuring each turn restores
only the streams it installed and removes only its own log handler before
releasing it.
- Around line 1841-1843: Update InteractiveTUI.run_single to apply the same
`@file` expansion used by _execute_in_background before calling _execute_prompt,
preserving the existing one-shot return behavior for both chat.py and code.py
entry points.
- Around line 1500-1525: Replace the prompt-id side tables used by
`_queue_or_execute` with queue entries containing the prompt,
`skip_file_mentions`, and `read_only` flags. Update `_execute_in_background` and
the `/queue` handling around `self._prompt_queue` to unpack and propagate all
three values when draining queued prompts, then remove the obsolete
metadata-table reads and writes.
- Around line 1410-1439: Update the completion logic around _processing and the
prompt-queue drain so _processing remains true while deciding whether to start
the next queued turn. Only set _processing to false after confirming the queue
is empty; keep it true before invoking _execute_in_background for a queued
prompt so handle_enter continues queueing new input.
- Around line 1533-1554: Update the runtime startup block around
self._start_runtime() so failure to import or use the shared run_sync_or_offload
bridge falls back to executing self._start_runtime() on a local event loop.
Preserve the existing Runtime unavailable message only when the fallback startup
also fails, ensuring standalone praisonai-code installations still start ACP/LSP
servers.
- Around line 541-553: Update _update_output to schedule the Buffer.set_document
operation through the prompt_toolkit event loop using loop.call_soon_threadsafe,
rather than modifying the document directly from the background thread. Keep the
existing formatted text and cursor position behavior, and ensure _app.invalidate
remains the final UI refresh after the scheduled update.
- Around line 326-330: The read-only filtering in the interactive TUI must also
exclude ACP/LSP tools with write or command capabilities, not just names in
self._WRITE_TOOL_NAMES. Update the filtering around the tools list to use the
established ACP read-only blocking or capability metadata, ensuring
acp_create_file, acp_edit_file, acp_delete_file, and acp_execute_command cannot
be exposed in review mode without mutating the tool objects.
- Around line 737-749: Update the command handling for “clear” and “new” to also
reset the cached _agent and _review_agent instances after clearing conversation
state. Ensure subsequent interactions rebuild fresh agents without stale
praisonaiagents.Agent history, while preserving the existing system messages and
session ID behavior.
In `@src/praisonai-code/praisonai_code/cli/interactive/core.py`:
- Around line 349-350: Update async method _execute_prompt so the synchronous
agent.chat call does not block the event loop: prefer awaiting the Agent async
API achat when available, otherwise offload agent.chat(context + message) to a
worker thread. Preserve the existing response handling and prompt behavior.
- Around line 638-653: Update the import flow around session_store.get_or_create
and the message-restoration loop to clear or replace the existing session
history before adding imported messages. Preserve the imported metadata and
session ID behavior while ensuring repeated imports do not append duplicate
messages.
In `@src/praisonai/praisonai/cli/interactive/async_tui.py`:
- Around line 9-17: Move the duplicated shim logic from async_tui.py (lines
9-17), config.py (lines 9-17), core.py (lines 8-16), and events.py (lines 8-16)
into a shared helper at src/praisonai/praisonai/cli/_shim.py. Update each shim
to invoke that helper while preserving module identity replacement and
parent-package attribute rebinding; all four sites require the same direct
change.
---
Outside diff comments:
In `@scripts/check_c7_imports.sh`:
- Around line 16-29: Update the file loop in the hot-path import check to
explicitly verify each path exists before running grep. If any listed file is
missing, print a failure message and exit nonzero; otherwise preserve the
existing HOT_PATH_RE matching behavior and failure handling.
---
Nitpick comments:
In `@src/praisonai-code/praisonai_code/cli/interactive/async_tui.py`:
- Around line 400-402: Update the ImportError handler in the interactive TUI
import flow to explicitly chain the caught exception when raising RuntimeError,
preserving the original traceback while retaining the existing log message and
error context.
- Around line 280-288: Validate the resolved path in the `@file` expansion loop
before checking or reading it, ensuring it remains contained within workspace
and rejecting traversal or absolute paths that escape it. Apply this check
around file_path and preserve the existing file reading and error behavior for
safe matches.
In `@src/praisonai-code/praisonai_code/cli/interactive/core.py`:
- Around line 122-133: Update `_emit` to iterate over a snapshot or shallow copy
of `self._event_handlers` rather than the mutable list directly, so handlers
unsubscribing during callback execution do not cause subsequent handlers to be
skipped. Preserve the existing filtering and exception-logging behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4a1a9c84-b4ff-4e5c-8bb6-07c88f3266fb
📒 Files selected for processing (11)
scripts/check_c7_imports.shsrc/praisonai-code/praisonai_code/cli/commands/chat.pysrc/praisonai-code/praisonai_code/cli/commands/code.pysrc/praisonai-code/praisonai_code/cli/interactive/async_tui.pysrc/praisonai-code/praisonai_code/cli/interactive/core.pysrc/praisonai-code/tests/unit/test_plan_mode_flag.pysrc/praisonai/praisonai/cli/interactive/__init__.pysrc/praisonai/praisonai/cli/interactive/async_tui.pysrc/praisonai/praisonai/cli/interactive/config.pysrc/praisonai/praisonai/cli/interactive/core.pysrc/praisonai/praisonai/cli/interactive/events.py
| if not wrapper_available(): | ||
| typer.echo( | ||
| "Error: code requires the praisonai wrapper. " | ||
| "Install the full wrapper: pip install praisonai", | ||
| err=True, | ||
| ) | ||
| raise typer.Exit(1) | ||
| _run_resident_code(prompt, args, compact=compact, session_id=session_id) | ||
| return |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
compact is undefined and breaks the standalone code path.
code_main has no compact parameter (see the signature at Lines 19-45). chat_main defines --compact, but code_main does not. Every wrapper-absent invocation of praisonai code raises NameError: name 'compact' is not defined, so the standalone flow this PR enables does not run.
Add a --compact option to code_main for parity with chat, or drop the argument at the call site.
🐛 Proposed fix (drop the argument)
if not wrapper_available():
- _run_resident_code(prompt, args, compact=compact, session_id=session_id)
+ _run_resident_code(prompt, args, session_id=session_id)
return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if not wrapper_available(): | |
| typer.echo( | |
| "Error: code requires the praisonai wrapper. " | |
| "Install the full wrapper: pip install praisonai", | |
| err=True, | |
| ) | |
| raise typer.Exit(1) | |
| _run_resident_code(prompt, args, compact=compact, session_id=session_id) | |
| return | |
| if not wrapper_available(): | |
| _run_resident_code(prompt, args, session_id=session_id) | |
| return |
🧰 Tools
🪛 Ruff (0.16.1)
[error] 305-305: Undefined name compact
(F821)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/commands/code.py` around lines 304 -
306, Fix the wrapper-absent branch in code_main so it no longer references the
undefined compact variable: either add a matching --compact parameter to
code_main and preserve passing it to _run_resident_code, or remove the compact
argument at that call site while maintaining the standalone code flow.
Source: Linters/SAST tools
| def _run_resident_code(prompt, args, *, compact=False, session_id=None): | ||
| """Run interactive/one-shot ``code`` on the resident split-pane TUI. | ||
|
|
||
| Used when the ``praisonai`` wrapper is absent so the CLI-first package | ||
| delivers its own flagship interactive coding session with no wrapper | ||
| dependency. Mirrors the ``chat`` command's resident path, mapping the code | ||
| session's args (model/workspace/no-acp/no-lsp) onto ``AsyncTUIConfig``. | ||
| """ | ||
| import os | ||
|
|
||
| from praisonai_code.cli.interactive.async_tui import AsyncTUI, AsyncTUIConfig | ||
|
|
||
| model = getattr(args, "llm", None) | ||
| try: | ||
| from ..configuration.model_resolver import resolve_default_model | ||
|
|
||
| resolved_model = resolve_default_model(model) | ||
| except Exception: | ||
| from praisonai_code.llm.env import DEFAULT_FALLBACK_MODEL | ||
|
|
||
| resolved_model = model or DEFAULT_FALLBACK_MODEL | ||
|
|
||
| tui_config = AsyncTUIConfig( | ||
| model=resolved_model, | ||
| show_logo=not compact, | ||
| show_status_bar=not compact, | ||
| session_id=session_id, | ||
| workspace=os.environ.get("PRAISONAI_WORKSPACE") or os.getcwd(), | ||
| debug=getattr(args, "verbose", False), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The docstring overstates the mapping, and --continue is dropped.
Two gaps in the resident path:
- The docstring states that the function maps
no-acp/no-lspontoAsyncTUIConfig.AsyncTUIConfighas no ACP or LSP field, andAsyncTUI._load_toolsalways requests thebasic,acp, andlspgroups.praisonai code --no-acp --no-lsptherefore still loads those tools. Correct the docstring, or add the fields and honor them. code_mainencodes--continueasargs.resume_session = 'last'at Line 259._run_resident_codeonly forwards the separatesession_idargument, so--continuestarts a fresh session without any message.
Also --autonomy/--no-autonomy is not forwarded, although AsyncTUIConfig.autonomy_mode exists and chat.py Line 243 sets it.
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 337-337: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/commands/code.py` around lines 320 -
348, Update _run_resident_code and its docstring to accurately describe and
implement the resident code options. Forward no-acp and no-lsp through
AsyncTUIConfig and ensure AsyncTUI._load_tools honors them, forward
args.autonomy to autonomy_mode, and translate code_main’s resume_session='last'
into the session value expected by the TUI so --continue resumes the latest
session.
| tui_config = AsyncTUIConfig( | ||
| model=resolved_model, | ||
| show_logo=not compact, | ||
| show_status_bar=not compact, | ||
| session_id=session_id, | ||
| workspace=os.environ.get("PRAISONAI_WORKSPACE") or os.getcwd(), | ||
| debug=getattr(args, "verbose", False), | ||
| ) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
_run_resident_code forwards only a subset of the code options. The resident path builds AsyncTUIConfig from six values and ignores the rest of the resolved session state, so several documented flags become silent no-ops without the wrapper. --plan is the severe case: its resolved approval config is discarded, so the read-only guarantee is lost.
src/praisonai-code/praisonai_code/cli/commands/code.py#L342-L349: applyargs.agent_approvalto the approval registry and setplan_modeonAsyncTUIConfig, so--planstill denies mutating tools.src/praisonai-code/praisonai_code/cli/commands/code.py#L320-L348: forward--continue(args.resume_session == 'last') and--autonomyintoAsyncTUIConfig, and correct the docstring claim thatno-acp/no-lspare mapped.
📍 Affects 1 file
src/praisonai-code/praisonai_code/cli/commands/code.py#L342-L349(this comment)src/praisonai-code/praisonai_code/cli/commands/code.py#L320-L348
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/commands/code.py` around lines 342 -
349, Update _run_resident_code and its AsyncTUIConfig construction to apply
args.agent_approval to the approval registry, set plan_mode, and forward
continue-session state from args.resume_session == "last" plus args.autonomy.
Correct the nearby docstring to remove the claim that no-acp/no-lsp are mapped.
Apply these changes at src/praisonai-code/praisonai_code/cli/commands/code.py
lines 342-349 (anchor) and 320-348 (sibling); both sites require updates as
described.
| if read_only and tools: | ||
| tools = [ | ||
| t for t in tools | ||
| if getattr(t, "__name__", "") not in self._WRITE_TOOL_NAMES | ||
| ] |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# List the tool callables each group can contribute, to compare against _WRITE_TOOL_NAMES.
rg -n -C 3 'def _load_acp_tools|def _load_lsp_tools|def _load_edit_tools|def _load_basic_tools' \
src/praisonai-code/praisonai_code/cli/features/interactive_tools.pyRepository: MervinPraison/PraisonAI
Length of output: 1015
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== async_tui relevant section =="
sed -n '1,80p' src/praisonai-code/praisonai_code/cli/interactive/async_tui.py
sed -n '260,360p' src/praisonai-code/praisonai_code/cli/interactive/async_tui.py
echo
echo "== interactive_tools loader section =="
sed -n '180,500p' src/praisonai-code/praisonai_code/cli/features/interactive_tools.pyRepository: MervinPraison/PraisonAI
Length of output: 19573
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== _load_tools dispatcher =="
rg -n -C 8 'def _load_tools|TOOL_GROUPS|tool_groups' src/praisonai-code/praisonai_code/cli/interactive/async_tui.py
echo
echo "== InteractiveRuntime and agent_tools outlines =="
fd -a 'interactive_runtime.py|agent_tools.py' src/praisonai-code/praisonai_code/cli | sed 's#^\./##'
for f in $(fd 'interactive_runtime.py|agent_tools.py' src/praisonai-code/praisonai_code/cli); do
echo "--- $f ---"
ast-grep outline "$f" || true
echo
done
echo
echo "== search ACP/LSP write-like names =="
rg -n 'acp_|lsp_|write|edit|patch|command|run|shell|bash|delete|move|create' \
src/praisonai-code/praisonai_code/cli/features src/praisonai-code/praisonai_code/cli/interactive/async_tui.py \
| head -n 250Repository: MervinPraison/PraisonAI
Length of output: 33148
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== agent_tool creation and tool function names =="
sed -n '130,260p' src/praisonai-code/praisonai_code/cli/features/agent_tools.py
sed -n '260,420p' src/praisonai-code/praisonai_code/cli/features/agent_tools.py
sed -n '580,660p' src/praisonai-code/praisonai_code/cli/features/agent_tools.py
echo
echo "== _load_lsp_tools tail =="
sed -n '488,510p' src/praisonai-code/praisonai_code/cli/features/interactive_tools.py
echo
echo "== deterministic set membership probe for denylist =="
python3 - <<'PY'
frozen = frozenset({
"write_file", "edit_file", "apply_patch", "create_file", "delete_file",
"move_file", "execute_command", "run_command", "shell", "bash",
})
candidate_names = [
"write_file", "edit_file", "apply_patch", # expected removed
"acp_create_plan", "acp_apply_plan", "code_review", "read_file", "list_files",
"lsp_find_defs", "lsp_find_refs", "lsp_read_file", "lsp_write_file",
"lsp_edit_file", "lsp_create_file", "lsp_delete_file", "execute_command",
"run_command", "shell", "bash", "create_file", "delete_file", "move_file",
]
kept_by_current_filter = [n for n in candidate_names if n not in frozen]
print("kept_by_current_filter=", kept_by_current_filter)
print("present_writelike_kept_by_current_filter=", [n for n in kept_by_current_filter if any(w in n.lower() for w in ("write", "edit", "patch", "create", "delete", "move"))])
PYRepository: MervinPraison/PraisonAI
Length of output: 14723
Avoid mutating ACP/LSP tools in read-only review mode.
_WRITE_TOOL_NAMES only removes the core write_file, edit_file, apply_patch, and command tool names. ACP tools such as acp_create_file, acp_edit_file, acp_delete_file, and acp_execute_command are filtered by ACP read-only blocking, but review mode should also exclude them or otherwise guarantee full capability-level enforcement instead of relying on __name__.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/interactive/async_tui.py` around lines
326 - 330, The read-only filtering in the interactive TUI must also exclude
ACP/LSP tools with write or command capabilities, not just names in
self._WRITE_TOOL_NAMES. Update the filtering around the tools list to use the
established ACP read-only blocking or capability metadata, ensuring
acp_create_file, acp_edit_file, acp_delete_file, and acp_execute_command cannot
be exposed in review mode without mutating the tool objects.
| def _update_output(self): | ||
| """Update the output buffer and refresh UI.""" | ||
| if self._output_buffer: | ||
| new_text = self._format_output() | ||
| self._output_buffer.set_document( | ||
| self._output_buffer.document.__class__( | ||
| text=new_text, | ||
| cursor_position=len(new_text) # Cursor at end for auto-scroll | ||
| ), | ||
| bypass_readonly=True | ||
| ) | ||
| if self._app: | ||
| self._app.invalidate() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
prompt_toolkit Buffer thread safety update from background thread
💡 Result:
In prompt_toolkit, Buffer objects and the core application state are not inherently thread-safe [1]. You must never modify these objects directly from a background thread, as this can lead to race conditions, crashes, or inconsistencies [2][3]. To update a Buffer or the UI from a background thread, you must communicate with the event loop running the prompt_toolkit application [1][4]. The standard and safe approach is to use loop.call_soon_threadsafe(callback, *args) [5][6], where the callback performs the actual modifications to the Buffer. Key patterns for background-to-UI communication include: 1. Thread-safe Event Loop Scheduling: Use the running event loop's call_soon_threadsafe method to schedule your update logic [5][6]. For example: get_app.loop.call_soon_threadsafe(lambda: buffer.text = "new content") 2. Invalidating the UI: If you have modified state (such as the prompt text or buffer content) and need the UI to reflect these changes, you must call Application.invalidate [7][4]. This method is explicitly thread-safe and signals the event loop to repaint the application [7]. 3. Asynchronous Tasks: For long-running operations, rather than raw threading, it is recommended to use async/await patterns within the main event loop. You can schedule background tasks using Application.create_background_task [1][4], which ensures the task is properly managed and cancelled if the application terminates [1]. If you are using threaded components like ThreadedHistory or ThreadedCompleter, ensure they are correctly integrated; these components are designed to handle the rendezvous with the main event loop internally [8][3]. Always avoid direct access to prompt_toolkit UI objects from secondary threads [1].
Citations:
- 1: https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html
- 2: Assert fail in document.py when typing while large history loading prompt-toolkit/python-prompt-toolkit#1158
- 3: https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG
- 4: asyncio concepts and prompt toolkit for UI updates prompt-toolkit/python-prompt-toolkit#1847
- 5: https://github.com/prompt-toolkit/python-prompt-toolkit/blob/3.0.29/docs/pages/upgrading/3.0.rst
- 6: https://python-prompt-toolkit.readthedocs.io/en/stable/pages/upgrading/3.0.html
- 7: support updating prompt token in background prompt-toolkit/python-prompt-toolkit#1236
- 8: ThreadedHistory: invoke Buffer.new_history_item() on event loop from background thread prompt-toolkit/python-prompt-toolkit#1159
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the relevant async_tui.py section plus call paths.
file="src/praisonai-code/praisonai_code/cli/interactive/async_tui.py"
echo "== file exists and size =="
test -f "$file" && wc -l "$file"
echo "== outline around relevant symbols =="
ast-grep outline "$file" --match AsyncTUI --view expanded || true
echo "== relevant async_tui.py sections =="
sed -n '440,570p' "$file" | cat -n -v
echo "== call paths for _execute_in_background and _update_output =="
grep -n "_execute_in_background\|_update_output" "$file" || trueRepository: MervinPraison/PraisonAI
Length of output: 9421
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="src/praisonai-code/praisonai_code/cli/interactive/async_tui.py"
echo "== _execute_in_background section =="
sed -n '1290,1455p' "$file" | cat -n -v
echo "== prompt application construction and output buffer sections =="
rg -n "Application|key_bindings=|_output_buffer|Buffer|invalidate|loop" "$file" || true
sed -n '1535,1625p' "$file" | cat -n -v
echo "== background thread timing and imports =="
rg -n "Thread|target=|sleep\\(|time\\.sleep|_execute_in_background\\(" "$file" -C 2 || true
echo "== deterministic probe: current worker path reaches _update_output =="
python3 - <<'PY'
import ast
from pathlib import Path
path = Path("src/praisonai-code/praisonai_code/cli/interactive/async_tui.py")
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)) and node.name == "_execute_in_background":
print("line", node.lineno)
print(ast.get_source_segment(path, node))
PYRepository: MervinPraison/PraisonAI
Length of output: 17337
Schedule _update_output via the prompt_toolkit event loop.
_execute_in_background runs as a daemon thread and calls _update_output while LLM execution is in progress. Application.invalidate is thread-safe, but Buffer.set_document modifies prompt_toolkit application state and can race with rendering. Move the document update into loop.call_soon_threadsafe(...) and keep invalidate() as the final UI refresh.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/interactive/async_tui.py` around lines
541 - 553, Update _update_output to schedule the Buffer.set_document operation
through the prompt_toolkit event loop using loop.call_soon_threadsafe, rather
than modifying the document directly from the background thread. Keep the
existing formatted text and cursor position behavior, and ensure _app.invalidate
remains the final UI refresh after the scheduled update.
| # Start runtime (ACP/LSP servers) before TUI | ||
| try: | ||
| # Use the shared async bridge instead of a throwaway loop so | ||
| # per-loop connection pools are preserved for later turns. This is a | ||
| # wrapper-only optimisation; the surrounding except degrades to | ||
| # running without the shared bridge when it is unavailable. | ||
| from praisonai_code._wrapper_bridge import import_wrapper_module | ||
| run_sync_or_offload = import_wrapper_module( | ||
| "praisonai._async_bridge" | ||
| ).run_sync_or_offload | ||
| run_sync_or_offload( | ||
| self._start_runtime(), | ||
| thread_name="praisonai-tui-runtime", | ||
| ) | ||
| # Runtime status logged to debug file only (not shown in UI) | ||
| # Tools are available silently when runtime is ready | ||
| except Exception as e: | ||
| # Continue without runtime | ||
| self.messages.append(ChatMessage( | ||
| role="system", | ||
| content=f"Runtime unavailable: {e}" | ||
| )) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Without the wrapper, the ACP/LSP runtime never starts.
import_wrapper_module("praisonai._async_bridge") raises when the praisonai wrapper is absent. The except Exception branch only appends a "Runtime unavailable" message; it never awaits self._start_runtime(). The wrapper-absent configuration is exactly the one this PR targets, so a standalone praisonai-code install starts the TUI with no ACP/LSP servers.
Fall back to a local event loop when the shared bridge is unavailable.
🐛 Proposed fix
# Start runtime (ACP/LSP servers) before TUI
try:
from praisonai_code._wrapper_bridge import import_wrapper_module
run_sync_or_offload = import_wrapper_module(
"praisonai._async_bridge"
).run_sync_or_offload
run_sync_or_offload(
self._start_runtime(),
thread_name="praisonai-tui-runtime",
)
- # Runtime status logged to debug file only (not shown in UI)
- # Tools are available silently when runtime is ready
- except Exception as e:
+ except ImportError:
+ # Resident path: no wrapper bridge, so run the coroutine directly.
+ try:
+ asyncio.run(self._start_runtime())
+ except Exception as e:
+ self.messages.append(ChatMessage(
+ role="system", content=f"Runtime unavailable: {e}"
+ ))
+ except Exception as e:
# Continue without runtime
self.messages.append(ChatMessage(
role="system",
content=f"Runtime unavailable: {e}"
))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Start runtime (ACP/LSP servers) before TUI | |
| try: | |
| # Use the shared async bridge instead of a throwaway loop so | |
| # per-loop connection pools are preserved for later turns. This is a | |
| # wrapper-only optimisation; the surrounding except degrades to | |
| # running without the shared bridge when it is unavailable. | |
| from praisonai_code._wrapper_bridge import import_wrapper_module | |
| run_sync_or_offload = import_wrapper_module( | |
| "praisonai._async_bridge" | |
| ).run_sync_or_offload | |
| run_sync_or_offload( | |
| self._start_runtime(), | |
| thread_name="praisonai-tui-runtime", | |
| ) | |
| # Runtime status logged to debug file only (not shown in UI) | |
| # Tools are available silently when runtime is ready | |
| except Exception as e: | |
| # Continue without runtime | |
| self.messages.append(ChatMessage( | |
| role="system", | |
| content=f"Runtime unavailable: {e}" | |
| )) | |
| # Start runtime (ACP/LSP servers) before TUI | |
| try: | |
| # Use the shared async bridge instead of a throwaway loop so | |
| # per-loop connection pools are preserved for later turns. This is a | |
| # wrapper-only optimisation; the surrounding except degrades to | |
| # running without the shared bridge when it is unavailable. | |
| from praisonai_code._wrapper_bridge import import_wrapper_module | |
| run_sync_or_offload = import_wrapper_module( | |
| "praisonai._async_bridge" | |
| ).run_sync_or_offload | |
| run_sync_or_offload( | |
| self._start_runtime(), | |
| thread_name="praisonai-tui-runtime", | |
| ) | |
| except ImportError: | |
| # Resident path: no wrapper bridge, so run the coroutine directly. | |
| try: | |
| asyncio.run(self._start_runtime()) | |
| except Exception as e: | |
| self.messages.append(ChatMessage( | |
| role="system", content=f"Runtime unavailable: {e}" | |
| )) | |
| except Exception as e: | |
| # Continue without runtime | |
| self.messages.append(ChatMessage( | |
| role="system", | |
| content=f"Runtime unavailable: {e}" | |
| )) |
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 1549-1549: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/interactive/async_tui.py` around lines
1533 - 1554, Update the runtime startup block around self._start_runtime() so
failure to import or use the shared run_sync_or_offload bridge falls back to
executing self._start_runtime() on a local event loop. Preserve the existing
Runtime unavailable message only when the fallback startup also fails, ensuring
standalone praisonai-code installations still start ACP/LSP servers.
| def run_single(self, prompt: str) -> Optional[str]: | ||
| """Run a single prompt (non-interactive).""" | ||
| return self._execute_prompt(prompt) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
run_single skips @file expansion, unlike the interactive path.
_execute_in_background expands @file mentions for every interactive turn. run_single calls _execute_prompt directly, so praisonai code "explain @main.py" sends the literal @main.py token. Both chat.py and code.py route one-shot prompts through run_single, so the two entry points behave differently.
♻️ Proposed fix
def run_single(self, prompt: str) -> Optional[str]:
"""Run a single prompt (non-interactive)."""
- return self._execute_prompt(prompt)
+ return self._execute_prompt(self._process_file_mentions(prompt))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def run_single(self, prompt: str) -> Optional[str]: | |
| """Run a single prompt (non-interactive).""" | |
| return self._execute_prompt(prompt) | |
| def run_single(self, prompt: str) -> Optional[str]: | |
| """Run a single prompt (non-interactive).""" | |
| return self._execute_prompt(self._process_file_mentions(prompt)) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/interactive/async_tui.py` around lines
1841 - 1843, Update InteractiveTUI.run_single to apply the same `@file` expansion
used by _execute_in_background before calling _execute_prompt, preserving the
existing one-shot return behavior for both chat.py and code.py entry points.
| # Execute | ||
| response = agent.chat(context + message) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
praisonaiagents Agent achat async chat method
💡 Result:
In the PraisonAI SDK, achat is the asynchronous version of the standard chat method [1][2][3]. It is a method of the Agent class designed for non-blocking, asynchronous execution [4][5]. Key characteristics and usage of the achat method include: Asynchronous Execution: Being an async function, it must be called using the await keyword within an asynchronous context (e.g., inside an async def function) [4][5]. It is specifically used for low-level, non-blocking agent interactions [4][6]. Function Signature: async def achat(prompt: str, temperature: Any, tools: Any, output_json: Any, output_pydantic: Any, reasoning_steps: Any, task_name: Any, task_description: Any, task_id: Any, attachments: Any) -> Any Self-Reflection Support: The method natively supports self-reflection, making it a robust option for complex, iterative agent tasks [1][2][3]. Tool Safety: The achat method incorporates modern safety mechanisms, including support for required approvals, circuit breakers, and timeouts, ensuring that tools executed within an async flow follow the same safety protocols as synchronous operations [7]. Ephemeral Attachments: The method supports passing a list of image or file paths as attachments. These are considered ephemeral, meaning they are used only for the current turn and are not stored in the agent's chat history [2][3]. Usage Example: import asyncio from praisonaiagents import Agent async def main: agent = Agent(name="AsyncAgent", instructions="Handle async operations") # Non-blocking chat execution result = await agent.achat("Process this request") print(result) asyncio.run(main) In addition to achat, the Agent class provides other async-specific methods such as astart (for interactive, streaming-aware async execution) and arun (for production-oriented async execution) [4][8][6].
Citations:
- 1: https://docs.praison.ai/docs/sdk/reference/praisonaiagents/classes/Agent
- 2: https://praisonai.mintlify.app/docs/sdk/reference/praisonaiagents/functions/Agent-achat
- 3: https://docs.praison.ai/docs/sdk/reference/praisonaiagents/functions/Agent-achat
- 4: https://praisonai.mintlify.app/docs/sdk/praisonaiagents/agent/agent
- 5: https://docs.praison.ai/docs/features/async
- 6: https://docs.praison.ai/docs/sdk/praisonaiagents/agent/agent
- 7: https://docs.praison.ai/docs/features/async-tool-safety
- 8: https://docs.praison.ai/docs/sdk/reference/praisonaiagents/functions/Agent-astart
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files matching core.py:"
fd -a 'core\.py$' . | sed 's#^\./##'
echo
echo "Candidate file excerpt and import/agent setup:"
file="src/praisonai-code/praisonai_code/cli/interactive/core.py"
if [ -f "$file" ]; then
wc -l "$file"
sed -n '1,220p' "$file" | cat -n
echo "---- execute area ----"
sed -n '280,365p' "$file" | cat -n
fi
echo
echo "Search Agent initialization/chat usage in praisonai-code:"
rg -n "Agent\(|\.chat\(|\.achat\(" src/praisonai-code -g '*.py' || trueRepository: MervinPraison/PraisonAI
Length of output: 26488
🌐 Web query:
PrisonAI docs achat usage async Agent achat prompt
💡 Result:
Based on available technical records, there is no widely documented software framework officially named PrisonAI that provides standard async agent or chat usage patterns. It is highly probable that the query refers to PraisonAI, a prominent open-source multi-agent orchestration framework [1][2], which has been the subject of multiple security advisories in 2026 [3][4][2][5]. If you are working with the PraisonAI framework, here is important context regarding your query: Async and Agent Usage: PraisonAI is designed for multi-agent orchestration [1][2]. It typically utilizes configuration files (often named agents.yaml) to define agent workflows [1]. Users interact with these agents via defined API endpoints or the framework's CLI [1]. Security Advisory: If you are implementing agents using this framework, please be aware that recent versions (prior to 4.6.78) contained critical vulnerabilities, including Remote Code Execution (RCE) via prompt injection [2] and authentication bypasses [1]. It is strongly recommended to use version 4.6.78 or higher [4][2][5]. Regarding the specific terms in your request: - Chat/Prompt Usage: PraisonAI agents process prompts through its orchestration engine, which can be triggered via specific API paths (e.g., /dialog) [1]. - Async Operations: The framework's ability to execute code and perform tool calls is handled by subsystems like CodeAgent [2], which has historically required careful configuration to ensure safe execution (e.g., enabling sandboxing with sandbox=True) to prevent RCE [2]. If you are actually looking for software specifically named PrisonAI in the context of surveillance, unrelated sources suggest it may refer to proprietary prison monitoring tools [6] rather than an AI agent development framework. Top results: [1][4][2]
Citations:
- 1: https://techsparking.com/prisonai-cve-2026-44338-auth-bypass-targeted-within-hours-of-disclosure/
- 2: https://www.secnews.gr/en/720827/praisonai-rce-cve-2026-61447-ai-agent/
- 3: https://cve.imfht.com/intel/678484
- 4: https://cve.imfht.com/intel/665908
- 5: https://cve.imfht.com/intel/665909
- 6: https://synai.co.za/
Do not block the event loop in async def _execute_prompt.
agent.chat() is synchronous, so this call blocks the running event loop for the whole model turn. Await the async Agent API if it supports achat, or offload agent.chat(...) to a thread so async frontends stay responsive.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/interactive/core.py` around lines 349 -
350, Update async method _execute_prompt so the synchronous agent.chat call does
not block the event loop: prefer awaiting the Agent async API achat when
available, otherwise offload agent.chat(context + message) to a worker thread.
Preserve the existing response handling and prompt behavior.
| session_id = data.get("session_id", str(uuid.uuid4())[:8]) | ||
|
|
||
| # Create session using get_or_create | ||
| session = self.session_store.get_or_create(session_id) | ||
| session.metadata["title"] = data.get("title", "Imported Session") | ||
| session.metadata.update(data.get("metadata", {})) | ||
|
|
||
| # Add messages | ||
| for msg in data.get("messages", []): | ||
| if msg.get("role") == "user": | ||
| session.add_user_message(msg.get("content", "")) | ||
| elif msg.get("role") == "assistant": | ||
| session.add_assistant_message(msg.get("content", "")) | ||
|
|
||
| self.session_store.save(session) | ||
| return session_id |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm get_or_create returns existing sessions and check for a history-reset API.
fd -t f -g '*session*.py' src/praisonai-code | while IFS= read -r f; do
rg -n -C 6 'def get_or_create|def add_user_message|def clear|def reset' "$f"
doneRepository: MervinPraison/PraisonAI
Length of output: 1816
🏁 Script executed:
#!/bin/bash
set -u
echo "== candidate files =="
fd -t f 'session|core\.py' src/praisonai-code -x sh -c 'echo "-- {}"; wc -l "{}" | awk "{print \$1}"' | sed -n '1,120p'
echo "== relevant outlines =="
for f in $(fd -t f -g '*session*.py' src/praisonai-code); do
ast-grep outline "$f" --view compact | sed -n '1,120p'
done
echo "== core import/export section =="
f=$(fd -t f '^core\.py$' src/praisonai-code/praisonai_code/cli/interactive | head -n1)
if [ -n "$f" ]; then
sed -n '550,675p' "$f" | cat -n -v
fi
echo "== Session/store message APIs =="
rg -n -C 5 'class Session|def add_message|def messages|messages:|def get_or_create|def clear_session|def delete_session|def get_or_create_session|class .*SessionStore' src/praisonai-codeRepository: MervinPraison/PraisonAI
Length of output: 50379
🏁 Script executed:
#!/bin/bash
set -u
core=$(fd -t f '^core\.py$' 'src/praisonai-code/praisonai_code/cli/interactive' | head -n1)
echo "== core file: $core"
wc -l "$core"
rg -n 'session_store|class .*Session|SessionStore|get_or_create|clear_session|save\(|load\(' "$core"
echo "== nearby imports/constructor around interactive core =="
rg -n -C 3 'SessionStore|self\.session_store|interactive' src/praisonai-code/praisonai_code/cli/interactive/*.py | sed -n '1,220p'Repository: MervinPraison/PraisonAI
Length of output: 22260
🏁 Script executed:
#!/bin/bash
set -u
session_store=$(fd -t f '^session\.py$' src/praisonai-code/praisonai_code/cli | awk '$0 ~ /features\/session\.py$/ {print; exit}')
echo "== file: $session_store"
wc -l "$session_store"
rg -n -C 8 'class .*Store|def get_or_create|def clear_session|def delete_session|def load|def save' "$session_store"Repository: MervinPraison/PraisonAI
Length of output: 290
🏁 Script executed:
#!/bin/bash
set -u
core=$(fd -t f '^core\.py$' 'src/praisonai-code/praisonai_code/cli/interactive' | head -n1)
echo "== InteractiveCore.__getattr__ =="
sed -n '124,150p' "$core" | cat -n
echo
echo "== get_session_store locations =="
fd -t f 'session\.py$' src/praisonai-code/praisonai_code/cli -x sh -c 'echo "-- $1"; rg -n -C 8 "def get_session_store|class .*Store|def get_or_create|def clear_session|def load|def save" "$1"' sh {}
echo
echo "== tests mentioning import_session/export =="
rg -n -C 5 'import_session|export_session|get_session_store|get_or_createSession|SessionStore|session_store' src/praisonai-code/tests src/praisonai-code/praisonai_code/cli/interactive -g '*.py' | sed -n '1,220p'Repository: MervinPraison/PraisonAI
Length of output: 23203
🏁 Script executed:
#!/bin/bash
set -u
echo "== core.py exact function imports =="
python3 - <<'PY'
import ast
from pathlib import Path
p = Path("src/praisonai-code/praisonai_code/cli/interactive/core.py")
tree = ast.parse(p.read_text())
for node in ast.walk(tree):
if isinstance(node, (ast.Import, ast.ImportFrom)) and any(
isinstance(imp, (ast.Attribute, ast.Name)) and "session" in (imp.attr if hasattr(imp, "attr") else imp.id)
for imp in node.names
):
print(f"{node.lineno}: {ast.unparse(node)}")
PY
echo
echo "== Locate get_session_store definition by parsing files =="
python3 - <<'PY'
import ast
from pathlib import Path
search = "get_session_store"
for p in Path("src/praisonai-code/praisonai_code/cli").rglob("*.py"):
try:
tree = ast.parse(p.read_text())
except Exception:
continue
for n in tree.body:
if isinstance(n, ast.FunctionDef) and n.name == search:
print(f"{p}:{n.lineno}")
elif isinstance(n, ast.ClassDef):
for item in n.body:
if isinstance(item, ast.FunctionDef) and item.name == search:
print(f"{p}:{item.lineno}")
PY
echo
echo "== all get_session_store usages =="
rg -n 'get_session_store\(|def get_session_store|class.*Store|add_message\(' src/praisonai-code/praisonai_code/cli -g '*.py' | sed -n '1,260p'Repository: MervinPraison/PraisonAI
Length of output: 3237
🏁 Script executed:
#!/bin/bash
set -u
echo "== UnifiedSessionStore relevant API =="
sed -n '176,285p' src/praisonai-code/praisonai_code/cli/session/unified.py | cat -n -v
sed -n '600,660p' src/praisonai-code/praisonai_code/cli/session/unified.py | cat -n -v
echo
echo "== TUI SessionStore relevant API =="
sed -n '75,190p' src/praisonai-code/praisonai_code/cli/features/tui/session_store.py | cat -n -v
echo
echo "== Programmatic behavioral probe for current import_session logic =="
python3 - <<'PY'
from pathlib import Path
import re
paths = [
Path("src/praisonai-code/praisonai_code/cli/session/unified.py"),
Path("src/praisonai-code/praisonai_code/cli/features/tui/session_store.py"),
]
for path in paths:
text = path.read_text()
print(f"\n{path}")
for name in ("get_or_create", "load", "clear_session", "add_user_message", "add_assistant_message", "add_message"):
pat = rf'(?m)^(\s*)def {name}\b'
m = re.search(pat, text)
print(f" {name}: {'found' if m else 'missing'}")
if m:
start = int(m.group(0).count('\n'))
print(f" line {sum(text[:start].count(chr(10)))+1}")
PYRepository: MervinPraison/PraisonAI
Length of output: 12873
🏁 Script executed:
#!/bin/bash
set -u
echo "== UnifiedSessionStore API =="
python3 - <<'PY'
import ast
from pathlib import Path
p = Path("src/praisonai-code/praisonai_code/cli/session/unified.py")
tree = ast.parse(p.read_text())
for cls in tree.body:
if isinstance(cls, ast.ClassDef) and cls.name == "UnifiedSessionStore":
for i, node in enumerate(cls.body):
if isinstance(node, ast.FunctionDef):
print(f"{node.name}: line {node.lineno}")
PY
echo
echo "== unified store method bodies =="
sed -n '320,560p' src/praisonai-code/praisonai_code/cli/session/unified.py | cat -n -v
echo
echo "== TUI store method bodies =="
sed -n '88,170p' src/praisonai-code/praisonai_code/cli/features/tui/session_store.py | cat -n -vRepository: MervinPraison/PraisonAI
Length of output: 14837
🏁 Script executed:
#!/bin/bash
set -u
echo "== UnifiedSession definition and message methods =="
sed -n '1,130p' src/praisonai-code/praisonai_code/cli/session/unified.py | cat -n -v
echo
echo "== UnifiedSessionStore add methods/search =="
rg -n -C 4 'def (add_user_message|add_assistant_message|add_message|replace_messages|set_messages|import|message)' src/praisonai-code/praisonai_code/cli/session/unified.py
echo
echo "== TUI session model add_message =="
sed -n '42,58p' src/praisonai-code/praisonai_code/cli/features/tui/session_store.py | cat -n -vRepository: MervinPraison/PraisonAI
Length of output: 7801
Clear the session before restoring an imported snapshot.
get_or_create(session_id) returns the existing session when the same id is present, and the imported messages are appended to the existing messages list. Importing an exported session over an existing id duplicates every message. Clear or replace the session history before applying the imported messages.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-code/praisonai_code/cli/interactive/core.py` around lines 638 -
653, Update the import flow around session_store.get_or_create and the
message-restoration loop to clear or replace the existing session history before
adding imported messages. Preserve the imported metadata and session ID behavior
while ensuring repeated imports do not append duplicate messages.
| import sys as _sys | ||
|
|
||
| Lets the CLI propagate a non-zero exit code on failure (issue #2562). | ||
| """ | ||
| return self._last_error is not None | ||
| import praisonai_code.cli.interactive.async_tui as _impl | ||
|
|
||
| _sys.modules[__name__] = _impl | ||
|
|
||
| def start_async_tui( | ||
| model: str = "gpt-4o-mini", | ||
| show_logo: bool = True, | ||
| **kwargs | ||
| ) -> None: | ||
| """Start the async TUI application.""" | ||
| config = AsyncTUIConfig( | ||
| model=model, | ||
| show_logo=show_logo, | ||
| ) | ||
|
|
||
| tui = AsyncTUI(config=config) | ||
| tui.run() | ||
| _parent_name, _, _child_name = __name__.rpartition(".") | ||
| if _parent_name and _parent_name in _sys.modules: | ||
| setattr(_sys.modules[_parent_name], _child_name, _impl) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -uo pipefail
echo "=== _shim.py helper surface ==="
fd -t f '_shim.py' src/praisonai --exec cat -n {}
echo "=== existing helper call sites ==="
rg -n --type=py -C2 '_shim' src/praisonai -g '!**/_shim.py'
echo "=== all hand-rolled sys.modules aliases under praisonai ==="
rg -n --type=py -C3 'sys\.modules\[__name__\]' src/praisonaiRepository: MervinPraison/PraisonAI
Length of output: 50379
Move the four praisonai.cli.interactive shims into _shim.py.
These files duplicate the same module-identity and parent-package attribute rebind pattern. Route them through one helper under src/praisonai/praisonai/cli/_shim.py, or add an equivalent helper if needed, so future package-layout changes stay in the shared shim path.
📍 Affects 4 files
src/praisonai/praisonai/cli/interactive/async_tui.py#L9-L17(this comment)src/praisonai/praisonai/cli/interactive/config.py#L9-L17src/praisonai/praisonai/cli/interactive/core.py#L8-L16src/praisonai/praisonai/cli/interactive/events.py#L8-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai/praisonai/cli/interactive/async_tui.py` around lines 9 - 17,
Move the duplicated shim logic from async_tui.py (lines 9-17), config.py (lines
9-17), core.py (lines 8-16), and events.py (lines 8-16) into a shared helper at
src/praisonai/praisonai/cli/_shim.py. Update each shim to invoke that helper
while preserving module identity replacement and parent-package attribute
rebinding; all four sites require the same direct change.
Source: Coding guidelines
The wrapper-free `code` fallback (`_run_resident_code`) had three defects flagged by reviewers on PR #3818: - NameError: it referenced an undefined `compact` local, crashing every standalone `code` invocation before the TUI could start. - --plan was silently dropped: the resolved read-only PLAN backend was never registered, so mutating tools (write/edit/shell) stayed enabled. - --no-acp/--no-lsp were ignored: the flags never reached the TUI, which hardcoded ACP+LSP tool loading and runtime startup. Fix by registering the resolved approval backend on the global registry (same pattern `chat` uses; the TUI syncs its PLAN state from it) and threading plan/no-acp/no-lsp onto AsyncTUIConfig via new enable_acp/ enable_lsp fields (default True, backward compatible). _load_tools and _start_runtime now honor those fields. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
|
@claude this PR has merge conflicts with |
|
Claude finished @MervinPraison's task —— View job Rebase complete ✅ — pushed to
|
Relocate the async split-pane TUI (async_tui.py) and InteractiveCore (core.py) from the praisonai wrapper into praisonai-code so `pip install praisonai-code` alone yields a full interactive chat/code session with no wrapper required. - Move async_tui.py + core.py impls into praisonai_code.cli.interactive; leave identity-preserving back-compat shims at the old praisonai paths (plus config/events shims so isinstance/monkeypatch hold across paths). - Drop the hard-fail in chat.py; route interactive chat to the resident AsyncTUI. Route interactive code to a resident AsyncTUI path (_run_resident_code) when the wrapper is absent, keeping the richer legacy dispatch when it is installed. - Convert lazy wrapper-only imports (_async_bridge, integration.context_files) to _wrapper_bridge with graceful fallback so no `praisonai` import remains in the interactive package. - Extend scripts/check_c7_imports.sh to gate interactive async_tui/core at module load; C7 gate now reports 0 wrapper import lines. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
The wrapper-free `code` fallback (`_run_resident_code`) had three defects flagged by reviewers on PR #3818: - NameError: it referenced an undefined `compact` local, crashing every standalone `code` invocation before the TUI could start. - --plan was silently dropped: the resolved read-only PLAN backend was never registered, so mutating tools (write/edit/shell) stayed enabled. - --no-acp/--no-lsp were ignored: the flags never reached the TUI, which hardcoded ACP+LSP tool loading and runtime startup. Fix by registering the resolved approval backend on the global registry (same pattern `chat` uses; the TUI syncs its PLAN state from it) and threading plan/no-acp/no-lsp onto AsyncTUIConfig via new enable_acp/ enable_lsp fields (default True, backward compatible). _load_tools and _start_runtime now honor those fields. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
e11ee78 to
bf5ccdf
Compare
| try: | ||
| from praisonaiagents.approval import get_approval_registry | ||
|
|
||
| get_approval_registry().set_backend(backend) |
There was a problem hiding this comment.
PLAN backend bypasses the agent
When a standalone user starts code --plan, this code registers the PLAN backend globally, but the resident TUI constructs its Agent without that backend. Mutating tools consequently reach the agent's ordinary console-approval path instead of being unconditionally denied, allowing an approved write, edit, or shell operation during a purportedly read-only session.
How this was verified: The resident agent receives no approval argument, retains no PLAN permission mode, and routes dangerous tools to its separate ConsoleBackend.
Knowledge Base Used: praisonai-code
Fixes #3814
Summary
Relocates the flagship async split-pane TUI (
async_tui.py) andInteractiveCore(core.py) from the heavypraisonaiwrapper into the CLI-firstpraisonai-codepackage, sopip install praisonai-codealone now delivers a full interactivechat/codesession — no wrapper required. This completes the CLI-first split rather than reversing it.Changes
async_tui.py+core.pyimplementations intopraisonai_code.cli.interactive. They import onlypraisonaiagents+prompt_toolkitat module level. Identity-preserving back-compat shims remain atpraisonai.cli.interactive.{async_tui,core}(plus newconfig/eventsshims) so old import paths,isinstance, and test monkeypatching keep working.chatcommand: removed the "requires the praisonai wrapper" hard-fail; interactive chat now routes to the residentAsyncTUI.codecommand: replaced the hard-fail with_run_resident_code, which runs the resident split-pane TUI when the wrapper is absent, while keeping the richer legacy dispatch (PraisonAI._start_interactive_mode) when the wrapper is installed.praisonai._async_bridge,praisonai.integration.context_files) to route throughpraisonai_code._wrapper_bridgewith graceful fallback. Nopraisonaiimport remains anywhere in the interactive package.scripts/check_c7_imports.shnow gates interactiveasync_tui/coreat module load. The gate reports 0 wrapper import lines inpraisonai-code.Test plan
scripts/check_c7_imports.sh→ all gates ok, 0 wrapper import linescore/async_tui/chat/codeimport + instantiate with nopraisonaiwrapper presenttests/unit/cli/interactive/,test_async_tui,test_headless_interactive_core,test_interactive_surfaces_parity,test_event_routing,test_c7_1_boundaries)praisonai-code: 62 passed (test_plan_mode_flag,test_standalone_stub_commands,test_code_print_json,test_run_standalone_wrapper_gate)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
praisonai-codeinstallations can now run chat and code sessions without requiring the separate wrapper.Compatibility