rightnow-system-zero 0.3.0__tar.gz → 0.4.2__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.
Files changed (36) hide show
  1. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/NOTICE +1 -1
  2. {rightnow_system_zero-0.3.0/src/rightnow_system_zero.egg-info → rightnow_system_zero-0.4.2}/PKG-INFO +46 -10
  3. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/README.md +44 -8
  4. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/pyproject.toml +2 -2
  5. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2/src/rightnow_system_zero.egg-info}/PKG-INFO +46 -10
  6. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/rightnow_system_zero.egg-info/SOURCES.txt +2 -0
  7. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/__init__.py +28 -3
  8. rightnow_system_zero-0.4.2/src/system_zero/_redaction.py +207 -0
  9. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/_retry.py +1 -1
  10. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/_transport.py +2 -2
  11. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/batch.py +8 -6
  12. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/client.py +39 -22
  13. rightnow_system_zero-0.4.2/src/system_zero/enabled_types.py +227 -0
  14. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/error-catalog.json +118 -110
  15. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/error_codes.py +5 -1
  16. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/errors.py +25 -3
  17. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/evaluate.py +1 -1
  18. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/flows.py +12 -8
  19. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/media.py +23 -5
  20. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/stream.py +190 -24
  21. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/tool-schemas.json +4 -4
  22. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/tools.py +11 -5
  23. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/types.py +11 -3
  24. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/workflow_types.py +4 -1
  25. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/LICENSE +0 -0
  26. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/setup.cfg +0 -0
  27. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/rightnow_system_zero.egg-info/dependency_links.txt +0 -0
  28. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/rightnow_system_zero.egg-info/requires.txt +0 -0
  29. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/rightnow_system_zero.egg-info/top_level.txt +0 -0
  30. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/_connection.py +0 -0
  31. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/argument-limits.json +0 -0
  32. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/decisions.py +0 -0
  33. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/public_error_codes.py +0 -0
  34. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/py.typed +0 -0
  35. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/response.py +0 -0
  36. {rightnow_system_zero-0.3.0 → rightnow_system_zero-0.4.2}/src/system_zero/stream_interfaces.py +0 -0
@@ -1,3 +1,3 @@
1
- System Zero SDKs
1
+ RightNow System Zero SDKs
2
2
  Copyright 2026 RightNow AI, Inc.
3
3
  Licensed under Apache 2.0.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rightnow-system-zero
3
- Version: 0.3.0
4
- Summary: System Zero API client with typed decisions and client-side workflows.
3
+ Version: 0.4.2
4
+ Summary: RightNow System Zero API client with typed decisions and client-side workflows.
5
5
  Author: RightNow AI, Inc.
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Changelog, https://github.com/RightNow-AI/system-zero-sdk/blob/main/CHANGELOG.md
@@ -29,9 +29,11 @@ Requires-Dist: httpcore<2,>=1.0.9
29
29
  Requires-Dist: websockets<16,>=15.0.1
30
30
  Dynamic: license-file
31
31
 
32
- # System Zero Python SDK
32
+ # RightNow System Zero Python SDK
33
33
 
34
- Synchronous server-side Python client for the System Zero API.
34
+ RightNow is the model name; System Zero is its category. The full name is RightNow System Zero.
35
+
36
+ Synchronous server-side Python client for the RightNow System Zero API.
35
37
 
36
38
  Requires Python 3.10 or later.
37
39
 
@@ -73,9 +75,10 @@ manager to release the HTTP transport.
73
75
  ## Media on decide
74
76
 
75
77
  `attachments` is optional; `null` remains valid. Each inline attachment has a unique
76
- `name`, a `type` of `image` or `audio`, and a canonical base64 data URL in `data`.
78
+ `name`, a `type` of `image`, `audio`, `document` or `video`, and a canonical base64 data URL in `data`.
77
79
  Names start with an ASCII letter and contain at most 64 letters, digits, `_` or `-`.
78
- Function tools and the MCP adapter carry the same inline forms.
80
+ Function tools and the MCP adapter carry the image and audio forms.
81
+ Send PDF and video through the ordinary client request form shown below.
79
82
 
80
83
  - Images: static PNG (`image/png`) and JPEG (`image/jpeg`), at most 4 images,
81
84
  16,777,216 pixels each, with an aspect ratio no greater than 50:1.
@@ -96,13 +99,23 @@ prove a complete image is valid.
96
99
  GIF (`image/gif`), WebP (`image/webp`), animated PNG and images outside the decoded
97
100
  profile are refused with `media_unpriced`. A mismatched image signature uses
98
101
  `media_decode_failed`; other unsupported image MIME types use `media_unsupported`.
99
- Documents remain refused with `document_not_available`, and video with
100
- `video_not_available`. Other audio formats, rates, malformed WAV and audio upload
102
+ Other audio formats, rates, malformed WAV and audio upload
101
103
  references use `media_unsupported`. Image upload references are unavailable and
102
104
  return `service_unavailable` in the listed service. Send bytes inline.
103
105
 
