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.
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.
Basic usage
AG-UI event types
Events are typed as a looseAguiEvent = 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 aCUSTOM 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:
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-Afterup tomax_retries). - If the server responds with a normal JSON error envelope instead of starting the stream (e.g. the
400 EXTERNAL_USER_REQUIREDabove), the error is raised as an exception — it does not appear as an event. - There is no
contextOverrideor cancellation parameter in the Python SDK (unlike the Node SDK). For a timeout/cancellation, wrap the call inasyncio.wait_for(...)or cancel the enclosingasyncio.Taskyourself — a documented gap, not a hidden one. run.error-style structured failures:ChatResulthas noerrorfield in this SDK — a failed run surfaces as a raisedPersonaApiErrorrather than a returned event. See Errors.