Background Subagents

Spawn subagents that run in the background and return a job ID immediately — collect results later or deliver them to chat

A parent agent can fire off one or more subagents and continue working. Each background subagent returns a job_id immediately — results can be pulled on demand or delivered automatically back to the originating chat.

python
from praisonaiagents import Agent
from praisonaiagents.tools.subagent_tool import create_subagent_tool

subagent = create_subagent_tool()  # spawn_subagent + subagent_result

agent = Agent(
    name="Researcher",
    instructions=(
        "When the user asks for research, spawn a background subagent with "
        "background=True and deliver='origin'. Tell the user you'll ping them "
        "when it's ready, then end your turn."
    ),
    tools=[subagent["function"], subagent["result_tool"]["function"]],
)

agent.start("Research the latest Python 3.13 features and let me know when done")

How a user experiences it in a bot chat:

  1. The user types a long task.
  2. The agent spawns the subagent, replies “Started; I’ll ping you when done”, and ends the turn.
  3. The user does other things in the chat (or walks away).
  4. When the subagent finishes, the gateway posts the result as a new message in the same chat — no new prompt needed.
Diagram

Background Subagents

🤖 Parent Agent

spawn background=true\ntask A

spawn background=true\ntask B

spawn background=true\ntask C

job_id: abc

job_id: def

job_id: ghi

⏳ continues working…

subagent_result abc

subagent_result def

subagent_result ghi

📄 Combine results

Quick Start

  1. Spawn a background subagent

    Call spawn_subagent with background=True. The parent gets a job_id and keeps running:

    python
    from praisonaiagents import Agent
    from praisonaiagents.tools.subagent_tool import create_subagent_tool
    
    subagent = create_subagent_tool()
    tools = [subagent["function"], subagent["result_tool"]["function"]]
    
    agent = Agent(
        name="Boss",
        instructions=(
            "Use spawn_subagent with background=true to start the research task. "
            "Then do other work. Finally call subagent_result with the job_id to get the answer."
        ),
        tools=tools,
    )
    
    agent.start("Research the latest Python 3.13 features in the background, "
                "then write a haiku while waiting, then fetch the research result.")
  2. Deliver result back to chat automatically

    Ask for deliver="origin" when the agent runs behind a gateway (BotOS) — the finished result is sent back to the same chat without polling:

    python
    from praisonaiagents import Agent
    from praisonaiagents.tools.subagent_tool import create_subagent_tool
    
    subagent = create_subagent_tool()  # spawn_subagent + subagent_result
    
    agent = Agent(
        name="Researcher",
        instructions=(
            "When the user asks for research, spawn a background subagent with "
            "deliver='origin' so the finished result is delivered back to this chat "
            "automatically. Tell the user you'll ping them when it's ready."
        ),
        tools=[subagent["function"], subagent["result_tool"]["function"]],
    )
  3. Fan out and collect all results

    Spawn multiple subagents in parallel and wait for all of them:

    python
    from praisonaiagents import Agent
    from praisonaiagents.tools.subagent_tool import create_subagent_tool
    
    subagent = create_subagent_tool()
    tools = [subagent["function"], subagent["result_tool"]["function"]]
    
    orchestrator = Agent(
        name="Orchestrator",
        instructions=(
            "1. Spawn three background subagents — one for Python news, one for Rust news, "
            "one for Go news. 2. Record all three job IDs. "
            "3. Call subagent_result(job_id, wait=true) for each to collect results. "
            "4. Write a short comparative summary."
        ),
        tools=tools,
    )
    
    orchestrator.start("Compare the latest language news for Python, Rust, and Go")
  4. Check status without blocking

    Poll a job without waiting — useful when you want to check periodically:

    python
    from praisonaiagents import Agent
    from praisonaiagents.tools.subagent_tool import create_subagent_tool
    
    subagent = create_subagent_tool()  # spawn_subagent + subagent_result
    
    agent = Agent(
        name="Checker",
        instructions=(
            "Spawn a slow background task. Then check its status with "
            "subagent_result(job_id, wait=false) every few steps until it's done."
        ),
        tools=[subagent["function"], subagent["result_tool"]["function"]],
    )
    agent.start("Run a background analysis and poll for its completion")

Chat-Back Delivery Flow

When running inside a gateway (BotOS), a background job delivers itself back to the originating chat when it finishes — no polling required.

Diagram
sequenceDiagram
    participant U as User (chat)
    participant G as Gateway (BotOS)
    participant A as Agent (turn)
    participant BG as BackgroundJobManager
    participant M as Outbound Messenger
    U->>G: "research X in the background"
    G->>A: turn starts
    Note over A: register_gateway_loop(loop)<br/>register_outbound_messenger(M)
    A->>BG: spawn_subagent(background=True, deliver="origin")
    Note over BG: origin filled from<br/>get_session_context()
    BG-->>A: job_id
    A-->>G: "Started; I'll ping you when done"
    G-->>U: reply (turn ends)
    Note over BG: job runs on worker thread...
    BG->>M: on_complete → messenger.send("origin", text)
    M-->>U: 📬 Result delivered (no active turn)
    Note over BG: on send OK → info.delivered = True<br/>on send fail → stays delivered = False

The result goes through the same outbound messenger and DeliveryRouter path that send_message uses — no extra configuration beyond deliver="origin".

The model never picks the chat. At spawn time the origin (platform, chat_id, thread_id, user_id) is filled from the turn’s session context. The captured user_id is also what scopes /tasks to the caller’s own jobs.

When auto-delivery kicks in

The result is sent back automatically only when all of these hold:

  1. The host did not pass its own on_job_complete to create_subagent_tool(...) (an explicit callback always wins).
  2. deliver="origin" — any other token (e.g. "telegram:123", "all") stays pull-only unless the host passes on_job_complete.
  3. The spawning turn has an outbound messenger registered (a running gateway / BotOS).
  4. The active send policy does not deny "origin" (the same check send_message uses).
  5. The target is the turn’s own chat — any explicit chat_id / platform / thread_id is empty or equal to the current turn’s.
Diagram

yes

no

no

yes

no

yes

no

yes

no

yes

🚀 spawn_subagent
background=True, deliver set

Host passed
on_job_complete?

🪝 Host callback runs

deliver == origin?

📥 Pull-only

Gateway messenger
registered?

Send policy
allows?

Same chat
as the turn?

📬 Auto-delivered to chat

Without a gateway the job is pull-only: info.delivered stays False and info.origin["chat_id"] is "".


How It Works (Pull Mode)

Diagram
Subagent (background thread)BackgroundJobManagerSubagentToolParent AgentSubagent (background thread)BackgroundJobManagerSubagentToolParent AgentParent continues with other workspawn_subagent(task, background=True)start_job(coroutine)job_id = “abc123”{“success”: true, “job_id”: “abc123”, “status”: “running”}Execute subagent taskResult storedsubagent_result(“abc123”, wait=True)get_result(“abc123”, block=True){“output”: “…”, “status”: “completed”}Full result

Background subagents run on a shared thread pool managed by BackgroundJobManager. Job IDs are 8-character random strings. Results are kept in memory until retrieved.


How Delivery is Wired

Diagram

terminal state

origin

terminal state

🔄 Background job

📞 on_complete

📬 Turn’s outbound messenger

💬 Originating chat

🪝 JOB_COMPLETED hook

📊 Observers only

Core layer (praisonaiagents):

  • BackgroundJobManager.start_job(..., on_complete=None, origin=None) — accepts an optional completion callback.
  • spawn_subagent captures the origin into the job. When the auto-delivery conditions hold, it installs a per-turn callback that sends the result through the turn’s outbound messenger. A host that passes on_job_complete to create_subagent_tool(...) replaces this.
  • HookEvent.JOB_COMPLETED fires when a job reaches a terminal state. It is observational — it is not the chat delivery path.

Wrapper layer (praisonai_bot):

  • Each gateway turn registers its outbound messenger and its event loop (register_gateway_loop), so the background worker can send after the turn has ended.

Delivery Tokens