106
+ PDF is supported through ordinary inline attachments.
107
+ Text PDFs and readable scans are read as text.
108
+ Only PDF is supported; other document formats are unavailable.
109
+ Use `type: "document"` and a canonical `data:application/pdf;base64,...` URL.
110
+ The image and audio helpers do not encode PDF; construct its inline attachment directly.
111
+
112
+
113
+ Video is supported through ordinary inline attachments. Send one silent H.264 MP4 video as an inline base64 data URL. Maximum duration: 10 seconds. Maximum dimensions: 1920 x 1080. Maximum decoded file size: 2,621,440 bytes (2.5 MiB). The complete JSON request must fit 3,665,920 bytes, including base64 and questions. Rotated videos, soundtracks, separate attachments and video upload references are not supported.
114
+
115
+ Use `videoAttachment({ name: 'clip', data: bytes })` in TypeScript or `video_attachment(name='clip', data=bytes)` in Python. Video helpers encode the bytes; the API performs complete-file validation. A `video_not_available` response becomes `VideoNotAvailableError`, preserving status, code, message and request ID.
104
116
  ```python
105
117
  from pathlib import Path
118
+ from base64 import b64encode
106
119
  from system_zero import SystemZero, inline_attachment, validate_attachments
107
120
 
108
121
  attachments = validate_attachments([
@@ -115,6 +128,13 @@ with SystemZero() as client:
115
128
  "questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
116
129
  })
117
130
  print(reply["answers"])
131
+ # PDF uses the ordinary inline request form.
132
+ client.decide({
133
+ "model": "system-zero", "state": "Read the PDF.",
134
+ "questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
135
+ "attachments": [{"name": "document", "type": "document",
136
+ "data": "data:application/pdf;base64," + b64encode(Path("document.pdf").read_bytes()).decode("ascii")}],
137
+ })
118
138
  ```
119
139
 
120
140
  `MIMES`, `MEDIA_LIMITS` and `attachment_magic_matches` expose the served registry,
@@ -133,7 +153,7 @@ attempts are limited by the remaining budget. A wait above the maximum wait, or
133
153
  one that exhausts the remaining budget, raises the last error without waiting.
134
154
  A retry needs at least one millisecond remaining.
135
155
 
136
- Only GET requests or requests carrying a nonempty idempotency key can retry.
156
+ Only GET requests or requests carrying a non-empty idempotency key can retry.
137
157
  With retries enabled, decide POSTs receive a generated key when none is supplied.
138
158
  An explicit blank key omits the header and disables decision retries. Each retry
139
159
  retains the same key and serialized body. Setting the retry count or budget to
@@ -217,7 +237,7 @@ Every HTTP attempt sends `X-SDK-Retry-Count`, starting at `0`, and a
217
237
  A blank or whitespace-only `client_request_id` omits the
218
238
  `X-Client-Request-Id` header.
219
239
 
220
- Keep API keys on trusted servers. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
240
+ Keep API keys on trusted servers. Don't enable DEBUG logging for the HTTP or WebSocket libraries in production; it can record request headers, including your API key. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
221
241
  for private vulnerability reporting.
222
242
 
223
243
  ## Function tool formats
@@ -299,3 +319,19 @@ Billing is per decision, not per connection, heartbeat, acknowledgement, or
299
319
  mutation. Several mutations can be covered by one decision. A decision may be
300
320
  counted even if the connection closes before its event arrives; use the event's
301
321
  usage and request identifier when reconciling completed work.
322
+
323
+ ## LongContextRequest opt-in
324
+
325
+ The SDK publishes the `LongContextRequest` type as an opt-in surface. It is unavailable until the API enables it for the workspace; workspace opt-in through the console's Settings is also required. Once available, give explicit per-request consent with `dangerZone: 'accept'` in TypeScript or `danger_zone="accept"` in Python. The option sends exactly `SZ-Danger-Zone: accept`; it is never sent by default or inferred from state size. Until availability, the API ignores or refuses this option. Do not supply this header through custom headers. Retries retain the option. The same explicit option is supported by batch and flow helpers.
326
+
327
+ Until workspace enablement, the expected result is HTTP 404 `long_context_not_available`, surfaced as `LongContextNotAvailableError`. The same refusal applies without workspace opt-in. When the API supplies `details.eligibility`, `LongContextNotAvailableError.eligibility` exposes the rule: `billing`, `opt_in`, `class`, `deny_list`, `global_switch` or `account_deleted`, with a string fallback for future rules. A generic held-route refusal can omit the rule. Status, code, message and request ID remain intact. Held routes are never retried.
328
+
329
+ `LongContextRequest`, `EnabledDecideRequest`, `OrdinaryRequest`, `MediaRequest` and their question types describe the separate enabled request contract in `enabled.openapi.json`. `LongContextLimits` describes its discovery limits. `openapi.json` remains the served contract. The API selects the request band and checks bounds: a `LongContextRequest` takes at most 32 questions, choices with 2 to 25 options, and scores with 2 to 4 levels. Only inline document attachments fit its document branch. The API validates document admission separately.
330
+
331
+ ```typescript
332
+ await client.decide(request, { dangerZone: 'accept' });
333
+ ```
334
+
335
+ ```python
336
+ client.decide(request, danger_zone="accept")
337
+ ```
@@ -1,6 +1,8 @@
1
- # System Zero Python SDK
1
+ # RightNow System Zero Python SDK
2
2
 
3
- Synchronous server-side Python client for the System Zero API.
3
+ RightNow is the model name; System Zero is its category. The full name is RightNow System Zero.
4
+
5
+ Synchronous server-side Python client for the RightNow System Zero API.
4
6
 
5
7
  Requires Python 3.10 or later.
6
8
 
@@ -42,9 +44,10 @@ manager to release the HTTP transport.
42
44
  ## Media on decide
43
45
 
44
46
  `attachments` is optional; `null` remains valid. Each inline attachment has a unique
45
- `name`, a `type` of `image` or `audio`, and a canonical base64 data URL in `data`.
47
+ `name`, a `type` of `image`, `audio`, `document` or `video`, and a canonical base64 data URL in `data`.
46
48
  Names start with an ASCII letter and contain at most 64 letters, digits, `_` or `-`.
47
- Function tools and the MCP adapter carry the same inline forms.
49
+ Function tools and the MCP adapter carry the image and audio forms.
50
+ Send PDF and video through the ordinary client request form shown below.
48
51
 
49
52
  - Images: static PNG (`image/png`) and JPEG (`image/jpeg`), at most 4 images,
50
53
  16,777,216 pixels each, with an aspect ratio no greater than 50:1.
@@ -65,13 +68,23 @@ prove a complete image is valid.
65
68
  GIF (`image/gif`), WebP (`image/webp`), animated PNG and images outside the decoded
66
69
  profile are refused with `media_unpriced`. A mismatched image signature uses
67
70
  `media_decode_failed`; other unsupported image MIME types use `media_unsupported`.
68
- Documents remain refused with `document_not_available`, and video with
69
- `video_not_available`. Other audio formats, rates, malformed WAV and audio upload
71
+ Other audio formats, rates, malformed WAV and audio upload
70
72
  references use `media_unsupported`. Image upload references are unavailable and
71
73
  return `service_unavailable` in the listed service. Send bytes inline.
72
74
 
75
+ PDF is supported through ordinary inline attachments.
76
+ Text PDFs and readable scans are read as text.
77
+ Only PDF is supported; other document formats are unavailable.
78
+ Use `type: "document"` and a canonical `data:application/pdf;base64,...` URL.
79
+ The image and audio helpers do not encode PDF; construct its inline attachment directly.
80
+
81
+
82
+ Video is supported through ordinary inline attachments. Send one silent H.264 MP4 video as an inline base64 data URL. Maximum duration: 10 seconds. Maximum dimensions: 1920 x 1080. Maximum decoded file size: 2,621,440 bytes (2.5 MiB). The complete JSON request must fit 3,665,920 bytes, including base64 and questions. Rotated videos, soundtracks, separate attachments and video upload references are not supported.
83
+
84
+ Use `videoAttachment({ name: 'clip', data: bytes })` in TypeScript or `video_attachment(name='clip', data=bytes)` in Python. Video helpers encode the bytes; the API performs complete-file validation. A `video_not_available` response becomes `VideoNotAvailableError`, preserving status, code, message and request ID.
73
85
  ```python
74
86
  from pathlib import Path
87
+ from base64 import b64encode
75
88
  from system_zero import SystemZero, inline_attachment, validate_attachments
76
89
 
77
90
  attachments = validate_attachments([
@@ -84,6 +97,13 @@ with SystemZero() as client:
84
97
  "questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
85
98
  })
86
99
  print(reply["answers"])
100
+ # PDF uses the ordinary inline request form.
101
+ client.decide({
102
+ "model": "system-zero", "state": "Read the PDF.",
103
+ "questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
104
+ "attachments": [{"name": "document", "type": "document",
105
+ "data": "data:application/pdf;base64," + b64encode(Path("document.pdf").read_bytes()).decode("ascii")}],
106
+ })
87
107
  ```
88
108
 
89
109
  `MIMES`, `MEDIA_LIMITS` and `attachment_magic_matches` expose the served registry,
@@ -102,7 +122,7 @@ attempts are limited by the remaining budget. A wait above the maximum wait, or
102
122
  one that exhausts the remaining budget, raises the last error without waiting.
103
123
  A retry needs at least one millisecond remaining.
104
124
 
105
- Only GET requests or requests carrying a nonempty idempotency key can retry.
125
+ Only GET requests or requests carrying a non-empty idempotency key can retry.
106
126
  With retries enabled, decide POSTs receive a generated key when none is supplied.
107
127
  An explicit blank key omits the header and disables decision retries. Each retry
108
128
  retains the same key and serialized body. Setting the retry count or budget to
@@ -186,7 +206,7 @@ Every HTTP attempt sends `X-SDK-Retry-Count`, starting at `0`, and a
186
206
  A blank or whitespace-only `client_request_id` omits the
187
207
  `X-Client-Request-Id` header.
188
208
 
189
- Keep API keys on trusted servers. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
209
+ Keep API keys on trusted servers. Don't enable DEBUG logging for the HTTP or WebSocket libraries in production; it can record request headers, including your API key. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
190
210
  for private vulnerability reporting.
191
211
 
192
212
  ## Function tool formats
@@ -268,3 +288,19 @@ Billing is per decision, not per connection, heartbeat, acknowledgement, or
268
288
  mutation. Several mutations can be covered by one decision. A decision may be
269
289
  counted even if the connection closes before its event arrives; use the event's
270
290
  usage and request identifier when reconciling completed work.
291
+
292
+ ## LongContextRequest opt-in
293
+
294
+ The SDK publishes the `LongContextRequest` type as an opt-in surface. It is unavailable until the API enables it for the workspace; workspace opt-in through the console's Settings is also required. Once available, give explicit per-request consent with `dangerZone: 'accept'` in TypeScript or `danger_zone="accept"` in Python. The option sends exactly `SZ-Danger-Zone: accept`; it is never sent by default or inferred from state size. Until availability, the API ignores or refuses this option. Do not supply this header through custom headers. Retries retain the option. The same explicit option is supported by batch and flow helpers.
295
+
296
+ Until workspace enablement, the expected result is HTTP 404 `long_context_not_available`, surfaced as `LongContextNotAvailableError`. The same refusal applies without workspace opt-in. When the API supplies `details.eligibility`, `LongContextNotAvailableError.eligibility` exposes the rule: `billing`, `opt_in`, `class`, `deny_list`, `global_switch` or `account_deleted`, with a string fallback for future rules. A generic held-route refusal can omit the rule. Status, code, message and request ID remain intact. Held routes are never retried.
297
+
298
+ `LongContextRequest`, `EnabledDecideRequest`, `OrdinaryRequest`, `MediaRequest` and their question types describe the separate enabled request contract in `enabled.openapi.json`. `LongContextLimits` describes its discovery limits. `openapi.json` remains the served contract. The API selects the request band and checks bounds: a `LongContextRequest` takes at most 32 questions, choices with 2 to 25 options, and scores with 2 to 4 levels. Only inline document attachments fit its document branch. The API validates document admission separately.
299
+
300
+ ```typescript
301
+ await client.decide(request, { dangerZone: 'accept' });
302
+ ```
303
+
304
+ ```python
305
+ client.decide(request, danger_zone="accept")
306
+ ```
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rightnow-system-zero"
7
- version = "0.3.0"
8
- description = "System Zero API client with typed decisions and client-side workflows."
7
+ version = "0.4.2"
8
+ description = "RightNow System Zero API client with typed decisions and client-side workflows."
9
9
  license = "Apache-2.0"
10
10
  license-files = ["LICENSE", "NOTICE"]
11
11
  readme = "README.md"
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rightnow-system-zero
3
- Version: 0.3.0
4
- Summary: System Zero API client with typed decisions and client-side workflows.
3
+ Version: 0.4.2
4
+ Summary: RightNow System Zero API client with typed decisions and client-side workflows.
5
5
  Author: RightNow AI, Inc.
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Changelog, https://github.com/RightNow-AI/system-zero-sdk/blob/main/CHANGELOG.md
@@ -29,9 +29,11 @@ Requires-Dist: httpcore<2,>=1.0.9
29
29
  Requires-Dist: websockets<16,>=15.0.1
30
30
  Dynamic: license-file
31
31
 
32
- # System Zero Python SDK
32
+ # RightNow System Zero Python SDK
33
33
 
34
- Synchronous server-side Python client for the System Zero API.
34
+ RightNow is the model name; System Zero is its category. The full name is RightNow System Zero.
35
+
36
+ Synchronous server-side Python client for the RightNow System Zero API.
35
37
 
36
38
  Requires Python 3.10 or later.
37
39
 
@@ -73,9 +75,10 @@ manager to release the HTTP transport.
73
75
  ## Media on decide
74
76
 
75
77
  `attachments` is optional; `null` remains valid. Each inline attachment has a unique
76
- `name`, a `type` of `image` or `audio`, and a canonical base64 data URL in `data`.
78
+ `name`, a `type` of `image`, `audio`, `document` or `video`, and a canonical base64 data URL in `data`.
77
79
  Names start with an ASCII letter and contain at most 64 letters, digits, `_` or `-`.
78
- Function tools and the MCP adapter carry the same inline forms.
80
+ Function tools and the MCP adapter carry the image and audio forms.
81
+ Send PDF and video through the ordinary client request form shown below.
79
82
 
80
83
  - Images: static PNG (`image/png`) and JPEG (`image/jpeg`), at most 4 images,
81
84
  16,777,216 pixels each, with an aspect ratio no greater than 50:1.
@@ -96,13 +99,23 @@ prove a complete image is valid.
96
99
  GIF (`image/gif`), WebP (`image/webp`), animated PNG and images outside the decoded
97
100
  profile are refused with `media_unpriced`. A mismatched image signature uses
