Skip to content

feat: implement Python SDK v5 - #8

Draft
varin-nair-factory wants to merge 11 commits into
mainfrom
vn/python-sdk-v5
Draft

feat: implement Python SDK v5#8
varin-nair-factory wants to merge 11 commits into
mainfrom
vn/python-sdk-v5

Conversation

@varin-nair-factory

Copy link
Copy Markdown
Contributor

Summary

  • Add the typed Python SDK v5 API with an exact low-level namespace.
  • Implement lifecycle-safe sessions and streaming, structured output, attachments, interactions, MCP, discovery, and observability.
  • Add complete reference docs, runnable examples, strict typing, and package validation.

Validation

  • 1,452 passed, 8 skipped; 93.28% coverage.
  • Ruff, mypy, Pyright, typing fixtures, compileall, lockfile, build, and wheel smoke checks passed.
  • All 12 examples executed, including live authenticated flows.

Add the typed high-level API, exact low-level namespace, lifecycle-safe streaming and sessions, MCP, observability, documentation, examples, and comprehensive tests.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@varin-nair-factory varin-nair-factory self-assigned this Aug 12, 2026
varin-nair-factory and others added 10 commits August 11, 2026 17:29
Core install now depends only on pydantic and typing-extensions;
mcp/starlette/uvicorn move to the "droid-sdk[mcp]" extra. Importing
droid_sdk.mcp without the extra raises a clear ImportError, and external
MCP server configs stay importable from the package root.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
DroidClient no longer imports InteractionDispatcher or carries a second
handler mechanism. Session owns one dispatcher and registers it through
the existing set_permission_handler/set_ask_user_handler slots, swapping
only the dispatcher error sink per turn.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Session and streaming each carried their own copies of the wire<->public
converters (settings, MCP status, usage, notification extraction), and the
copies had already diverged: session's settings merge re-parsed raw camelCase
dicts by hand with per-field fallbacks while streaming validated typed
payloads. All shared conversions now live in _high_level/_convert.py with one
strictly typed implementation per concept, and the settings merge goes
through the validated SettingsUpdatedPayload model with model_fields_set
deciding which explicit nulls clear a value.

The third copy of _consume_task_result (transport, session, streaming) moves
to droid_sdk/_util.py.

The session test fixture now builds a real wire SessionSettings model instead
of a SimpleNamespace so converter changes are exercised against the actual
schema.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…t hook

The open, close, and replacement paths each hand-rolled the same
cancellation-safety idiom: shield-await a shared task, run compensation when
the caller is cancelled, and cancel-then-drain tasks that must not outlive
their operation. Those idioms now live once in droid_sdk._util
(wait_shielded, cancel_and_drain), and the replacement path's inline
compensation becomes the named _abandon_cancelled_replacement method.

The module-global _replacement_handoff_checkpoint existed only so tests could
monkeypatch it. The cancellation delivery point is now an instance seam
(Session._replacement_checkpoint, defaulting to _util.cancellation_checkpoint)
that tests gate per session without touching module state.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Successor sessions were cloned with object.__new__ plus a __dict__ copy and a
trailing list of field resets, so every new Session attribute silently leaked
into successors unless someone remembered to reset it here. The successor is
now built through Session's real constructor, and _create_successor names
exactly which state carries over: load policies, and the live wiring (client,
dispatcher, in-process MCP servers) the successor adopts.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
session.py fused the lifecycle state machine with every request/response
operation the session exposes. The plain one-call-one-request methods
(settings, tools, skills, MCP management, context, spec mode) and the
replacement-producing operations (fork, compact, rewind) now live in
SessionOperationsMixin in _session_operations.py, with the state and helpers
they rely on declared explicitly at the top of the mixin. session.py keeps
the cohesive open/close/replacement state machine, streaming turn
management, and run(), dropping from 1,998 lines at review time to 1,166.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
client.py disabled reportCallIssue, reportUnknownMemberType, and
reportUnknownVariableType for the whole file. The unknown-type escapes were
four real gaps in _dispatch_notification's payload probing, fixed with
explicit narrowing. What remains is only reportCallIssue, which pyright
raises because its dataclass_transform view of the request-param models sees
the camelCase wire aliases instead of the snake_case names that
populate_by_name accepts; mypy's pydantic plugin validates those calls. The
header now documents exactly that.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
SdkMcpServer smuggled mutable runtime state through object-typed
fields on a frozen dataclass, forcing droid_sdk.mcp to read and write
them via object.__getattribute__/__setattr__ plus casts. The handle is
now honestly frozen: droid_sdk.mcp keeps a WeakKeyDictionary of typed
per-server state, so the attribute smuggling and its casts are gone.
The handle drops slots (weak references need __weakref__) and field
equality (registry keys need an identity hash; the field-based hash
was already unusable on unhashable tool schemas).

Also port the server start/close single-flight to the shared
wait_shielded/cancel_and_drain helpers, and name RunStream's finish
grace period (_FINISH_GRACE_SECONDS) instead of an inline 0.1.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Full-branch simplify pass. The timezone-awareness validator was copied
verbatim in config.py and messages.py; it now lives once in _immutable
as ensure_aware. The three terminal result dataclasses repeated the
same messages/structured_output freezing in their __post_init__, now
shared as _seal_terminal_result.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Collapse the repeated-type continuation rows in the field and enum
tables into one row per type, remove the duplicated
SessionSettingsUpdate and MCP method tables, fold the two attachment
examples into one, and trim prose that restated adjacent tables.
No factual changes; claims re-verified against the source.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant