driftstack-sdk 0.1.5__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- driftstack_sdk-0.3.0/.gitignore +17 -0
- driftstack_sdk-0.3.0/CHANGELOG.md +438 -0
- driftstack_sdk-0.3.0/LICENSE +21 -0
- driftstack_sdk-0.3.0/PKG-INFO +350 -0
- driftstack_sdk-0.3.0/README.md +314 -0
- {driftstack_sdk-0.1.5 → driftstack_sdk-0.3.0}/pyproject.toml +47 -3
- driftstack_sdk-0.3.0/src/driftstack/__init__.py +137 -0
- driftstack_sdk-0.3.0/src/driftstack/_generated/models.py +2730 -0
- {driftstack_sdk-0.1.5 → driftstack_sdk-0.3.0}/src/driftstack/_version.py +1 -1
- driftstack_sdk-0.3.0/src/driftstack/client.py +206 -0
- driftstack_sdk-0.3.0/src/driftstack/errors.py +684 -0
- driftstack_sdk-0.3.0/src/driftstack/http.py +1248 -0
- driftstack_sdk-0.3.0/src/driftstack/pagination.py +103 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/account.py +171 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/agent_sessions.py +996 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/api_keys.py +119 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/archetypes.py +47 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/audit_log.py +98 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/auth.py +187 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/billing.py +54 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/crypto_orders.py +271 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/egress.py +119 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/email_preferences.py +68 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/legal.py +52 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/mfa.py +74 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/profile_snapshots.py +141 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/profiles.py +290 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/recipes.py +149 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/sessions.py +353 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/team.py +197 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/usage.py +47 -0
- driftstack_sdk-0.3.0/src/driftstack/resources/webhooks.py +231 -0
- {driftstack_sdk-0.1.5 → driftstack_sdk-0.3.0}/src/driftstack/retry.py +36 -12
- driftstack_sdk-0.3.0/src/driftstack/webhook_signature.py +165 -0
- driftstack_sdk-0.1.5/.gitignore +0 -45
- driftstack_sdk-0.1.5/PKG-INFO +0 -226
- driftstack_sdk-0.1.5/README.md +0 -191
- driftstack_sdk-0.1.5/src/driftstack/__init__.py +0 -64
- driftstack_sdk-0.1.5/src/driftstack/_generated/models.py +0 -490
- driftstack_sdk-0.1.5/src/driftstack/client.py +0 -120
- driftstack_sdk-0.1.5/src/driftstack/errors.py +0 -240
- driftstack_sdk-0.1.5/src/driftstack/http.py +0 -324
- driftstack_sdk-0.1.5/src/driftstack/resources/api_keys.py +0 -61
- driftstack_sdk-0.1.5/src/driftstack/resources/sessions.py +0 -156
- driftstack_sdk-0.1.5/src/driftstack/resources/usage.py +0 -29
- driftstack_sdk-0.1.5/src/driftstack/resources/webhooks.py +0 -110
- driftstack_sdk-0.1.5/src/driftstack/webhook_signature.py +0 -106
- {driftstack_sdk-0.1.5 → driftstack_sdk-0.3.0}/src/driftstack/_generated/__init__.py +0 -0
- {driftstack_sdk-0.1.5 → driftstack_sdk-0.3.0}/src/driftstack/py.typed +0 -0
- {driftstack_sdk-0.1.5 → driftstack_sdk-0.3.0}/src/driftstack/resources/__init__.py +0 -0
- {driftstack_sdk-0.1.5 → driftstack_sdk-0.3.0}/src/driftstack/resources/_common.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Build and tool caches for the Python SDK. Every pattern here is already
|
|
2
|
+
# covered by the repository root .gitignore, so this file changes nothing about
|
|
3
|
+
# what git tracks.
|
|
4
|
+
#
|
|
5
|
+
# It exists for the SDIST. Hatchling force-includes the nearest .gitignore above
|
|
6
|
+
# the package root into `driftstack_sdk-<version>.tar.gz`, and `exclude` does
|
|
7
|
+
# not override a force-include. Without this file the nearest one is the
|
|
8
|
+
# repository root's, which PyPI would then serve to anyone who downloads the
|
|
9
|
+
# source distribution. Pinned by the packaging guard in apps/server/tests/unit.
|
|
10
|
+
.venv/
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
dist/
|
|
17
|
+
build/
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `driftstack` Python SDK. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
|
|
5
|
+
follows [SemVer](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-09-22
|
|
10
|
+
|
|
11
|
+
**Nothing was removed.** Every name 0.2.0 exported, every method, every
|
|
12
|
+
keyword argument and every error class is still here and still means the
|
|
13
|
+
same thing, so upgrading takes no code change on its own — except where
|
|
14
|
+
**Migration** below says a string comparison needs updating. Every addition
|
|
15
|
+
below exists on **both** `Driftstack` and `AsyncDriftstack`.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`EgressCapabilities.safeguards`** — `"passed"`, `"failed"` or
|
|
20
|
+
`"unverified"`, summarising whether every egress safeguard held for a
|
|
21
|
+
session. `"failed"` wins whenever any check did not pass; `"passed"` only
|
|
22
|
+
when the device declared the full set of checks a healthy session reports
|
|
23
|
+
and every one of them reported back; `"unverified"` otherwise. The field is
|
|
24
|
+
**absent**, not `None`, on a session reported before it existed — do not
|
|
25
|
+
read an absent value as `"unverified"` or `"passed"`. Rides everywhere
|
|
26
|
+
`egress_capabilities` already does: `sessions.get()` / `.list()` /
|
|
27
|
+
`.create()`, `profiles.launch()`, and the
|
|
28
|
+
`session.egress_capability_changed` webhook payload.
|
|
29
|
+
- **`measured_by`** on a `?check=full` proxy test result
|
|
30
|
+
(`egress.test_proxy(proxy_id)`) — `"phone"` when a real phone session took
|
|
31
|
+
the measurement, `"driftstack"` when Driftstack itself did because no phone
|
|
32
|
+
could be reached in time. The field this replaces, `measured_from`, is
|
|
33
|
+
still sent beside it with its original values for existing integrations,
|
|
34
|
+
but is no longer documented; read `measured_by` from here on.
|
|
35
|
+
- **`direct_reading` and `website_like_reading`** on `os_fingerprint` — the
|
|
36
|
+
same two facts `single_host_vantage` and `web_port_vantage` already carry,
|
|
37
|
+
under plainer names, added beside the originals rather than replacing
|
|
38
|
+
them. Present on the proxy test result **and** on each saved
|
|
39
|
+
proxy returned by `egress.list_proxies()` / `.update_proxy(proxy_id, body)`.
|
|
40
|
+
- **`"page_unreadable"`** joins the `AgentNoticeReason` values a
|
|
41
|
+
`plan-executed` result can carry: the page could not be read to plan the
|
|
42
|
+
next step, so the task stopped rather than guess. Send `continue` to try
|
|
43
|
+
again.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **Two dead `egress_capabilities.warnings` codes are retired from the
|
|
48
|
+
documentation**: `quic_disabled_fallback_http2` and
|
|
49
|
+
`dns_remote_resolve_unsupported_by_proxy`. Neither has ever been sent, so
|
|
50
|
+
this is a documentation correction, not a behavioural change.
|
|
51
|
+
|
|
52
|
+
### Migration
|
|
53
|
+
|
|
54
|
+
Two closed-string fields were narrowed — values removed, not added — inside
|
|
55
|
+
the `?check=full` proxy test result. `egress.test_proxy()` and
|
|
56
|
+
`egress.list_proxies()` return a plain `dict`, not a validated model, so the
|
|
57
|
+
affected `AccountProxyTestResult*` / `OsFingerprint` models in
|
|
58
|
+
`driftstack._generated.models` are typing-only for these two fields: neither
|
|
59
|
+
change raises at call time, and code comparing a value against one of the old
|
|
60
|
+
strings simply stops matching, silently. The server has sent the new values
|
|
61
|
+
only since 2026-09-21.
|
|
62
|
+
|
|
63
|
+
- **`not_run`** — `"node_busy"`, `"node_error"` and `"no_node"` merged into
|
|
64
|
+
`"check_unavailable"` (you can do exactly one thing about any of the
|
|
65
|
+
three: try again shortly, or contact support if it persists);
|
|
66
|
+
`"unresolvable"` is now `"config_unresolvable"`, matching the word the
|
|
67
|
+
"why a launch is refused" vocabulary already used for the identical fact.
|
|
68
|
+
`"live_session"` is unchanged.
|
|
69
|
+
- **`os_fingerprint_unavailable`** — `"vpn_tunnel"` is now
|
|
70
|
+
`"not_available_for_vpn"`, `"not_observed"` is now `"not_captured"`, and
|
|
71
|
+
`"observer_off"` is now `"not_offered_here"`.
|
|
72
|
+
|
|
73
|
+
Update any code that compares `not_run` or `os_fingerprint_unavailable`
|
|
74
|
+
against one of the old strings to compare against its replacement instead.
|
|
75
|
+
|
|
76
|
+
### Pre-1.0 stability
|
|
77
|
+
|
|
78
|
+
The SDK is pre-1.0. Pin `driftstack-sdk~=0.3.0` rather than an exact version
|
|
79
|
+
and read this file before bumping.
|
|
80
|
+
|
|
81
|
+
## [0.2.0] - 2026-09-20
|
|
82
|
+
|
|
83
|
+
The release the guide [Run AI tasks from your
|
|
84
|
+
code](https://docs.driftstack.io/guides/run-ai-tasks-from-code/) is written
|
|
85
|
+
against — 0.1.5 cannot run any of its examples, because the AI agent was not
|
|
86
|
+
reachable from it at all.
|
|
87
|
+
|
|
88
|
+
**Nothing was removed.** Every name 0.1.5 exported, every method, every
|
|
89
|
+
keyword argument and every error class is still here and still means the same
|
|
90
|
+
thing, so upgrading takes no code change. What grew: both clients went from 4
|
|
91
|
+
resources to 19, and `driftstack.__all__` from 22 names to 56. Every addition
|
|
92
|
+
below exists on **both** `Driftstack` and `AsyncDriftstack`. Read **Changed**
|
|
93
|
+
before you upgrade anyway — a few behaviours differ, and one of them changes
|
|
94
|
+
which exception an `except` clause sees.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
#### Run an AI task end to end
|
|
99
|
+
|
|
100
|
+
`client.agent_sessions` is new, and is the whole AI surface.
|
|
101
|
+
|
|
102
|
+
- **Start it, send the task, close it** — `create(body, ...)` opens a
|
|
103
|
+
session, on a saved profile if you pass one; `message(id, text, ...)` sends
|
|
104
|
+
the task in plain words and returns what happened; `get(id)`,
|
|
105
|
+
`list(...)` and `iterate(...)` read sessions back; `close(id)` ends one and
|
|
106
|
+
saves the profile's sign-in. A new session is `provisioning` until its
|
|
107
|
+
browser is ready, then `active`.
|
|
108
|
+
- **Every way a turn can end is a named result kind** — `plan-executed` (the
|
|
109
|
+
steps ran, with `answer` when you asked a question), `clarify` (the agent
|
|
110
|
+
is asking you something), `refuse` (it will not do this), `stopped`, and a
|
|
111
|
+
step held for your approval. `answer_unavailable` says why there is no
|
|
112
|
+
`answer` when you asked for one.
|
|
113
|
+
- **Live progress while it runs** — `message(..., on_step=..., on_event=...)`
|
|
114
|
+
parses the turn's stream as it arrives: `on_step(step)` gets each step
|
|
115
|
+
(`{"index", "result"}`) and `on_event(name, data)` every other progress
|
|
116
|
+
event (`phase`, `plan`, `step_start`, `answer`, `notice`, and any added
|
|
117
|
+
later). On the async client either callback may be `async def`.
|
|
118
|
+
- **Approve a step, or don't** — a step with real-world consequences (a
|
|
119
|
+
payment, a message sent, something deleted) pauses the turn and comes back
|
|
120
|
+
as `confirmation_required`. Send the same task again with
|
|
121
|
+
`approve_consequential_actions=` to release it; it accepts the step result
|
|
122
|
+
exactly as it was returned, as well as `{"category", "matched_text"}`
|
|
123
|
+
dicts, and raises `ValueError` before sending anything if an entry is
|
|
124
|
+
neither. An unattended job simply never approves.
|
|
125
|
+
- **Stop a task that runs too long** — `stop(id)` asks the running turn to
|
|
126
|
+
stop; the waiting `message()` then returns kind `stopped` rather than
|
|
127
|
+
raising.
|
|
128
|
+
- **Screenshots** — `get_capture(id, capture_id)` returns the image behind a
|
|
129
|
+
`capture` step's `captureId` as `{"content_type", "bytes"}` (`image/png` or
|
|
130
|
+
`image/jpeg`). Screenshots are kept only briefly, so fetch one as soon as
|
|
131
|
+
its turn ends; one that is no longer kept raises `NotFoundError`.
|
|
132
|
+
- **Transcripts** — `transcript(id, last_event_id=..., timeout_s=...)`
|
|
133
|
+
iterates the session's conversation — an iterator on the sync client, an
|
|
134
|
+
async iterator on the async one: every entry so far, then each new one as
|
|
135
|
+
it is written. `last_event_id` resumes after the last `index` you saw, and
|
|
136
|
+
closing the iterator closes the connection.
|
|
137
|
+
- **Why a turn handed back, in one word** — a `plan-executed` result carries
|
|
138
|
+
`notice_reason` beside the `notice` sentence: `"step_limit"`,
|
|
139
|
+
`"time_limit"`, `"budget_low"`, `"no_progress"`, `"repeated_step"`,
|
|
140
|
+
`"ai_unavailable"`, `"question"` or `"declined"`, exported as the open
|
|
141
|
+
union `AgentNoticeReason` (`Literal[...] | str`), so an ending this SDK has
|
|
142
|
+
never heard of still type-checks and still parses — match the ones you know
|
|
143
|
+
and show `notice` for the rest.
|
|
144
|
+
- **When it is safe to retry a message** — a refusal that did no work leaves
|
|
145
|
+
its `Idempotency-Key` free: after a 409 whose `turn_in_progress` is set, a
|
|
146
|
+
429, a 402, a 403 about the plan's AI or the model, or a 502 whose
|
|
147
|
+
`key_rejected` is false, send the same request again with the **same**
|
|
148
|
+
`idempotency_key=`. Any other failure gets a new one. One message is one
|
|
149
|
+
key, always.
|
|
150
|
+
- **Typed refusals you can act on** — `ForbiddenError.requires_own_key` /
|
|
151
|
+
`.model` (this model needs your own Anthropic key);
|
|
152
|
+
`ConflictError.turn_in_progress`, `.session_status`, `.idempotency_status`,
|
|
153
|
+
`.ai_control_unavailable`, `.phase`, `.tokens_consumed`, `.usage`,
|
|
154
|
+
`.partial_results` and `.closed_reason` (why a closed session ended,
|
|
155
|
+
without a second call); `FeatureUnavailableError.stop_unconfirmed` (the one
|
|
156
|
+
`stop()` 503 worth calling again); and
|
|
157
|
+
`ByokAnthropicRequiredError.key_rejected` / `.key_source` /
|
|
158
|
+
`.key_rejected_reason` (your own key was refused, which key, and why).
|
|
159
|
+
- **Send your own Anthropic key** — `byok_api_key=` on `create()`, so a
|
|
160
|
+
session can run on a model that requires one without storing anything.
|
|
161
|
+
- **Bound the whole call** — `timeout_s=` on `message()` (default 50
|
|
162
|
+
minutes).
|
|
163
|
+
- **Why a step failed** — a failed step's `Diagnosis.category` explains it,
|
|
164
|
+
including `"target_unverified"` (the tap was not made because its target
|
|
165
|
+
could not be checked first — not retryable as the same step; the agent
|
|
166
|
+
re-plans).
|
|
167
|
+
- **`examples/agent_chat.py`** is the complete flow end to end: create, wait
|
|
168
|
+
until ready, send a task with a fresh idempotency key and live progress,
|
|
169
|
+
handle every result kind, close in `finally`.
|
|
170
|
+
|
|
171
|
+
#### Watch one live, or take the wheel
|
|
172
|
+
|
|
173
|
+
- **`livekit_token(id)`** — a token for the live video view of a running
|
|
174
|
+
session, so a person can watch it work. Returns `LiveKitInfo`.
|
|
175
|
+
- **`set_mode(id, body)`**, **`takeover(id, client_id)`** and
|
|
176
|
+
**`handback(id)`** — hand control of a running session between the agent
|
|
177
|
+
and a person, and hand it back. **`send_input_event(id, body)`** drives it
|
|
178
|
+
while a person holds it, and **`resume(id)`** picks a session back up.
|
|
179
|
+
- **`set_egress(id, body)`** changes which of your proxies a running session
|
|
180
|
+
goes out through.
|
|
181
|
+
|
|
182
|
+
#### The rest of the API
|
|
183
|
+
|
|
184
|
+
`client.sessions`, `client.api_keys`, `client.usage` and `client.webhooks`
|
|
185
|
+
were the whole client in 0.1.5, and each of the four gained methods:
|
|
186
|
+
`sessions` gained `get` / `iterate` / `extract` (pull structured data off the
|
|
187
|
+
page) / `search` / `login`; `usage` gained `series()` for usage over time;
|
|
188
|
+
`api_keys` gained
|
|
189
|
+
`rotate()` (issue the replacement and keep the old key working for a grace
|
|
190
|
+
window); `webhooks` gained `update()` (partial update — it does not rotate
|
|
191
|
+
the signing secret), `rotate_secret()` (fresh secret shown once, previous one
|
|
192
|
+
valid for 24h, both signatures sent during the window), `send_test()` (a
|
|
193
|
+
synthetic `test.ping` delivery so you can check your handler before depending
|
|
194
|
+
on it), `replay_delivery()` and `iterate_deliveries()`.
|
|
195
|
+
|
|
196
|
+
And fourteen resources are new:
|
|
197
|
+
|
|
198
|
+
- **`client.profiles`** — create, list, iterate, get, update, delete, plus
|
|
199
|
+
`clone(profile_id, body=None)` (pass `None` to let the server name it
|
|
200
|
+
"(copy)", "(copy 2)", …) and `trim()`.
|
|
201
|
+
- **`client.profile_snapshots`** — immutable point-in-time copies of a
|
|
202
|
+
profile: `capture`, `list_for_profile`, `list`, `iterate`, `get`,
|
|
203
|
+
`restore`, `delete`. `restore` creates a NEW profile; the original is never
|
|
204
|
+
modified.
|
|
205
|
+
- **`client.account`** — `me()` (the full account profile: timezone, slug,
|
|
206
|
+
region, avatar, whether MFA is enrolled, team memberships),
|
|
207
|
+
`update_me()`, `upload_avatar()` / `clear_avatar()`,
|
|
208
|
+
`list_web_sessions()` / `revoke_web_session(id)` /
|
|
209
|
+
`revoke_all_other_web_sessions()`, and `rate_limits()` for the limits
|
|
210
|
+
actually in force on your account.
|
|
211
|
+
- **`client.auth`** — sign-up, e-mail verification, log in, magic links,
|
|
212
|
+
password reset, refresh, log out, and the three-call activation flow a CLI
|
|
213
|
+
or desktop app uses instead of asking for a pasted key
|
|
214
|
+
(`cli_authorize_initiate` → open the returned `browser_url` → poll
|
|
215
|
+
`cli_authorize_exchange`, which delivers the key once, then reports
|
|
216
|
+
`expired`).
|
|
217
|
+
- **`client.mfa`** — `status`, `enroll`, `verify`, `disable`,
|
|
218
|
+
`regenerate_recovery_codes`; plus `auth.mfa_challenge()` to exchange a
|
|
219
|
+
login challenge for a session (the response says whether it was satisfied
|
|
220
|
+
by `"totp"` or `"recovery"`) and `auth.mfa_step_up()` to refresh the
|
|
221
|
+
freshness window an operation asked for.
|
|
222
|
+
- **`client.team`** — members, invites, roles, and `list_owners()` for the
|
|
223
|
+
workspaces your account has joined.
|
|
224
|
+
- **`client.audit_log`** — `list` / `iterate`, and `export()`: a single-call
|
|
225
|
+
JSON export of your account's audit log, up to 10,000 rows, with
|
|
226
|
+
`truncated` set when there were more.
|
|
227
|
+
- **`client.billing`** — current state, checkout, and the billing portal.
|
|
228
|
+
- **`client.crypto_orders`** — `quote`, `create_checkout` (takes
|
|
229
|
+
`idempotency_key=` so a retry cannot mint a second order), `list`,
|
|
230
|
+
`iterate` (walks every page for you; narrow it with `status`,
|
|
231
|
+
`created_after`, `created_before`), `get`, `update_note`, `cancel`,
|
|
232
|
+
`receipt`. Crypto payments are not refundable, and cancelling only works
|
|
233
|
+
while an order is pending.
|
|
234
|
+
- **`client.egress`** and account proxies — manage saved proxies and route a
|
|
235
|
+
session's traffic through one with `proxy_id` on create.
|
|
236
|
+
- **`client.archetypes`** — the device archetypes your plan can use.
|
|
237
|
+
- **`client.recipes`** — `create(agent_session_id=, label=, description=)`
|
|
238
|
+
snapshots a finished agent session's steps and transcript into a recipe you
|
|
239
|
+
can replay.
|
|
240
|
+
- **`client.email_preferences`** — `list` / `set` / `opt_in` / `opt_out`.
|
|
241
|
+
- **`client.legal`** — record acceptance of a document version.
|
|
242
|
+
|
|
243
|
+
#### Errors and retries
|
|
244
|
+
|
|
245
|
+
- **`is_retryable(err)`** is exported, so the predicate the built-in retry
|
|
246
|
+
loop uses is one you can call yourself.
|
|
247
|
+
- **New error classes**, all importable from `driftstack`:
|
|
248
|
+
`BadRequestError`, `InternalError`, `FeatureUnavailableError`,
|
|
249
|
+
`MfaStepUpRequiredError`, `EmailAlreadyRegisteredError`,
|
|
250
|
+
`InvalidCredentialsError`, `InvalidAuthTokenError`,
|
|
251
|
+
`EmailNotVerifiedError`, `ByokAnthropicRequiredError`,
|
|
252
|
+
`ProxyValidationFailedError`, `StorageQuotaExceededError`,
|
|
253
|
+
`ProfileInUseError`, `BundledLlmConsentRequiredError`,
|
|
254
|
+
`BundledLlmBudgetExhaustedError`, `PairModeConflictError` and
|
|
255
|
+
`PairModeStateInvalidTransitionError`.
|
|
256
|
+
- **`verify_webhook_signature` accepts `header_prev=`** — an optional second
|
|
257
|
+
signature header. You rarely need it: during a rotation grace window both
|
|
258
|
+
signatures arrive inside the one `x-driftstack-signature` header, which the
|
|
259
|
+
verifier already checks.
|
|
260
|
+
|
|
261
|
+
### Changed
|
|
262
|
+
|
|
263
|
+
- **Too many AI turns is a `RateLimitError` now.** This refusal answers as
|
|
264
|
+
`rate-limited` with `retry_after_seconds` 5, so `message()` raises
|
|
265
|
+
`RateLimitError` where it raised `ConcurrencyLimitError`.
|
|
266
|
+
`ConcurrencyLimitError` still means exactly what it always meant on
|
|
267
|
+
`create()`: your plan's limit on sessions running at once.
|
|
268
|
+
- **A generic 400 raises `BadRequestError` now**, not `ValidationError`. A
|
|
269
|
+
400 that carries field-level issues is still a `ValidationError`. Callers
|
|
270
|
+
with `except ValidationError` around a generic 400 should switch to
|
|
271
|
+
`except BadRequestError`; `except DriftstackError` catch-alls and
|
|
272
|
+
`is_retryable` are unaffected.
|
|
273
|
+
- **The agent-session docstrings describe what the API does** — result kinds,
|
|
274
|
+
how an approval resumes paused steps, when a key may be reused, the
|
|
275
|
+
progress event names — and no longer describe how the service is built. The
|
|
276
|
+
README no longer claims every resource returns a Pydantic model
|
|
277
|
+
(`agent_sessions` returns dicts) and now says that `ForbiddenError` is a
|
|
278
|
+
subclass of `AuthError`.
|
|
279
|
+
|
|
280
|
+
### Fixed
|
|
281
|
+
|
|
282
|
+
- **A failure category newer than the SDK no longer breaks parsing.**
|
|
283
|
+
`Diagnosis.category` is `Literal[...] | str`, so a category the server adds
|
|
284
|
+
after this release is kept as a plain string. ⚠️ **This is the one reason
|
|
285
|
+
to upgrade before you need to.** In 0.1.5 the field is a closed `Literal`,
|
|
286
|
+
so a response carrying a category that SDK has never seen raises a pydantic
|
|
287
|
+
`ValidationError` for the **whole** response — not just that field. Treat a
|
|
288
|
+
value you do not recognise as `"unknown"`; code comparing `category`
|
|
289
|
+
against known strings needs no change.
|
|
290
|
+
- **The account profile matches the full response** — `me()` returns every
|
|
291
|
+
field the API sends, including the ones added since 0.1.5.
|
|
292
|
+
|
|
293
|
+
### Pre-1.0 stability
|
|
294
|
+
|
|
295
|
+
The SDK is pre-1.0. The surface is stable enough to build against, but a
|
|
296
|
+
MINOR bump may still carry additive changes — new methods, new fields, new
|
|
297
|
+
error subclasses. **Patch** releases (0.2.x) are fixes and additive types
|
|
298
|
+
only. Breaking changes that would stop shipping customer code are deferred to
|
|
299
|
+
1.0; until then pin `driftstack-sdk~=0.2.0` rather than an exact version, and
|
|
300
|
+
read this file before bumping.
|
|
301
|
+
|
|
302
|
+
## [0.1.5] - 2026-05-03
|
|
303
|
+
|
|
304
|
+
Written on 2026-09-20 from the published wheel: 0.1.5 went to PyPI without a
|
|
305
|
+
CHANGELOG heading of its own.
|
|
306
|
+
|
|
307
|
+
### Added
|
|
308
|
+
|
|
309
|
+
- **`LegalAcceptanceRequiredError`** — a 409 that asks you to accept a
|
|
310
|
+
document version is its own exception now, carrying the pending
|
|
311
|
+
acceptances (`document_key` + `current_version` for each) as
|
|
312
|
+
`pending_acceptances`. Exported from the package root.
|
|
313
|
+
|
|
314
|
+
## [0.1.4] - 2026-05-03
|
|
315
|
+
|
|
316
|
+
### Added
|
|
317
|
+
|
|
318
|
+
- **`SessionTimeoutError`** — new typed error subclass mapping
|
|
319
|
+
the `https://errors.driftstack.dev/session-timeout` problem type
|
|
320
|
+
(status 504). Distinguished from `DriverError` so callers can
|
|
321
|
+
react specifically to "the operation didn't finish within the
|
|
322
|
+
per-call timeout I supplied" without conflating with downstream
|
|
323
|
+
driver failures. Carries `timeout_ms: int | None` from the
|
|
324
|
+
problem extension. Re-exported at `driftstack.SessionTimeoutError`
|
|
325
|
+
for convenient `isinstance` checks.
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from driftstack import SessionTimeoutError
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
client.sessions.interact(sid, body)
|
|
332
|
+
except SessionTimeoutError as e:
|
|
333
|
+
# Retry with a longer timeout, or surface to the user.
|
|
334
|
+
print(f"Op timed out after {e.timeout_ms} ms")
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- Test coverage at `tests/test_errors.py::test_session_timeout_extracts_timeout_ms`.
|
|
338
|
+
|
|
339
|
+
## [0.1.3] - 2026-05-03
|
|
340
|
+
|
|
341
|
+
### Removed
|
|
342
|
+
|
|
343
|
+
- `tap.offset` field stripped from the public `InteractAction.tap`
|
|
344
|
+
shape. Same reason as `tap_at`: a coordinate primitive on the
|
|
345
|
+
customer-facing schema lets the customer bypass the behavioral
|
|
346
|
+
simulation layer. Bounded coordinates are still coordinates.
|
|
347
|
+
|
|
348
|
+
### Migration
|
|
349
|
+
|
|
350
|
+
If your code passes `offset={"x": ..., "y": ...}` to a `tap`
|
|
351
|
+
action, the value is now silently dropped (Pydantic strips unknown
|
|
352
|
+
keys by default). Re-express the intent through selector
|
|
353
|
+
specificity — better selectors, child-element targeting, ARIA-role
|
|
354
|
+
qualifiers, text-content matching:
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
# Before (0.1.x):
|
|
358
|
+
client.sessions.interact(
|
|
359
|
+
session_id,
|
|
360
|
+
InteractRequest(action={"kind": "tap", "selector": "button.cta", "offset": {"x": 0, "y": 50}}),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# After (0.1.3+):
|
|
364
|
+
client.sessions.interact(
|
|
365
|
+
session_id,
|
|
366
|
+
InteractRequest(action={"kind": "tap", "selector": "button.cta .icon-arrow"}),
|
|
367
|
+
)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Coordinate-level addressing for screenshot-driven workflows lives
|
|
371
|
+
behind a separate endpoint gated by the `gui_control` API-key
|
|
372
|
+
scope, and is not exposed in this SDK.
|
|
373
|
+
|
|
374
|
+
## [0.1.2] - 2026-05-03
|
|
375
|
+
|
|
376
|
+
### Added
|
|
377
|
+
|
|
378
|
+
- Wire-shape regression tests at `tests/test_wire_shape.py` (10
|
|
379
|
+
tests). Locks the canonical JSON shape for `InteractRequest`,
|
|
380
|
+
`WaitRequest`, `NavigateRequest`. Asserts rejection of `tap_at` /
|
|
381
|
+
`type_focused` (these live behind the `gui_control`-scoped
|
|
382
|
+
endpoint).
|
|
383
|
+
|
|
384
|
+
### Fixed
|
|
385
|
+
|
|
386
|
+
- `tests/test_client.py::test_version_string_matches_pyproject_default`
|
|
387
|
+
was pinning `__version__ == "0.0.1"` (stale from the pre-publish
|
|
388
|
+
era). Fixed to assert SemVer shape, not exact value.
|
|
389
|
+
|
|
390
|
+
## [0.1.1] - 2026-05-02
|
|
391
|
+
|
|
392
|
+
### Changed
|
|
393
|
+
|
|
394
|
+
- Re-cut: `tap_at` and `type_focused` removed from
|
|
395
|
+
`InteractAction`. They were briefly added in 0.1.0+ for the
|
|
396
|
+
self-hosted GUI's manual-control input forwarding, and reverted.
|
|
397
|
+
Customer-facing schemas stay intent-only — coordinate primitives
|
|
398
|
+
bypass the behavioral simulation layer. The GUI now uses a
|
|
399
|
+
separate, scope-gated endpoint
|
|
400
|
+
(`/v1/sessions/:id/gui-input`).
|
|
401
|
+
|
|
402
|
+
## [0.1.0] - 2026-05-02
|
|
403
|
+
|
|
404
|
+
### Added
|
|
405
|
+
|
|
406
|
+
- Inaugural PyPI publish (under a maintainer account pre-entity;
|
|
407
|
+
will transfer to a company-owned account once the legal entity
|
|
408
|
+
is registered).
|
|
409
|
+
- Pydantic models regenerated from updated OpenAPI spec.
|
|
410
|
+
|
|
411
|
+
## [0.0.1] - 2026-05-02
|
|
412
|
+
|
|
413
|
+
### Added
|
|
414
|
+
|
|
415
|
+
- `Driftstack` (sync) and `AsyncDriftstack` (async) clients.
|
|
416
|
+
- Resource accessors mounted on the client: `sessions` (9 methods),
|
|
417
|
+
`api_keys` (3), `usage` (1), `webhooks` (5).
|
|
418
|
+
- Typed Pydantic v2 models generated from the API's OpenAPI 3.1 spec.
|
|
419
|
+
- Error hierarchy: `DriftstackError` base + 14 subclasses covering
|
|
420
|
+
every documented RFC 7807 problem type. `RateLimitError`,
|
|
421
|
+
`ConcurrencyLimitError`, `QuotaExceededError` carry the relevant
|
|
422
|
+
payload fields.
|
|
423
|
+
- Retry policy (`RetryConfig` + `with_retry`) with exponential
|
|
424
|
+
backoff and full jitter; honours `Retry-After`.
|
|
425
|
+
- `verify_webhook_signature` helper (Stripe-style HMAC-SHA256,
|
|
426
|
+
constant-time).
|
|
427
|
+
- 85 tests covering errors, retry, webhook signatures, every
|
|
428
|
+
resource method, and end-to-end customer-journey workflows.
|
|
429
|
+
- Examples: `quickstart`, `error_handling`, `webhook_receiver`,
|
|
430
|
+
`langchain_tool`, `pytest_fixture`.
|
|
431
|
+
|
|
432
|
+
### Build
|
|
433
|
+
|
|
434
|
+
- Hatchling backend, `py.typed` marker for PEP 561.
|
|
435
|
+
- Runtime deps: `httpx>=0.27,<1.0`, `pydantic[email]>=2.5,<3.0`.
|
|
436
|
+
- Dev deps: `pytest`, `pytest-asyncio`, `respx`, `ruff`, `mypy`,
|
|
437
|
+
`datamodel-code-generator`.
|
|
438
|
+
- CI: lint + format + mypy + pytest on Ubuntu / Python 3.10.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 driftstackdev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|