@@ -174,7 +174,9 @@ def handle_message(msg: StreamMessage) -> None:
174174 print (f " Tool call: { msg.tool_name} ( { msg.tool_input} ) " )
175175 elif isinstance (msg, ToolResult):
176176 status = " ❌" if msg.is_error else " ✅"
177- print (f " { status} { msg.content} " )
177+ # tool_use_id correlates the result with its ToolUse; tool_name is
178+ # backfilled from that call (None if the call was never seen).
179+ print (f " { status} [ { msg.tool_use_id} ] { msg.tool_name} : { msg.content} " )
178180 elif isinstance (msg, ToolProgress):
179181 print (f " ⏳ { msg.tool_name} : { msg.content} " )
180182 elif isinstance (msg, WorkingStateChanged):
@@ -184,7 +186,9 @@ def handle_message(msg: StreamMessage) -> None:
184186 elif isinstance (msg, TurnComplete):
185187 print (" \n --- Turn complete ---" )
186188 elif isinstance (msg, ErrorEvent):
187- print (f " Error [ { msg.error_type} ]: { msg.message} " )
189+ # error_type is often the unhelpful "Error"; error_name exposes the
190+ # nested error.name (e.g. "LLMInvalidRequestError") to branch on.
191+ print (f " Error [ { msg.error_name or msg.error_type} ]: { msg.message} " )
188192```
189193
190194## Permission Handler
@@ -264,12 +268,52 @@ async def main():
264268The main client class. Wraps a transport and provides typed async methods for all ` droid.* ` RPC methods.
265269
266270** Session methods:**
267- - ` initialize_session(...) ` — Create a new session
271+ - ` initialize_session(...) ` — Create a new session (supports ` enabled_tool_ids ` and ` disabled_tool_ids ` )
268272- ` load_session(session_id=...) ` — Load an existing session
269- - ` add_user_message(text=...) ` — Send a user message
273+ - ` add_user_message(text=..., output_format=... ) ` — Send a user message, optionally with a structured-output (JSON Schema) contract
270274- ` interrupt_session() ` — Interrupt the current session
271275- ` kill_worker_session(worker_session_id=...) ` — Kill a worker session
272- - ` update_session_settings(...) ` — Update session settings
276+ - ` update_session_settings(...) ` — Update session settings (supports ` enabled_tool_ids ` /` disabled_tool_ids ` )
277+ - ` close_session(reason=...) ` — Close the active session
278+ - ` compact_session(custom_instructions=...) ` — Compact the conversation to reclaim context
279+ - ` fork_session(title=..., tags=...) ` — Fork the session into a new one
280+ - ` rename_session(title=...) ` — Rename the session
281+
282+ ** Discovery methods:**
283+ - ` list_tools(...) ` — List native CLI tools with ` default_allowed ` /` currently_allowed ` (useful for locking the tool set down)
284+ - ` list_commands() ` — List custom slash commands
285+
286+ ** Context and rewind methods:**
287+ - ` get_context_stats() ` — Context-window usage (used/remaining/limit)
288+ - ` get_context_breakdown() ` — Per-category/skill/MCP/droid token breakdown
289+ - ` get_rewind_info(message_id=...) ` — Restorable/created/evicted files for a rewind point
290+ - ` execute_rewind(...) ` — Rewind to a message, forking the session
291+
292+ ** Locking the tool set down:**
293+
294+ ``` python
295+ # enabled_tool_ids is additive, so pass an explicit disable list to
296+ # actually restrict native tools. list_tools() lets you verify the result.
297+ catalog = await client.list_tools()
298+ tool_ids = [t.id for t in catalog.tools]
299+ await client.update_session_settings(enabled_tool_ids = [], disabled_tool_ids = tool_ids)
300+ ```
301+
302+ ** Structured output:**
303+
304+ ``` python
305+ await client.add_user_message(
306+ text = " Return an answer." ,
307+ output_format = {
308+ " type" : " json_schema" ,
309+ " schema" : {
310+ " type" : " object" ,
311+ " properties" : {" answer" : {" type" : " integer" }},
312+ " required" : [" answer" ],
313+ },
314+ },
315+ )
316+ ```
273317
274318** MCP methods:**
275319- ` toggle_mcp_server(...) ` — Enable/disable an MCP server
@@ -308,7 +352,16 @@ Protocol (interface) that all transport implementations must satisfy. Use this t
308352uv sync
309353
310354# Run tests
311- uv run pytest
355+ uv run --group dev python -m pytest
356+
357+ # Run opt-in tests against the installed, authenticated droid exec CLI.
358+ # These create real sessions and consume model usage.
359+ DROID_LIVE_TESTS=1 uv run --group dev python -m pytest \
360+ tests/test_live_droid_exec.py -v
361+
362+ # Override the executable path when droid is not on PATH.
363+ DROID_LIVE_TESTS=1 DROID_EXEC_PATH=/path/to/droid \
364+ uv run --group dev python -m pytest tests/test_live_droid_exec.py -v
312365
313366# Type check (strict mode)
314367uv run mypy --strict src/
0 commit comments