98
101
  `media_decode_failed`; other unsupported image MIME types use `media_unsupported`.
99
- Documents remain refused with `document_not_available`, and video with
100
- `video_not_available`. Other audio formats, rates, malformed WAV and audio upload
102
+ Other audio formats, rates, malformed WAV and audio upload
101
103
  references use `media_unsupported`. Image upload references are unavailable and
102
104
  return `service_unavailable` in the listed service. Send bytes inline.
103
105
 
106
+ PDF is supported through ordinary inline attachments.
107
+ Text PDFs and readable scans are read as text.
108
+ Only PDF is supported; other document formats are unavailable.
109
+ Use `type: "document"` and a canonical `data:application/pdf;base64,...` URL.
110
+ The image and audio helpers do not encode PDF; construct its inline attachment directly.
111
+
112
+
113
+ Video is supported through ordinary inline attachments. Send one silent H.264 MP4 video as an inline base64 data URL. Maximum duration: 10 seconds. Maximum dimensions: 1920 x 1080. Maximum decoded file size: 2,621,440 bytes (2.5 MiB). The complete JSON request must fit 3,665,920 bytes, including base64 and questions. Rotated videos, soundtracks, separate attachments and video upload references are not supported.
114
+
115
+ Use `videoAttachment({ name: 'clip', data: bytes })` in TypeScript or `video_attachment(name='clip', data=bytes)` in Python. Video helpers encode the bytes; the API performs complete-file validation. A `video_not_available` response becomes `VideoNotAvailableError`, preserving status, code, message and request ID.
104
116
  ```python
105
117
  from pathlib import Path
118
+ from base64 import b64encode
106
119
  from system_zero import SystemZero, inline_attachment, validate_attachments
107
120
 
108
121
  attachments = validate_attachments([
@@ -115,6 +128,13 @@ with SystemZero() as client:
115
128
  "questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
116
129
  })
117
130
  print(reply["answers"])
131
+ # PDF uses the ordinary inline request form.
132
+ client.decide({
133
+ "model": "system-zero", "state": "Read the PDF.",
134
+ "questions": {"ready": {"type": "boolean", "instructions": "Is the item ready?"}},
135
+ "attachments": [{"name": "document", "type": "document",
136
+ "data": "data:application/pdf;base64," + b64encode(Path("document.pdf").read_bytes()).decode("ascii")}],
137
+ })
118
138
  ```
119
139
 
120
140
  `MIMES`, `MEDIA_LIMITS` and `attachment_magic_matches` expose the served registry,
@@ -133,7 +153,7 @@ attempts are limited by the remaining budget. A wait above the maximum wait, or
133
153
  one that exhausts the remaining budget, raises the last error without waiting.
134
154
  A retry needs at least one millisecond remaining.
135
155
 
136
- Only GET requests or requests carrying a nonempty idempotency key can retry.
156
+ Only GET requests or requests carrying a non-empty idempotency key can retry.
137
157
  With retries enabled, decide POSTs receive a generated key when none is supplied.
138
158
  An explicit blank key omits the header and disables decision retries. Each retry
139
159
  retains the same key and serialized body. Setting the retry count or budget to
@@ -217,7 +237,7 @@ Every HTTP attempt sends `X-SDK-Retry-Count`, starting at `0`, and a
217
237
  A blank or whitespace-only `client_request_id` omits the
218
238
  `X-Client-Request-Id` header.
219
239
 
220
- Keep API keys on trusted servers. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
240
+ Keep API keys on trusted servers. Don't enable DEBUG logging for the HTTP or WebSocket libraries in production; it can record request headers, including your API key. See [the security policy](https://github.com/RightNow-AI/system-zero-sdk/blob/main/SECURITY.md)
221
241
  for private vulnerability reporting.
222
242
 
223
243
  ## Function tool formats
@@ -299,3 +319,19 @@ Billing is per decision, not per connection, heartbeat, acknowledgement, or
299
319
  mutation. Several mutations can be covered by one decision. A decision may be
300
320
  counted even if the connection closes before its event arrives; use the event's
301
321
  usage and request identifier when reconciling completed work.
