Python SDK — ADK V2 Streaming
Agent execution, ADK V2 WebSocket streaming, callbacks, local tools, sessions, async requests, and skill generation
Updated 2026-11-03
Verified 2026-11-03
The Python SDK exposes ADK through client.adk and the higher-level client.send_message() helper. Use REST converse() for simple request/response calls. Use ADK V2 streaming when you need real-time chunks, local tool execution, model overrides, skills, images, or prompt-only agents.
Conversation User ID
For normal user tokens, user_id must match the Cosmos user id in the token. It is not a display id and it is not the email address.
claims = client.auth.decode_token()
conversation_user_id = claims.sub or claims.user_id
Passing a different value returns HTTP 403 with Token user is not authorized for the requested conversation user.
REST Converse
result = await client.adk.converse(
query="What were the main Q1 risks?",
user_id=conversation_user_id,
agent_id="agent-uuid",
agent_version="1.0",
channel="chat",
reasoning_event=True,
)
print(result["session_id"])
print(result["response"]["content"])
ADK owns session creation and persistence. Pass the same user_id and agent/app identity for normal multi-turn behavior.
V2 Streaming
client.send_message() keeps an ADK V2 WebSocket session open and auto-wires client.tool_manager definitions and handlers.
from cosmos_sdk import ADKV2SendOptions, ADKV2TurnCallbacks
callbacks = ADKV2TurnCallbacks(
on_chunk=lambda text: print(text, end="", flush=True),
on_reasoning=lambda content: print(f"\nReasoning: {content}"),
on_error=lambda code, message, fatal: print(f"\n{code}: {message}"),
)
result = await client.send_message(
"Analyze this support case.",
user_id=conversation_user_id,
callbacks=callbacks,
options=ADKV2SendOptions(
agent_id="agent-uuid",
agent_version="1.0",
provider_id="azure_openai",
model_id="gpt-4o",
session_state={"project_id": "project-uuid"},
),
)
For a follow-up turn, call send_message() again. Close the WebSocket when the conversation is done:
await client.send_message("What should we do next?", user_id=conversation_user_id)
client.close_adk_session()
Start Options
Common ADKV2SendOptions fields:
| Field | Purpose |
|---|---|
agent_id, agent_version | Run a configured agent |
app_id, app_version | Run a configured app |
system_prompt | Prompt-only mode or additional prompt input |
additional_instructions | Append request-specific instructions without replacing the agent prompt |
additional_collection_ids | Attach extra KB collections for the turn |
provider_id, model_id | Override model selection for this session |
temperature, max_output_tokens | Model generation controls |
skills | Attach explicit skill versions, e.g. [{"id": "...", "version": "1.0"}] |
tools | Client-side tool definitions |
exclude_tools | Remove named server-side tools from the agent prompt/tool set |
images | DMS URLs or image payloads for multimodal requests |
session_state | Initial state merged into the ADK session |
mcp_servers | Per-session server-side MCP server definitions |
execution_mode | "direct" or "plan_execute" |
app_name | Observability label override |
max_tool_roundtrips | Client-side guard against runaway tool loops |
Advanced fields such as mcp_servers and execution_mode should be used only when the target ADK deployment supports them.
ADK owns context compaction and history filtering. ADKV2SendOptions still accepts token_threshold, compaction_interval, overlap_size, and event_retention_size and forwards them on the start frame, but the ADK V2 start schema does not define them, so they have no effect. Leave them unset in new code.
Callbacks
callbacks = ADKV2TurnCallbacks(
on_chunk=lambda text: ...,
on_tool_start=lambda name: ...,
on_tool_complete=lambda name, result: ...,
on_tool_call_args_delta=lambda name, json_path, delta: ...,
on_reasoning=lambda content: ...,
on_error=lambda code, message, fatal: ...,
on_custom_event=lambda event_type, payload: ...,
on_plan_proposed=lambda plan_id, steps, summary: ...,
on_plan_step_start=lambda plan_id, step_id: ...,
on_plan_step_complete=lambda plan_id, step_id, status, preview: ...,
)
Plan approval and rejection helpers are intentionally not exposed by the Python SDK. Plan events are observable through callbacks, while execution control remains with the ADK protocol.
Local Tool Execution
Register tools on client.tool_manager; send_message() includes the schemas and dispatches tool calls to handlers.
from cosmos_sdk import ToolDefinition, ToolResult
async def get_order(args: dict) -> ToolResult:
order_id = args["order_id"]
return ToolResult(ok=True, data={"order_id": order_id, "status": "shipped"})
client.tool_manager.register_tool(
"get_order",
handler=get_order,
definition=ToolDefinition(
name="get_order",
description="Get an order by ID",
input_schema={
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
),
)
await client.send_message(
"Where is order 1001?",
user_id=conversation_user_id,
options=ADKV2SendOptions(agent_id="agent-uuid"),
)
The server sends tool_call; the SDK invokes the Python handler and sends tool_result back over the same WebSocket.
Tool Results and Errors
| Handler outcome | result sent | error sent |
|---|---|---|
ToolResult(ok=True, data=...) | data (strings as-is, other values JSON-encoded) | Not sent |
ToolResult(ok=False, error={"code": ..., "message": ...}) | JSON of the error dict | error["message"], or "Tool failed" when no message is set |
| Handler raises | JSON of {"code": "TOOL_EXECUTION_FAILED", "message": str(exc)} | str(exc) |
| No handler registered | JSON of {"code": "TOOL_NOT_FOUND", ...} | No handler registered for tool: <name> |
When error is present, ADK returns {"error": "<message>"} to the model as the tool response, so the model sees the real failure reason. ADK also returns error responses itself when a client tool times out, is cancelled, or the connection closes.
async def get_order(args: dict) -> ToolResult:
order = await orders.find(args["order_id"])
if order is None:
# The model receives: {"error": "Order 1001 not found"}
return ToolResult(ok=False, error={"code": "TOOL_EXECUTION_FAILED", "message": f"Order {args['order_id']} not found"})
return ToolResult(ok=True, data=order)
Server Events
ADK V2 emits lower-case event names:
| Event | Meaning |
|---|---|
session | Session ID assigned or resumed |
chunk | Streamed assistant text |
tool_call | Request for the client to run a local tool |
tool_call_args_delta | Partial function-call arguments while the model is still generating |
tool_start, tool_complete | Server-side tool lifecycle |
reasoning | Agent transfer, tool, file, image, skill, or RAG reasoning update |
custom_event | Service-defined extension event |
plan_proposed, plan_step_start, plan_step_complete | Plan/execute progress events |
turn_complete | Turn finished with usage and session ID |
error | Error with code, user-facing message, and fatal flag |
ping | Keepalive; SDK responds automatically |
Fatal errors close the WebSocket and should be treated as terminal for that turn.
Async Requests
Use async requests for long-running work where the caller does not need a live stream.
queued = await client.adk.submit_async(
query="Generate a compliance report",
user_id=conversation_user_id,
agent_id="agent-uuid",
agent_version="1.0",
callback_url="https://app.example.com/webhooks/adk",
)
final = await client.adk.poll_async_until_done(queued["request_id"])
print(final["status"])
Sessions
sessions = await client.adk.list_sessions(user_id=conversation_user_id, agent_id="agent-uuid")
events = await client.adk.get_session_events(sessions[0]["id"])
await client.adk.update_session_metadata(
sessions[0]["id"],
{"title": "Q1 risk review", "tags": ["risk", "q1"]},
)
Skill Generation
result = await client.generate_skills(
source="description",
description="Help support agents resolve refund requests",
provider_id="azure_openai",
model_id="gpt-4o",
max_skills=3,
)