Gateway Threads

Open a native platform thread/topic under a channel — capability-gated, never raises

An agent opens a native thread (Telegram forum topic, Discord thread, Slack thread) under a channel and posts a follow-up into it — falling back to the parent channel when the platform can’t thread.

Diagram

Gateway Threads

Yes

No

🤖 Agent

🔀 DeliveryRouter

🔍 supports_threads?

🧩 Adapter

💬 Platform Thread

↩️ Parent Channel

Quick Start

  1. Open a thread and post into it
    python
    from praisonaiagents import Agent
    
    agent = Agent(
        name="assistant",
        instructions="Group replies by topic when the channel supports threads.",
    )
    
    result = agent.gateway.create_thread(target="telegram:123456", name="daily-standup")
    if result.ok:
        agent.gateway.send(target=f"{result.target}:{result.thread_id}", text="Standup starting…")
    else:
        # unsupported | failed | no_route — fall back to the parent channel
        agent.gateway.send(target="telegram:123456", text="Standup starting…")
  2. Read the typed outcome
    python
    from praisonaiagents.gateway import ThreadResult
    
    result: ThreadResult = agent.gateway.create_thread(target="slack:C123", name="research")
    
    result.status      # "ok" | "unsupported" | "failed" | "no_route"
    result.ok          # True only when status == "ok"
    result.thread_id   # the new thread id (set on "ok", "" otherwise)
    result.target      # the resolved parent target

How It Works

create_thread resolves the target, checks the channel’s supports_threads capability, and dispatches to the adapter — returning a typed outcome instead of raising.

Diagram
PlatformAdapterDeliveryRouterAgentPlatformAdapterDeliveryRouterAgentalt[supports_threads is False][supports_threads is True]create_thread(target, name)resolve targetThreadResult(status=”unsupported”, thread_id=””)send to parent channelcreate_thread(channel_id, name)open native thread/topicthread_idthread_idThreadResult(status=”ok”, thread_id=…)send into “<target>:<thread_id>”

Status Decision Guide

Each call resolves to exactly one ThreadStatus — map it to the right caller action.

Diagram

ok

unsupported

no_route

failed

ThreadResult.status

ok?

✅ Use thread_id

↩️ Fall back to parent channel

🛑 Stop — log the bad target

↩️ Retry-safe fallback to parent


Configuration Options

ThreadResult (from praisonaiagents.gateway) is the typed return of every create_thread call.

FieldTypeDefaultDescription
statusThreadStatus(required)Outcome: "ok", "unsupported", "failed", or "no_route"
targetstr""Resolved parent target the thread was opened under
thread_idstr""New thread/topic id — populated only when status == "ok"
detailOptional[str]NoneModel-readable explanation (set on non-ok outcomes)
okbool (property)—Convenience for status == "ok"

ThreadStatus is a string literal — one of the four values below.

ValueMeaningCaller action
"ok"A new thread/topic was opened; thread_id carries its idSend into "<target>:<thread_id>"
"unsupported"The channel has no supports_threads capabilityFall back to the parent channel
"failed"The transport rejected the attempt (topics mode off, or missing permission)Retry-safe fallback to parent
"no_route"The target could not be resolved to a reachable channelStop and log the bad target

The capability flag lives on the adapter’s PlatformCapabilities.

FieldTypeDefaultDescription
supports_threadsboolFalseWhether the adapter can open a native thread/topic under a chat

Fallback Pattern

Try the thread, and route to the parent channel on any non-ok outcome.

python
from praisonaiagents import Agent

agent = Agent(name="assistant", instructions="Scope subtasks into threads when possible.")

parent = "slack:C123"
result = agent.gateway.create_thread(target=parent, name="research")

# Compose the routed target only when a thread was actually opened
destination = f"{result.target}:{result.thread_id}" if result.ok else parent
agent.gateway.send(target=destination, text="Kicking off the research subtask…")

Authoring a Custom Adapter

A custom OutboundMessengerProtocol implementation declares the capability and returns a ThreadResult.

python
from praisonaiagents.gateway import ThreadResult
from praisonaiagents.bots.protocols import PlatformCapabilities


class MyAdapter:
    @property
    def capabilities(self) -> PlatformCapabilities:
        return PlatformCapabilities(supports_threads=True)

    async def create_thread(self, target: str, name: str) -> ThreadResult:
        thread_id = await self._open_native_topic(target, name)
        if not thread_id:
            return ThreadResult(status="failed", target=target)
        return ThreadResult(status="ok", target=target, thread_id=str(thread_id))

Per-Platform Support

Support tracks each adapter’s supports_threads capability.

Channelsupports_threadsNotes
TelegramYesForum topics
DiscordYesNative threads
SlackYesThread anchoring (thread_ts)
WhatsAppNoNo native threads → unsupported
EmailNoNo native threads → unsupported

Common Patterns

Scope a multi-agent subtask into its own thread so it doesn’t flood the shared channel.

python
result = agent.gateway.create_thread(target="discord:987654", name="deploy-run")
target = f"{result.target}:{result.thread_id}" if result.ok else "discord:987654"
agent.gateway.send(target=target, text="Deploy started — updates will land here.")

Branch parallel workflow steps into separate threads, each falling back independently.

python
parent = "telegram:123456"
for name in ("build", "test", "ship"):
    r = agent.gateway.create_thread(target=parent, name=name)
    agent.gateway.send(
        target=f"{r.target}:{r.thread_id}" if r.ok else parent,
        text=f"{name} stage running…",
    )

Best Practices

Always compose the routed target from the result

Send into f"{result.target}:{result.thread_id}" only when result.ok is True. On any other status, send to the parent target so the message still lands.

Skip try/except — the call never raises

create_thread resolves to a typed unsupported / failed / no_route outcome instead of throwing. Branch on result.status, not on exceptions.

Treat no_route as a bug, not a fallback

"no_route" means the target string didn’t resolve to a reachable channel — log it and fix the target rather than silently retrying.

Declare supports_threads on custom adapters

Return PlatformCapabilities(supports_threads=True) and implement create_thread together. Declaring the capability without the method leaves the router returning unsupported.


Related