Background Shell Execution

Non-blocking execute_command for dev servers, watchers, and multi-minute builds and test suites

execute_command(..., run_in_background=True) starts a shell command and returns a handle immediately, so dev servers, watchers, and long test suites don’t block the turn or get killed by the 30-second timeout.

python
from praisonaiagents import Agent
from praisonaiagents import execute_command, get_command_output, stop_command

agent = Agent(
    name="Dev Runner",
    instructions=(
        "Start the dev server with execute_command(run_in_background=True). "
        "Tail its output with get_command_output until it reports ready. "
        "Run a smoke test with a short foreground execute_command. "
        "Finish by calling stop_command on the server's process_id."
    ),
    tools=[execute_command, get_command_output, stop_command],
)
agent.start("Launch `npm run dev`, confirm it serves on :3000, then shut it down.")
Diagram

Background Shell Lifecycle

status: running

status: exited

User Request

execute_command
run_in_background=True

process_id handle

get_command_output
tail=N

stop_command

exit_code + output

Quick Start

  1. Start
    python
    from praisonaiagents import execute_command
    
    h = execute_command("npm run dev", run_in_background=True)
    # {"success": True, "process_id": "3f9a1c2b", "pid": 4821, "status": "running",
    #  "log_file": "/tmp/praisonai-bg-xxxxxx.log"}
  2. Poll Output
    python
    from praisonaiagents import get_command_output
    
    print(get_command_output(h["process_id"], tail=20))
    # {"status": "running", "exit_code": None, "output": "...last lines..."}
  3. Stop
    python
    from praisonaiagents import stop_command
    
    stop_command(h["process_id"])
    # {"success": True, "status": "exited", "exit_code": -15}

Which Mode Should I Use?

Diagram

Yes

No

Yes – long build/test suite

No – dev server/watcher

Need to run a command?

Finishes within 30 seconds?

execute_command default — blocking

Will it ever return?

execute_command run_in_background=True
poll with get_command_output

execute_command run_in_background=True
stop_command when done


How It Works

The agent starts the process, polls a log file, and stops the process tree when done.

Diagram
Log FileBackground ProcessShellToolsAgentLog FileBackground ProcessShellToolsAgentloop[while agent needs the output]wait up to 5sexecute_command(run_in_background=True)Popen (own session on POSIX)redirect stdout+stderr{process_id, pid, status: running, log_file}get_command_output(process_id, tail=N)poll()read last 64 KB{status, exit_code, output}stop_command(process_id)SIGTERM process group (POSIX)SIGKILL process group{status: exited, exit_code}

The same safety checks apply as for foreground commands:

CheckWith run_in_background=True
shell=False (command split with shlex)Still applies
Shell-syntax refusal (redirects, pipes, chains, substitutions, newlines)Still fires before the process starts; no process_id is issued
DANGEROUS_COMMANDS + allow_dangerousStill applies
critical-risk approvalStill gates the start
timeoutIgnored — call stop_command or let the exit hook reap it

Configuration Options

execute_command (new parameter)

ParameterTypeDefaultDescription
run_in_backgroundboolFalseStart the command and return a process_id immediately. timeout is ignored.

get_command_output(process_id, tail=200)

ParameterTypeDefaultDescription
process_idstr—The process_id returned by execute_command.
tailint200Number of most-recent output lines to return. Reads at most the last 64 KB of the log.
python
{
    "success": True,        # True while running or on clean exit (exit_code in (None, 0))
    "process_id": "3f9a1c2b",
    "pid": 4821,
    "status": "running",    # or "exited"
    "exit_code": None,      # int once exited
    "output": "last N lines...",
    "log_file": "/tmp/praisonai-bg-xxxxxx.log",
}

stop_command(process_id)

ParameterTypeDefaultDescription
process_idstr—The process_id returned by execute_command.
python
{"success": True, "process_id": "3f9a1c2b", "status": "exited", "exit_code": -15}

An unknown id returns {"success": False, "error": "No background command with process_id '...'"} from both functions.


Common Patterns

Start a server, then test it

python
from praisonaiagents import execute_command, get_command_output, stop_command

server = execute_command("python -m http.server 8000", run_in_background=True)
print(get_command_output(server["process_id"], tail=5)["output"])
print(execute_command("curl -s -o /dev/null -w %{http_code} http://localhost:8000")["stdout"])
stop_command(server["process_id"])

Long test suite with periodic tailing

python
from praisonaiagents import execute_command, get_command_output

run = execute_command("pytest -q", run_in_background=True)
status = get_command_output(run["process_id"], tail=10)
while status["status"] == "running":
    status = get_command_output(run["process_id"], tail=10)
print(status["exit_code"], status["output"])

Watcher that outlives several turns

python
from praisonaiagents import Agent
from praisonaiagents import execute_command, get_command_output, stop_command

agent = Agent(
    name="Watcher",
    instructions="Keep the watcher running between turns. Stop it only when asked.",
    tools=[execute_command, get_command_output, stop_command],
)
agent.start("Start `npm run watch` in the background.")
agent.start("Show me the latest watcher output.")
agent.start("Stop the watcher.")

Platform Behaviour

PlatformHow stop_command ends the process
POSIXThe command runs in its own session. SIGTERM goes to the whole process group, waits up to 5 seconds, then SIGKILL hits anything left.
WindowsWalks the tree with psutil when available, otherwise taskkill /F /T /PID. A missing or hung taskkill still falls through to process.kill().

An atexit hook stops any still-running background commands when the interpreter exits, so dev servers are not orphaned.


Deliberate Limits

  • A restart loses the process handle. The log file stays on disk, but the process is not re-adopted.
  • There is no shell_list tool to list running background commands.
  • There is no push notification on completion — the agent polls get_command_output.
  • Background commands are not surfaced in the jobs CLI.

Best Practices

Stop what you start

The exit hook reaps leftovers, but relying on it is bad hygiene. Write the matching stop_command(process_id) into the agent’s instructions for every background start.

Poll with tail, don't scrape the full log

Use get_command_output(process_id, tail=20). The read is bounded to 64 KB, and a small tail keeps the agent’s context small.

Trust exit_code, not stdout contents

Once status is "exited", exit_code is the authoritative result. Output text can look healthy while the process failed.


Related