TokenMeaning
"" (empty)Pull-only via subagent_result (unchanged legacy behaviour)
"origin"Deliver back to the turn’s own chat — the only token auto-sent by the gateway
"all"Recorded on the job; delivered only by a host on_job_complete
"platform:chat_id"Recorded on the job (e.g. "telegram:12345"); delivered only by a host on_job_complete
"platform:chat_id:thread_id"Same, for a specific thread or reply context

Subscribe to JOB_COMPLETED

For observability or custom side effects when a background job finishes, register on HookEvent.JOB_COMPLETED (see Hook Events):

python
from praisonaiagents.hooks import HookEvent, register_hook

@register_hook(HookEvent.JOB_COMPLETED)
def log_completion(input):
    print(f"job {input.job_info.job_id} → {input.job_info.status.value}")

Tool Parameters

spawn_subagent

The model cannot set origin fields. The model-visible schema has only task, agent_name, context, tools, llm, permission_mode, background and deliver; platform, chat_id, thread_id and session_id are Python-only parameters, injected from the turn’s session context at spawn time. Explicit callers (Python code) can still override them.

ParameterTypeDefaultDescription
taskstrrequiredThe task or prompt for the subagent
agent_namestr"subagent"Name for the spawned agent
contextstrNoneOptional extra context for the task
llmstrNoneLLM model override
permission_modestrNonePermission mode for the subagent
toolslist[]Tools to give the subagent
backgroundboolFalseWhen True, return immediately with a job_id
deliverstr""Delivery token: "origin", "all", "platform:chat_id[:thread_id]", or "" (pull-only)
platformstr""Python-only. Origin platform; defaults to the turn’s platform.
chat_idstr""Python-only. Origin chat id; defaults to the turn’s chat. A different chat opts out of auto-delivery.
thread_idstr""Python-only. Origin thread id; defaults to the turn’s thread.
session_idstr""Python-only. Origin session id to preserve context.

Returns (foreground): Full result dict with output, success, error.

Returns (background): {"success": True, "job_id": "...", "status": "running"}.

subagent_result

ParameterTypeDefaultDescription
job_idstrrequiredThe job_id returned by spawn_subagent(background=True)
waitboolFalseWhen True, block until the job completes

Returns:

  • Job still running (wait=False): {"success": True, "job_id": "...", "status": "running"}
  • Job done: {"success": True, "job_id": "...", "status": "completed", "result": {...}}
  • Job failed: {"success": False, "job_id": "...", "error": "..."}

Observability

The SUBAGENT_STOP hook fires from the same completion chokepoint for background jobs as for synchronous spawns, so you do not need to poll subagent_result just to observe completion. Register a hook and let the runtime call you.

python
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult, SubagentStopInput

registry = HookRegistry()

@registry.on(HookEvent.SUBAGENT_STOP)
def on_bg_subagent_stop(event_data: SubagentStopInput):
    # event_data.task, event_data.success, event_data.output, event_data.duration_ms
    print(f"{event_data.agent_name}: success={event_data.success} in {event_data.duration_ms:.0f} ms")
    return HookResult.allow()

See Hook Events → Subagent Events for the payload reference.


Best Practices

Don't background trivially fast tasks

Background subagents have overhead (thread scheduling, job management). For tasks that complete in under a second, use foreground subagents (background=False, the default) — they’re simpler and return results immediately.

Always capture job IDs

The parent agent should record job IDs in its working memory or output immediately after spawning. If a job ID is lost, there is no way to retrieve the result.

Set a concurrency budget

The BackgroundJobManager runs on a thread pool. Avoid spawning hundreds of concurrent subagents without throttling — each subagent holds a connection and context window while running.

Use wait=True for final collection

When you need a result to proceed, always use subagent_result(job_id, wait=True) rather than polling in a loop. The wait=True mode blocks the calling thread efficiently without spinning.

Use deliver='origin' instead of polling for chat bots

In a bot context, deliver='origin' is simpler than polling: the parent can close its turn immediately and the result arrives as a new message when ready — no held connections.

Failure delivery behaviour

On failure, Background task failed: <error> is delivered to the same chat instead of the result. If the messenger rejects the send, the job stays delivered=False. The JOB_COMPLETED hook fires for both terminal states (COMPLETED and FAILED), and handler errors never crash the worker.


Related