Skip to main content
persona.chat (sync: ChatClient, async: AsyncChatClient) wraps /api/v1/developer/agui — it runs an Agent as the asserted external user, streaming the response over SSE.
Chat requires the client to have been constructed with external_user_id. A chat run always executes as a specific end user — a bare Project credential has no Subject to run as, and the server rejects it with 400 EXTERNAL_USER_REQUIRED.
ChatClient.stream() (sync) is a regular generator; AsyncChatClient.stream() (async) is an async generator — the natural per-language idiom for the same AG-UI event stream. send_message() and stream() take messages/thread_id/resume as direct keyword parameters rather than one options object.

Methods

messages: list[{"role": "user" | "assistant", "content": str}] — plain text only (this SDK doesn’t yet support multi-part/multimodal content on the way in). thread_id resumes a named Thread from threads.create() instead of the implicit deterministic one-conversation-per-Agent- per-user thread. resume — see Human-in-the-loop.
thread_id must belong to the asserted external user and the agent_id you’re calling with — passing one that doesn’t (someone else’s Thread, a different Agent’s Thread, or a nonexistent id) raises PersonaApiError with status_code: 404. It does not silently fall back to a different conversation.

Basic usage

Async is the same shape:

AG-UI event types

Events are typed as a loose AguiEvent = dict[str, Any] rather than pulling in an AG-UI protocol package — this SDK doesn’t depend on one, mirroring the Node SDK’s own call to reject @ag-ui/client’s heavier dependency chain in favor of a hand-rolled SSE parser. Every event has a type key (compare against EventType constants — note EventType is a plain class of string constants, not a real Python enum, so no .value unwrapping needed) plus type-specific fields (delta, name, value, etc.).

send_message() — ChatResult

send_message() drains the full run and returns a ChatResult:

Human-in-the-loop: interrupts and resuming

If an Agent’s configuration requires confirming a sensitive tool call, or the run needs a clarifying answer from the user, the run pauses instead of finishing normally and emits a CUSTOM event instead of RUN_FINISHED. send_message() detects this for you and sets result["interrupt"]:
Resume on the next send_message()/stream() call for the same Thread, with messages=[] (no new user message — you’re answering the interrupt, not starting a new turn) and resume set:
The resume shape you pass must match the pending interrupt’s kind: hitl expects {"decisions": [...]}, clarification expects {"answers": [...], "text": ...}. The full approve/reject flow is in Workflows.

Streaming behavior

  • The stream is delivered over SSE (text/event-stream) and framed by the SDK’s own tolerant parser — malformed or partial frames are skipped silently, never raised.
  • 429 retries apply to streaming requests exactly like regular JSON calls (honoring Retry-After up to max_retries).
  • If the server responds with a normal JSON error envelope instead of starting the stream (e.g. the 400 EXTERNAL_USER_REQUIRED above), the error is raised as an exception — it does not appear as an event.
  • There is no contextOverride or cancellation parameter in the Python SDK (unlike the Node SDK). For a timeout/cancellation, wrap the call in asyncio.wait_for(...) or cancel the enclosing asyncio.Task yourself — a documented gap, not a hidden one.
  • run.error-style structured failures: ChatResult has no error field in this SDK — a failed run surfaces as a raised PersonaApiError rather than a returned event. See Errors.