322
+
323
+ ## LongContextRequest opt-in
324
+
325
+ The SDK publishes the `LongContextRequest` type as an opt-in surface. It is unavailable until the API enables it for the workspace; workspace opt-in through the console's Settings is also required. Once available, give explicit per-request consent with `dangerZone: 'accept'` in TypeScript or `danger_zone="accept"` in Python. The option sends exactly `SZ-Danger-Zone: accept`; it is never sent by default or inferred from state size. Until availability, the API ignores or refuses this option. Do not supply this header through custom headers. Retries retain the option. The same explicit option is supported by batch and flow helpers.
326
+
327
+ Until workspace enablement, the expected result is HTTP 404 `long_context_not_available`, surfaced as `LongContextNotAvailableError`. The same refusal applies without workspace opt-in. When the API supplies `details.eligibility`, `LongContextNotAvailableError.eligibility` exposes the rule: `billing`, `opt_in`, `class`, `deny_list`, `global_switch` or `account_deleted`, with a string fallback for future rules. A generic held-route refusal can omit the rule. Status, code, message and request ID remain intact. Held routes are never retried.
328
+
329
+ `LongContextRequest`, `EnabledDecideRequest`, `OrdinaryRequest`, `MediaRequest` and their question types describe the separate enabled request contract in `enabled.openapi.json`. `LongContextLimits` describes its discovery limits. `openapi.json` remains the served contract. The API selects the request band and checks bounds: a `LongContextRequest` takes at most 32 questions, choices with 2 to 25 options, and scores with 2 to 4 levels. Only inline document attachments fit its document branch. The API validates document admission separately.
330
+
331
+ ```typescript
332
+ await client.decide(request, { dangerZone: 'accept' });
333
+ ```
334
+
335
+ ```python
336
+ client.decide(request, danger_zone="accept")
337
+ ```
@@ -9,12 +9,14 @@ src/rightnow_system_zero.egg-info/requires.txt
9
9
  src/rightnow_system_zero.egg-info/top_level.txt
10
10
  src/system_zero/__init__.py
11
11
  src/system_zero/_connection.py
12
+ src/system_zero/_redaction.py
12
13
  src/system_zero/_retry.py
13
14
  src/system_zero/_transport.py
14
15
  src/system_zero/argument-limits.json
15
16
  src/system_zero/batch.py
16
17
  src/system_zero/client.py
17
18
  src/system_zero/decisions.py
19
+ src/system_zero/enabled_types.py
18
20
  src/system_zero/error-catalog.json
19
21
  src/system_zero/error_codes.py
20
22
  src/system_zero/errors.py
@@ -1,4 +1,4 @@
1
- """System Zero API client and client-side helpers."""
1
+ """RightNow System Zero API client and client-side helpers."""
2
2
 
