Interactive Runtime Module
Unified core runtime for all interactive modes with session management and approval flows
Overview
The Interactive Runtime provides a unified core runtime that powers all interactive modes in PraisonAI:
praisonai chat– Interactive chat modepraisonai chat– Interactive terminal chat modepraisonai tui launch– Full-screen TUI mode
All modes share the same:
- Session storage and continuation
- Tool dispatch and loading
- Permission/approval semantics
- Event-based architecture
Installation
pip install praisonai-code# The wrapper still works and adds gateway/bot channels
pip install praisonaiQuick Start
import asyncio
from praisonai.cli.features import InteractiveRuntime, RuntimeConfig
async def main():
# Configure runtime
config = RuntimeConfig(
workspace="./my_project",
lsp_enabled=True,
acp_enabled=True,
approval_mode="auto",
trace_enabled=True
)
# Create and start runtime
runtime = InteractiveRuntime(config)
status = await runtime.start()
print(f"LSP ready: {runtime.lsp_ready}")
print(f"ACP ready: {runtime.acp_ready}")
print(f"Read-only: {runtime.read_only}")
# Use runtime for operations...
await runtime.stop()
asyncio.run(main())When the runtime is consumed via the PraisonAI YAML adapter (rather than constructed directly as above), startup failures now degrade gracefully — agents run without the runtime-backed centric tools instead of crashing the run.
Configuration
RuntimeConfig
| Parameter | Type | Default | Description |
|---|---|---|---|
workspace | str | “.” | Workspace root directory |
lsp_enabled | bool | True | Enable LSP code intelligence |
acp_enabled | bool | True | Enable ACP action orchestration |
approval_mode | str | “manual” | Approval mode: manual, auto, scoped |
trace_enabled | bool | False | Enable trace logging |
trace_file | str | None | Path to save trace file |
json_output | bool | False | Output JSON format |
timeout | float | 60.0 | Operation timeout in seconds |
model | str | None | LLM model to use |
verbose | bool | False | Verbose output |
Runtime Status
status = runtime.get_status()Returns:
{
"started": true,
"workspace": "/path/to/project",
"lsp": {
"enabled": true,
"status": "ready",
"ready": true,
"error": null
},
"acp": {
"enabled": true,
"status": "ready",
"ready": true,
"error": null
},
"read_only": false,
"approval_mode": "auto"
}Subsystem States
| Status | Description |
|---|---|
not_started | Subsystem not initialized |
starting | Subsystem is starting |
ready | Subsystem is ready for use |
failed | Subsystem failed to start |
stopped | Subsystem has been stopped |
LSP Operations
When LSP is ready, you can use code intelligence:
# Get symbols in a file
symbols = await runtime.lsp_get_symbols("main.py")
# Get definition location
definitions = await runtime.lsp_get_definition("main.py", line=10, col=5)
# Get references
references = await runtime.lsp_get_references("main.py", line=10, col=5)
# Get diagnostics
diagnostics = await runtime.lsp_get_diagnostics("main.py")ACP Operations
When ACP is ready, you can create and apply action plans:
# Create a plan
plan = await runtime.acp_create_plan("Create a new file")
# Apply a plan
result = await runtime.acp_apply_plan(plan, auto_approve=True)Tracing
Enable tracing to capture all operations:
config = RuntimeConfig(
workspace="./project",
trace_enabled=True,
trace_file="trace.json"
)
runtime = InteractiveRuntime(config)
await runtime.start()
# ... perform operations ...
# Save trace
runtime.save_trace("my_trace.json")
# Get trace object
trace = runtime.get_trace()
print(trace.to_dict())Graceful Degradation
The runtime handles subsystem failures gracefully:
- LSP fails: Code intelligence falls back to regex-based extraction
- ACP fails: Runtime enters read-only mode (no file modifications)
if runtime.read_only:
print("Warning: ACP unavailable, read-only mode")CLI Integration
The runtime integrates with CLI flags:
# Enable LSP
praisonai chat --lsp
# Enable ACP with auto-approval
praisonai chat --acp --approval auto
# Enable tracing
praisonai chat --trace --trace-file session.jsonLifecycle in YAML runs (framework: praisonai)
Setting config.acp: true or config.lsp: true in YAML triggers the same InteractiveRuntime lifecycle as the Python API:
framework: praisonai
topic: Refactor the auth module
config:
acp: true
lsp: true
roles:
refactorer:
role: Code Refactorer
goal: Tighten auth.py for readability and safety
backstory: Staff engineer, security-minded.
tasks:
refactor:
description: Refactor the auth module in {topic}
expected_output: Diff applied via the agent-centric tools.YAML Runtime Lifecycle
YAML Configuration Details
The YAML route uses default RuntimeConfig values with these settings:
- Workspace:
os.getcwd()(current working directory) - Approval Mode:
PRAISONAI_APPROVAL_MODEenvironment variable (default:"prompt") - Trace Enabled: Not explicitly set (uses RuntimeConfig default)
The runtime boots on the shared _async_bridge background loop (the same loop already used by ~77 wrapper-side run_sync callers), not on a per-run scoped loop. This is what makes ACP/LSP agent-centric tools safe — every async primitive owned by the runtime stays bound to a loop that is still running when tools re-enter. Tool calls made through the ACP/LSP create_agent_centric_tools set use the sibling praisonai_code.cli.features.agent_tools._run_sync bridge (Tier-2, no wrapper import) — it does not run on the wrapper’s background loop, but it does read the same PRAISONAI_RUN_SYNC_TIMEOUT env var so hung tool calls are bounded consistently (PR #3361).
run_sync() now cancels the runtime startup/shutdown coroutine on timeout (PR #1692), so a hung LSP server or ACP backend no longer leaks.
The shared default bridge’s atexit teardown hook is registered lazily, on that bridge’s first real use — a bare import praisonai no longer installs a process-wide atexit hook, so Django/Airflow/Streamlit embedders are unaffected until they actually call run_sync.
Full RuntimeConfig Control
For full RuntimeConfig control, use the Python API directly:
import asyncio
from praisonai.cli.features import InteractiveRuntime, RuntimeConfig
from praisonaiagents import Agent, AgentTeam
async def advanced_workflow():
config = RuntimeConfig(
workspace="/custom/path",
approval_mode="auto",
trace_enabled=True,
lsp_enabled=True,
acp_enabled=True
)
runtime = InteractiveRuntime(config)
await runtime.start()
# Create agents with runtime tools
from praisonai.cli.features.agent_tools import create_agent_centric_tools
tools = create_agent_centric_tools(runtime)
agent = Agent(name="Dev", tools=list(tools.values()))
team = AgentTeam(agents=[agent])
result = team.start()
await runtime.stop()
return result
# Run the workflow
result = asyncio.run(advanced_workflow())Operational Notes
Performance
- Subsystems start in parallel for faster initialization
- LSP client is lazy-loaded only when enabled
- ACP session is lightweight (in-process)
Dependencies
pylsp(optional) – For Python LSP supportpyright(optional) – Alternative Python LSP
Production Caveats
- LSP startup may take a few seconds for large workspaces
- Trace files can grow large for long sessions
- ACP session is in-memory; external storage needed for persistence
Concurrency safety
InteractiveRuntime.start() and .stop() are coroutines. The CLI boots them through the persistent background loop via the Async Bridge, so they are safe to call from sync code (e.g. inside PraisonAI._run_praisonai). Calling PraisonAI.run() from inside a running event loop raises RuntimeError instead of deadlocking — wrap your async caller around the runtime directly instead.
Session-store concurrency. The CLI session store (praisonai.cli.session.UnifiedSessionStore) used by TUI, --interactive, and InteractiveCore is safe across processes and threads as of PR #1854. save() reloads the on-disk JSON under an exclusive lock and merges concurrent message appends rather than overwriting them; load() invalidates the in-memory cache when the on-disk file’s mtime has advanced. See CLI Sessions → Concurrent Sessions for the user-facing description.
How sync/async crossings work in the wrapper layer
InteractiveCore (Unified Runtime)
The new InteractiveCore provides a unified runtime for all interactive modes:
from praisonai.cli.interactive import InteractiveCore, InteractiveConfig
# Create config
config = InteractiveConfig(
model="gpt-4o-mini",
continue_session=True, # Resume last session
files=["README.md"], # Attach files
)
# Create core
core = InteractiveCore(config=config)
# Create or continue session
if config.continue_session:
session_id = core.continue_session()
else:
session_id = core.create_session(title="My Session")
# Execute prompt
import asyncio
response = asyncio.run(core.prompt("Hello!"))
print(response)CLI Flags
| Flag | Short | Description |
|---|---|---|
--model | -m | LLM model to use |
--session | -s | Session ID to resume |
--continue | -c | Continue last session |
--file | -f | Attach file(s) to prompt |
--workspace | -w | Workspace directory |
--verbose | -v | Verbose output |
--memory | Enable memory |
Session Management
# List sessions
praisonai session list
# Export session
praisonai session export <session_id> --output session.json
# Import session
praisonai session import session.json
# Continue last session
praisonai chat --continue "What was my last question?"Approval Modes
The runtime supports three approval modes:
| Mode | Description |
|---|---|
prompt | Ask user for each action (default) |
auto | Auto-approve all actions |
reject | Reject all actions requiring approval |
config = InteractiveConfig(approval_mode="auto")In prompt mode, the Rich and Textual frontends render an approval dialog where “Always allow” persists the narrowest reusable pattern and “Always Allow All” is a separate, explicit blanket grant. See Interactive Approval › Rich / Textual TUI.
Visual replay on /continue
Resuming with /continue redraws your prior conversation through the same renderer live turns use, so scrollback shows what was actually said — bounded to the last 20 turns.
praisonai code
> /continue
# Terminal shows:
# Continued session: 019... (14 messages)
# … 12 earlier turns
# [user] Help me refactor auth.py to use JWT
# [assistant] Sure — here's the plan: …
# [system] [tool call] run_tests
# [user] Run the tests
# [assistant] 3 failing, all in test_login.py …Repeated /continue clears the display first, so turns never duplicate. Assistant messages that only ran tools show as [tool call] <name>, and session resume (non-interactive) renders the same bounded replay.
See Session Visual Replay for the full behaviour table and fallback-renderer notes.
Related
- Session Visual Replay – What appears on screen when you resume
- Agent-Centric Tools – Tools powered by this runtime
- Debug CLI – Debug commands
- ACP – Agent Communication Protocol