SDK Release Notes
Version-by-version changes to the Python SDK and the API surface it calls, written for developers upgrading an existing integration.
Not the same as platform release notes
The SDK is versioned separately from the Xagent platform. This page tracks the xagent-sdk package; platform releases are indexed at docs.xagent.co/release-notes under their own numbering. A lower version number here does not mean the SDK is behind — the two counters are unrelated.
v0.4.0
Adds a supported way to see and answer a question an agent asks mid-task. Before this release there was no correct way to answer one from the API.
Install
Install from git, pinned to a tag:
pip install "xagent-sdk @ git+https://github.com/xorbitsai/xagent-sdk@v0.4.0#subdirectory=python"Server requirement
v0.4.0 requires a server deployed on or after 2026-08-13. Against an older server the new field is always null and the new endpoint does not exist — see Compatibility.
Seeing a pending question
GET /v1/chat/tasks/{task_id} gained a pending_interaction field. It carries the question text and any structured input descriptors while the task sits in waiting_for_user, and is null otherwise.
{
"task_id": 1234,
"agent_id": 42,
"status": "waiting_for_user",
"run_id": "412a3072-5f4d-4b75-b78a-e16a015f618c",
"state_version": 2,
"control_state": "waiting_for_user",
"output": null,
"error": null,
"pending_interaction": {
"question": "Which deployment region should I use for the report?",
"interactions": [
{
"type": "select_one",
"field": "deployment_region",
"label": "Deployment region",
"options": [
{"label": "Singapore", "value": "Singapore"},
{"label": "Australia", "value": "Australia"},
{"label": "Both", "value": "Both"}
]
}
]
}
}On the SDK side this is TaskInfo.pending_interaction:
info = client.tasks.wait(task_id) # returns as soon as the task blocks on the user
if info.status == TaskStatus.WAITING_FOR_USER:
pending = info.pending_interaction
if pending is not None: # can be None -- see below
print(pending.question) # the question text
print(pending.interactions) # structured descriptors, or None| Field | Meaning |
|---|---|
question | The question text shown to the user. Non-empty when the task is waiting and a question was recorded. |
interactions | A list of structured input descriptors, such as the single-select control above. May also be null — that is what you get when the agent asked in plain text, with no form attached. |
Two nullability cases you must handle
interactionsmay benullor an empty list[]. Both are passed through as-is with no normalization, and clients should treat them the same way — both mean "no structured control; answer in plain text".pending_interactionitself may benulleven while status iswaiting_for_user. This happens when a task enters the waiting state without ever recording a question, when the persisted question cannot be parsed, or when its protocol version is not one the server recognizes.reply()still works in that case.- There is a third case in between: if the question was recorded but its saved execution progress cannot be resumed (an unavailable or missing checkpoint),
pending_interactionis not null — you still get thequestiontext, butinteractionscomes back empty.reply()against this task returnsinteraction_not_resumable(409); start a new task instead. See the error table on the Reply to a waiting task reference.
Answering a pending question
New endpoint: POST /v1/chat/tasks/{task_id}/reply. It delivers the answer into the original run and resumes it, rather than starting a new turn. Full reference: Reply to a waiting task.
POST /v1/chat/tasks/1234/reply
Authorization: Bearer <api-key>
Content-Type: application/json
{"agent_id": 42, "message": {"role": "user", "content": "Australia"}}SDK:
result = client.tasks.reply(task_id, agent_id=agent_id, message="Australia")
# AppendResult(task_id=1234, agent_id=42,
# status=<TaskStatus.RUNNING>, accepted_at=2026-08-14 05:24:03.819620+00:00)
final = client.tasks.wait(task_id)
# status=completed
# output='You chose the Australia deployment region.'
# pending_interaction=NoneThe answer is plain text for now. The "Australia" above happens to match an option's value, but the server does not validate it and does not require you to send one of the listed values — it is simply a user turn for the agent to interpret. Answering per field with structured values is a later capability.
One observable fact: run_id stays the same across the whole exchange while state_version advances. That is what distinguishes reply() — it resumes the original run, whereas append() starts a new turn.
No duplicate-submission protection
If reply() times out, do not blindly retry — call get() first and check the status. If it has left waiting_for_user, the answer most likely landed. Note the subtlety: seeing waiting_for_user again can also mean the answer landed and the agent immediately asked a new question. Comparing pending_interaction.question narrows this down, but a repeated question with identical text is still indistinguishable.
Behaviour changes on other endpoints
The error code for append() on a waiting task changed. It previously returned 409 task_busy, whose message said "task is busy, retry after it completes" — but a waiting task never completes on its own, so retrying was a dead end. It now returns a code that points to the right endpoint:
POST /v1/chat/tasks/1234/messages → 409
{"error": {
"code": "interaction_response_required",
"message": "This task is waiting for an answer to a pending question; use the task reply endpoint (POST .../reply) instead."
}}The SDK raises InteractionResponseRequired.
Action required if you branch on error codes
The branch that used to catch task_busy will no longer see it for this case, so add handling for interaction_response_required. Any retry loop built on the old code can be removed — it could never have succeeded.
Four new error codes. These are listed with every other code in the API Overview error table.
| Code | HTTP | SDK exception | Meaning |
|---|---|---|---|
interaction_response_required | 409 | InteractionResponseRequired | The task is waiting for an answer; use reply() |
no_pending_interaction | 409 | NoPendingInteraction | The task has no pending question right now (completed, failed, paused, or not yet started) |
interaction_not_resumable | 409 | InteractionNotResumable | The run cannot be resumed; retrying will not help |
temporarily_unavailable | 503 | TemporarilyUnavailable | Transient failure; safe to retry after a short backoff |
append()'s role, clarified. append() starts a new turn, and is never the way to answer a pending question. Older SDK documentation told callers to "resume with append() once the task reaches waiting_for_user" — that was wrong, and the server always rejected it. The docstrings, README and examples have all been corrected.
Compatibility
| Combination | Behaviour |
|---|---|
| New SDK + old server | pending_interaction is always None; reply() hits an unknown route and surfaces as the generic internal error. Nothing crashes. |
| Old SDK + new server | The new field is ignored and behaviour is unchanged. The waiting-task append() error code changed, and an old SDK maps it to its generic error. |
| Task status enum | No new status values — still pending / running / paused / waiting_for_user / completed / failed. |