3
3
  from .client import (
4
4
  VERSION,
@@ -143,6 +143,19 @@ from .workflow_types import (
143
143
  Usage,
144
144
  )
145
145
 
146
+ from .enabled_types import (
147
+ EnabledDecideRequest,
148
+ LongContextLimits,
149
+ LongContextQuestion,
150
+ LongContextRequest,
151
+ MediaQuestion,
152
+ MediaRequest,
153
+ OrdinaryQuestion,
154
+ OrdinaryRequest,
155
+ VideoAttachment,
156
+ VideoRequest,
157
+ )
158
+
146
159
  from .tools import (
147
160
  NESTED_FUNCTION_TOOLS,
148
161
  FLAT_FUNCTION_TOOLS,
@@ -155,7 +168,7 @@ from .tools import (
155
168
  __version__ = VERSION
156
169
 
157
170
  from .types import Attachment
158
- from .media import (ByteData, MAX_ATTACHMENTS, MAX_INLINE_BYTES, MEDIA_LIMITS, MIMES, attachment_magic_matches, inline_attachment, validate_attachments)
171
+ from .media import (ByteData, MAX_ATTACHMENTS, MAX_INLINE_BYTES, MEDIA_LIMITS, MIMES, VIDEO_PROFILE, attachment_magic_matches, inline_attachment, validate_attachments, video_attachment)
159
172
 
160
173
  __all__ = [
161
174
  "APIConnectionError",
@@ -180,6 +193,7 @@ __all__ = [
180
193
  "DecisionMode",
181
194
  "DeliberateNotAvailableError",
182
195
  "DocumentNotAvailableError",
196
+ "EnabledDecideRequest",
183
197
  "ErrorCode",
184
198
  "EvaluationRecord",
185
199
  "EvaluationReport",
@@ -206,17 +220,24 @@ __all__ = [
206
220
  "InvalidRequestError",
207
221
  "KNOWN_ERROR_CODES",
208
222
  "LegacyDecision",
223
+ "LongContextLimits",
209
224
  "LongContextNotAvailableError",
225
+ "LongContextQuestion",
226
+ "LongContextRequest",
210
227
  "LongRecordingNotAvailableError",
211
228
  "MAX_ATTACHMENTS",
212
229
  "MAX_FLOW_STEPS",
213
230
  "MAX_INLINE_BYTES",
214
231
  "MEDIA_LIMITS",
215
232
  "MIMES",
233
+ "MediaQuestion",
234
+ "MediaRequest",
216
235
  "Model",
217
236
  "ModelList",
218
237
  "NESTED_FUNCTION_TOOLS",
219
238
  "NotFoundError",
239
+ "OrdinaryQuestion",
240
+ "OrdinaryRequest",
220
241
  "OverloadedError",
221
242
  "PayloadTooLargeError",
222
243
  "PermissionError",
@@ -254,7 +275,10 @@ __all__ = [
254
275
  "ToolHandler",
255
276
  "Usage",
256
277
  "VERSION",
278
+ "VIDEO_PROFILE",
279
+ "VideoAttachment",
257
280
  "VideoNotAvailableError",
281
+ "VideoRequest",
258
282
  "__version__",
259
283
  "attachment_magic_matches",
260
284
  "create_tool_handler",
@@ -277,5 +301,6 @@ __all__ = [
277
301
  "stream_error",
278
302
  "validate_attachments",
279
303
  "validate_expected",
280
- "validate_flow"
304
+ "validate_flow",
305
+ "video_attachment"
281
306
  ]
@@ -0,0 +1,207 @@
1
+ """Credential-safe diagnostics without retaining credentials after client GC."""
2
+ import functools
3
+ import builtins
4
+ import logging
5
+ import re
6
+ from contextvars import ContextVar
7
+ from types import MappingProxyType, MemberDescriptorType, GetSetDescriptorType
8
+ from websockets.exceptions import ConnectionClosed, ConnectionClosedOK, ConnectionClosedError
9
+ from websockets.frames import Close
10
+ from collections.abc import Mapping
11
+
12
+ _active_key = ContextVar('system_zero_diagnostic_key', default=None)
13
+ _sensitive = re.compile(r'^(authorization|proxy-authorization|api[_-]?key)$', re.I)
14
+
15
+
16
+ class Secret:
17
+ __slots__ = ('__value', '__weakref__')
18
+
19
+ def __init__(self, value):
20
+ self.__value = value
21
+
22
+ def reveal(self):
23
+ return self.__value
24
+
25
+ def __repr__(self):
26
+ return '[redacted]'
27
+
28
+ __str__ = __repr__
29
+
30
+ def __reduce_ex__(self, protocol):
31
+ raise TypeError('SDK credentials cannot be pickled.')
32
+
33
+
34
+ def reveal(value):
35
+ # Revalidation still handles an application overwriting a private attribute.
36
+ return value.reveal() if isinstance(value, Secret) else value
37
+
38
+
39
+ def redact_text(value, key=None):
40
+ key = reveal(key if key is not None else _active_key.get())
41
+ if key:
42
+ value = value.replace(key, '[redacted]')
43
+ return re.sub(r'\bBearer[ \t]+[A-Za-z0-9._~+/-]+=*', 'Bearer [redacted]', value, flags=re.I)
44
+
45
+
46
+ def redact(value, _seen=None, _depth=0):
47
+ if isinstance(value, str):
48
+ return redact_text(value)
49
+ if isinstance(value, bytes):
50
+ clean = redact_text(value.decode('utf-8', errors='replace')).encode('utf-8')
51
+ return value if clean == value else clean
52
+ if value is None or isinstance(value, (bool, int, float, Secret)):
53
+ return value
54
+ seen = set() if _seen is None else _seen
55
+ if id(value) in seen or _depth > 32 or len(seen) > 4096:
56
+ return '[omitted]'
57
+ seen.add(id(value))
58
+ if isinstance(value, Mapping):
59
+ clean = {redact_text(str(k)): redact(v, seen, _depth + 1) for k, v in value.items()
60
+ if not _sensitive.fullmatch(str(k))}
61
+ return value if len(clean) == len(value) and all(k in value and v is value[k] for k, v in clean.items()) else clean
62
+ if isinstance(value, (tuple, list)):
63
+ clean = type(value)(redact(v, seen, _depth + 1) for v in value)
64
+ return value if all(a is b for a, b in zip(value, clean)) else clean
65
+ return '[omitted]'
66
+
67
+
68
+ def protect_exception(error, seen=None):
69
+ """Scrub reachable native causes, including suppressed Python contexts."""
70
+ seen = set() if seen is None else seen
71
+ if id(error) in seen:
72
+ return Exception('Cyclic exception context omitted.')
73
+ if len(seen) >= 32:
74
+ return Exception('Further exception context omitted.')
75
+ seen.add(id(error))
76
+ # Foreign exception classes may print or pickle hidden slots through custom hooks.
77
+ # Keep SDK/builtin classifications; retain only plain diagnostic state otherwise.
78
+ module = type(error).__module__
79
+ close = isinstance(error, ConnectionClosed)
80
+ if close:
81
+ def frame(value):
82
+ return None if value is None else Close(value.code, redact_text(value.reason))
83
+ cls = ConnectionClosedOK if isinstance(error, ConnectionClosedOK) else ConnectionClosedError if isinstance(error, ConnectionClosedError) else ConnectionClosed
84
+ clean = cls(frame(error.rcvd), frame(error.sent), error.rcvd_then_sent)
85
+ clean.__dict__.update({name: value for name, value in vars(error).items()
86
+ if name not in ('rcvd', 'sent', 'rcvd_then_sent')})
87
+ clean.__cause__, clean.__context__, clean.__traceback__ = error.__cause__, error.__context__, error.__traceback__
88
+ error = clean
89
+ elif module != 'builtins' and not module.startswith('system_zero.'):
90
+ clean = Exception(*redact(error.args))
91
+ clean.__dict__.update(vars(error))
92
+ clean.__cause__, clean.__context__, clean.__traceback__ = error.__cause__, error.__context__, error.__traceback__
93
+ error = clean
94
+ group = isinstance(error, getattr(builtins, 'BaseExceptionGroup', ()))
95
+ if group:
96
+ children = tuple(protect_exception(child, seen) for child in error.exceptions)
97
+ clean = builtins.BaseExceptionGroup(redact_text(error.message), children)
98
+ clean.__dict__.update(vars(error))
99
+ clean.__cause__, clean.__context__, clean.__traceback__ = error.__cause__, error.__context__, error.__traceback__
100
+ error = clean
101
+ elif not close:
102
+ error.args = redact(error.args)
103
+ # Built-in exception printing and pickling use native slots independently
104
+ # of args/__dict__ (SyntaxError, UnicodeError, AttributeError, ImportError...).
105
+ # Enumerate the built-in descriptors, not arbitrary foreign properties.
106
+ excluded = {'args', '__dict__', '__weakref__', '__traceback__', '__cause__', '__context__', 'exceptions', 'message'}
107
+ for cls in type(error).__mro__:
108
+ if cls.__module__ != 'builtins':
109
+ continue
110
+ for name, descriptor in vars(cls).items():
111
+ if name.startswith('__') or name in excluded or not isinstance(descriptor, (MemberDescriptorType, GetSetDescriptorType)):
112
+ continue
113
+ value = getattr(error, name, None)
114
+ clean = redact(value)
115
+ if clean is value:
116
+ continue
117
+ try:
118
+ setattr(error, name, clean)
119
+ except (AttributeError, TypeError):
120
+ # Unknown read-only native state cannot safely retain its type.
121
+ clean = Exception(*redact(error.args))
122
+ clean.__dict__.update(vars(error))
123
+ clean.__cause__, clean.__context__, clean.__traceback__ = error.__cause__, error.__context__, error.__traceback__
124
+ error = clean
125
+ break
126
+ for name, value in list(vars(error).items()):
127
+ if redact_text(name) != name or _sensitive.fullmatch(name) or name in ('request', '_request', 'response', 'config'):
128
+ delattr(error, name)
129
+ elif close and name in ('rcvd', 'sent'):
130
+ continue
131
+ elif isinstance(value, BaseException):
132
+ setattr(error, name, protect_exception(value, seen))
133
+ else:
134
+ clean = redact(value)
135
+ setattr(error, name, MappingProxyType(clean) if isinstance(value, MappingProxyType) else clean)
136
+ for name in ('__cause__', '__context__'):
137
+ cause = getattr(error, name)
138
+ if cause is not None:
139
+ setattr(error, name, protect_exception(cause, seen))
140
+ return error
141
+
142
+
143
+ def protected(method):
144
+ @functools.wraps(method)
145
+ def call(*args, **kwargs):
146
+ owner = args[0] if args else kwargs.get('client')
147
+ token = _active_key.set(getattr(owner, '_api_key', getattr(getattr(owner, '_client', None), '_api_key', None)))
148
+ try:
149
+ try:
150
+ return method(*args, **kwargs)
151
+ except Exception as error:
152
+ failure = protect_exception(error)
153
+ if failure is error:
154
+ raise
155
+ # Outside the except suite: do not attach the raw group as context.
156
+ raise failure from None
157
+ finally:
158
+ _active_key.reset(token)
159
+ return call
160
+
161
+
162
+ def protected_async(method):
163
+ @functools.wraps(method)
164
+ async def call(*args, **kwargs):
165
+ token = _active_key.set(getattr(args[0], '_api_key', None))
166
+ try:
167
+ try:
168
+ return await method(*args, **kwargs)
169
+ except Exception as error:
170
+ failure = protect_exception(error)
171
+ if failure is error:
172
+ raise
173
+ raise failure from None
174
+ finally:
175
+ _active_key.reset(token)
176
+ return call
177
+
178
+
179
+ class DiagnosticLogger(logging.LoggerAdapter):
180
+ """Redact before LogRecord creation; leave application logging untouched."""
181
+ def __init__(self, key):
182
+ super().__init__(logging.getLogger('websockets.client'), {})
183
+ self._key = key
184
+
185
+ def log(self, level, msg, *args, **kwargs):
186
+ if self.isEnabledFor(level):
187
+ text = str(msg) % args if args else str(msg)
188
+ # Native exceptions may carry requests. Never attach exc_info here.
189
+ kwargs.pop('exc_info', None)
190
+ kwargs.pop('extra', None)
191
+ self.logger.log(level, redact_text(text, self._key), **kwargs)
192
+
193
+
194
+ class _ContextFilter(logging.Filter):
195
+ def filter(self, record):
196
+ if _active_key.get() is not None:
197
+ record.msg = redact_text(record.getMessage())
198
+ record.args = ()
199
+ # A native traceback can retain a request with credential headers.
200
+ record.exc_info = record.exc_text = None
201
+ return True
202
+
203
+
204
+ # Library loggers, not handlers or the root logger. ContextVar isolates other callers.
205
+ _filter = _ContextFilter()
206
+ for _logger_name in ('httpx', 'httpcore.connection', 'httpcore.http11', 'httpcore.http2', 'httpcore.proxy', 'httpcore.socks'):
207
+ logging.getLogger(_logger_name).addFilter(_filter)