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 mode
  • praisonai chat – Interactive terminal chat mode
  • praisonai 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

bash
pip install praisonai-code
bash
# The wrapper still works and adds gateway/bot channels
pip install praisonai

Quick Start

python
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

ParameterTypeDefaultDescription
workspacestr“.”Workspace root directory
lsp_enabledboolTrueEnable LSP code intelligence
acp_enabledboolTrueEnable ACP action orchestration
approval_modestr“manual”Approval mode: manual, auto, scoped
trace_enabledboolFalseEnable trace logging
trace_filestrNonePath to save trace file
json_outputboolFalseOutput JSON format
timeoutfloat60.0Operation timeout in seconds
modelstrNoneLLM model to use
verboseboolFalseVerbose output

Runtime Status

python
status = runtime.get_status()

Returns:

json
{
  "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

StatusDescription
not_startedSubsystem not initialized
startingSubsystem is starting
readySubsystem is ready for use
failedSubsystem failed to start
stoppedSubsystem has been stopped

LSP Operations

When LSP is ready, you can use code intelligence:

python
# 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:

python
# 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:

python
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)
python
if runtime.read_only:
    print("Warning: ACP unavailable, read-only mode")

CLI Integration

The runtime integrates with CLI flags:

bash
# Enable LSP
praisonai chat --lsp

# Enable ACP with auto-approval
praisonai chat --acp --approval auto

# Enable tracing
praisonai chat --trace --trace-file session.json

Lifecycle in YAML runs (framework: praisonai)

Setting config.acp: true or config.lsp: true in YAML triggers the same InteractiveRuntime lifecycle as the Python API:

yaml
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

Diagram
AgentTeamAgent ToolsInteractiveRuntime_async_bridge (shared loop)YAML ParserAgentTeamAgent ToolsInteractiveRuntime_async_bridge (shared loop)YAML Parserrun_sync(runtime.start())start() on persistent bg loopcreate_agent_centric_tools(runtime)Merge into tools_dictteam.start() with merged toolstool_call → run_sync(_coro())re-enters SAME loopresultAgent execution resultrun_sync(runtime.stop()) (finally block)

YAML Configuration Details

The YAML route uses default RuntimeConfig values with these settings:

  • Workspace: os.getcwd() (current working directory)
  • Approval Mode: PRAISONAI_APPROVAL_MODE environment 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:

python
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 support
  • pyright (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.

Card

How sync/async crossings work in the wrapper layer

InteractiveCore (Unified Runtime)

The new InteractiveCore provides a unified runtime for all interactive modes:

python
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

FlagShortDescription
--model-mLLM model to use
--session-sSession ID to resume
--continue-cContinue last session
--file-fAttach file(s) to prompt
--workspace-wWorkspace directory
--verbose-vVerbose output
--memoryEnable memory

Session Management

bash
# 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:

ModeDescription
promptAsk user for each action (default)
autoAuto-approve all actions
rejectReject all actions requiring approval
python
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.

bash
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