pi-agent-python-sdk 0.1.0__tar.gz → 0.2.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.
- pi_agent_python_sdk-0.2.0/CHANGELOG.md +66 -0
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/PKG-INFO +46 -28
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/README.md +43 -25
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/pyproject.toml +3 -3
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/__init__.py +3 -0
- pi_agent_python_sdk-0.2.0/src/pi_agent/_collections.py +59 -0
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/_events.py +58 -17
- pi_agent_python_sdk-0.2.0/src/pi_agent/_observation.py +73 -0
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/_runs.py +49 -24
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/_transport.py +107 -25
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/client.py +273 -46
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/errors.py +1 -1
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/sync.py +222 -37
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/types.py +21 -16
- pi_agent_python_sdk-0.1.0/CHANGELOG.md +0 -28
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/.gitignore +0 -0
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/LICENSE +0 -0
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/_launch.py +0 -0
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/_usage.py +0 -0
- {pi_agent_python_sdk-0.1.0 → pi_agent_python_sdk-0.2.0}/src/pi_agent/py.typed +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- `prompt()` returns `None` after checked acknowledgement. Use `request("prompt", ...)`
|
|
10
|
+
for the response envelope and ID; `AcceptanceReceipt` is removed.
|
|
11
|
+
- `cycle_thinking_level()` and `export_html()` return their complete result
|
|
12
|
+
dictionaries, preserving unknown fields. Read `result["level"]` or `result["path"]`.
|
|
13
|
+
- Session mutations invalidate cached identity without an implicit state query.
|
|
14
|
+
Call `get_state()` explicitly when updated identity is needed.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- TypeScript-style listeners and settlement helpers: `on_event()`,
|
|
19
|
+
`collect_events()`, `wait_for_idle()`, and `prompt_and_wait()` through both facades.
|
|
20
|
+
- Opt-in observation of original stderr/stdout bytes and all parsed RPC objects,
|
|
21
|
+
with bounded queues and explicit completion/loss status.
|
|
22
|
+
- Subscriptions before startup, including extension events and startup failures.
|
|
23
|
+
- Maintained TypeScript command/event comparison for source checkouts and installed
|
|
24
|
+
wheels across supported Python and operating-system versions.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Stream events before prompt acknowledgement while still requiring successful
|
|
29
|
+
acknowledgement and settlement before completing a result.
|
|
30
|
+
- Preserve UI callback causes; dialog expiry cancels only the dialog, and late
|
|
31
|
+
handler answers cannot override it.
|
|
32
|
+
- Deliver buffered event prefixes before terminal failures; preserve overflow
|
|
33
|
+
errors during concurrent blocking-context cleanup.
|
|
34
|
+
- Yield during finite output bursts and batch blocking reads; result-only runs
|
|
35
|
+
no longer buffer discarded progress. Slow consumers still fail explicitly.
|
|
36
|
+
- Preserve JSON-escaped lone surrogates on outbound commands and UI replies.
|
|
37
|
+
|
|
38
|
+
- Share deadline validation across runs, commands, collectors, and limits; invalid
|
|
39
|
+
types and integers too large for timers consistently raise `ValueError`.
|
|
40
|
+
- Document the retained run/stream conveniences and the reasons for their
|
|
41
|
+
internal ownership, session, and cleanup rules.
|
|
42
|
+
|
|
43
|
+
## 0.1.0
|
|
44
|
+
|
|
45
|
+
- Introduce the `pi-agent-python-sdk` distribution with the `pi_agent` Python
|
|
46
|
+
import. Users of earlier source checkouts must update their imports.
|
|
47
|
+
- Keep the standalone SDK scoped to core RPC with zero runtime dependencies;
|
|
48
|
+
document loading caller-owned extensions through native Pi arguments.
|
|
49
|
+
- Prevent owned runs from claiming delayed events after low-level submissions.
|
|
50
|
+
- Bound retained run messages independently of event queues.
|
|
51
|
+
- Preserve unknown usage measurements, concatenate final text blocks directly,
|
|
52
|
+
and measure run latency from submission to settlement.
|
|
53
|
+
- Reject malformed known text deltas while preserving unknown event variants.
|
|
54
|
+
- Add `AsyncPiClient` and `PiClient` with explicit methods for all 33 Pi 0.85.1 RPC commands.
|
|
55
|
+
- Add typed wire payloads, future-only event subscriptions, extension dialogs,
|
|
56
|
+
streamed text, and settled results with session identity and observed usage.
|
|
57
|
+
- Preserve Pi configuration while owning process readiness, bounded I/O,
|
|
58
|
+
checked failures, cancellation cleanup, and offline compatibility checks.
|
|
59
|
+
- Add deterministic subprocess tests and isolated real-Pi integration using a
|
|
60
|
+
local faux provider, including retries, compaction, tools, and saved sessions.
|
|
61
|
+
- Add nine runnable examples and guides for the API, errors, compatibility,
|
|
62
|
+
dependency updates, and releases.
|
|
63
|
+
- Add Python/platform CI, Dependabot, daily latest-Pi checks, reviewed source
|
|
64
|
+
fingerprints, distribution inspection, and Trusted Publishing workflows.
|
|
65
|
+
- Verify published wheel and source hashes, and smoke-test index installations
|
|
66
|
+
before promoting the same artifacts from TestPyPI to PyPI.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pi-agent-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Typed async and synchronous Python clients for Pi's agent RPC runtime
|
|
5
5
|
Project-URL: Homepage, https://github.com/cheenulabs/pi-agent-python-sdk
|
|
6
|
-
Project-URL: Documentation, https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.
|
|
6
|
+
Project-URL: Documentation, https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.2.0/docs
|
|
7
7
|
Project-URL: Issues, https://github.com/cheenulabs/pi-agent-python-sdk/issues
|
|
8
|
-
Project-URL: Changelog, https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.
|
|
8
|
+
Project-URL: Changelog, https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/CHANGELOG.md
|
|
9
9
|
Author: cheenulabs
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
License-File: LICENSE
|
|
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
|
|
24
24
|
# Pi Agent Python SDK
|
|
25
25
|
|
|
26
|
+
[][pypi]
|
|
26
27
|
[][metadata]
|
|
27
28
|
[][license]
|
|
28
29
|
|
|
@@ -52,10 +53,10 @@ pi --version
|
|
|
52
53
|
Run `pi` once to configure your provider and model using Pi's normal setup.
|
|
53
54
|
The SDK uses that configuration when it starts Pi.
|
|
54
55
|
|
|
55
|
-
Install the Python package:
|
|
56
|
+
Install the Python package from [PyPI][pypi]:
|
|
56
57
|
|
|
57
58
|
```sh
|
|
58
|
-
python -m pip install pi-agent-python-sdk==0.
|
|
59
|
+
python -m pip install pi-agent-python-sdk==0.2.0
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
The distribution is named `pi-agent-python-sdk`; import it as
|
|
@@ -73,6 +74,19 @@ The context manager starts and closes Pi. `run()` waits for the conversation to
|
|
|
73
74
|
settle, including retries and queued follow-ups. The result includes finalized
|
|
74
75
|
messages, session identity, elapsed time, and observed assistant usage.
|
|
75
76
|
|
|
77
|
+
Continue the conversation with another call on the same client:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from pi_agent import PiClient
|
|
81
|
+
|
|
82
|
+
with PiClient() as pi:
|
|
83
|
+
print(pi.run("Explain this project's entry points without editing files.").text)
|
|
84
|
+
print(pi.run("Which of those entry points handles configuration?").text)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Pi keeps the conversation context. Each result contains only the messages and
|
|
88
|
+
answer from that call; a call with no assistant output has empty text.
|
|
89
|
+
|
|
76
90
|
## What you can do
|
|
77
91
|
|
|
78
92
|
- **Run and stream:** get a final answer or consume text, thinking, and tool events.
|
|
@@ -97,8 +111,7 @@ with PiClient() as pi:
|
|
|
97
111
|
```
|
|
98
112
|
|
|
99
113
|
`event.raw` contains the full Pi event, including fields the SDK does not yet
|
|
100
|
-
recognize. Keep the stream inside its context: leaving early
|
|
101
|
-
and aborts the work it owns. See [errors and cancellation][errors] for
|
|
114
|
+
recognize. Keep the stream inside its context: leaving early cleans up unfinished work. See [errors and cancellation][errors] for
|
|
102
115
|
handling timeouts and partial results.
|
|
103
116
|
|
|
104
117
|
## Async usage
|
|
@@ -164,6 +177,8 @@ Choose the interface that fits the work:
|
|
|
164
177
|
| A completed conversation result | `run()` |
|
|
165
178
|
| Events while a conversation runs | `stream()` |
|
|
166
179
|
| Prompt acknowledgement and your own event handling | `prompt()` with `events()` |
|
|
180
|
+
| Session events through the next settlement | `prompt_and_wait()` or `collect_events()` |
|
|
181
|
+
| Every event without retaining history | `on_event(callback)` |
|
|
167
182
|
| A specific Pi operation | `get_state()`, `set_model()`, `fork()`, and other command methods |
|
|
168
183
|
| A raw command response envelope | `request()` |
|
|
169
184
|
|
|
@@ -181,10 +196,10 @@ Python arguments use `snake_case`; wire dictionaries retain Pi's `camelCase`
|
|
|
181
196
|
fields. `prompt()` acknowledges submission, which may be handled entirely by an
|
|
182
197
|
extension. It does not wait for a completed answer.
|
|
183
198
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
Use one client per conversation and wait for each `run()` or stream to finish
|
|
200
|
+
before starting the next. After using `prompt()` for your own event handling,
|
|
201
|
+
use a fresh client for `run()` or `stream()` so results cannot include delayed
|
|
202
|
+
events from earlier work. The SDK launches a new process and cannot attach to an existing
|
|
188
203
|
Pi terminal session.
|
|
189
204
|
|
|
190
205
|
See [RPC structure][rpc] for the protocol mapping and module layout, and
|
|
@@ -200,6 +215,7 @@ the [command reference][commands] for every method.
|
|
|
200
215
|
| [RPC structure][rpc] | How the Python client maps to Pi's protocol |
|
|
201
216
|
| [Compatibility][compatibility] | Runtime versions and platform validation |
|
|
202
217
|
| [Examples][examples] | Runnable sync, async, streaming, sessions, steering, and UI examples |
|
|
218
|
+
| [Release notes][release] | Changes included in 0.2.0 |
|
|
203
219
|
|
|
204
220
|
Examples use your configured Pi and may make provider calls. Development checks
|
|
205
221
|
use an isolated local test provider; see [CONTRIBUTING.md][contributing] for
|
|
@@ -212,20 +228,22 @@ Maintainers: [protocol discovery][discovery] ·
|
|
|
212
228
|
|
|
213
229
|
[MIT][license]
|
|
214
230
|
|
|
215
|
-
[
|
|
216
|
-
[
|
|
217
|
-
[
|
|
218
|
-
[
|
|
219
|
-
[
|
|
220
|
-
[
|
|
221
|
-
[
|
|
222
|
-
[
|
|
223
|
-
[
|
|
224
|
-
[
|
|
225
|
-
[
|
|
226
|
-
[
|
|
227
|
-
[
|
|
228
|
-
[
|
|
229
|
-
[
|
|
230
|
-
[
|
|
231
|
-
[
|
|
231
|
+
[pypi]: https://pypi.org/project/pi-agent-python-sdk/
|
|
232
|
+
[release]: https://github.com/cheenulabs/pi-agent-python-sdk/releases/tag/v0.2.0
|
|
233
|
+
[metadata]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/pyproject.toml
|
|
234
|
+
[license]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/LICENSE
|
|
235
|
+
[compatibility]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/compatibility.md
|
|
236
|
+
[errors]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/errors.md
|
|
237
|
+
[stream-example]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/examples/stream.py
|
|
238
|
+
[constructor-options]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/api.md#constructor-options
|
|
239
|
+
[extensions]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/usage.md#using-your-own-extensions
|
|
240
|
+
[ui-example]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/examples/ui.py
|
|
241
|
+
[rpc]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/rpc.md
|
|
242
|
+
[commands]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/api.md#all-33-rpc-commands
|
|
243
|
+
[usage]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/usage.md
|
|
244
|
+
[api]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/api.md
|
|
245
|
+
[examples]: https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.2.0/examples
|
|
246
|
+
[contributing]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/CONTRIBUTING.md
|
|
247
|
+
[discovery]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/discovery.md
|
|
248
|
+
[maintenance]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/maintenance.md
|
|
249
|
+
[releasing]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/releasing.md
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Pi Agent Python SDK
|
|
2
2
|
|
|
3
|
+
[][pypi]
|
|
3
4
|
[][metadata]
|
|
4
5
|
[][license]
|
|
5
6
|
|
|
@@ -29,10 +30,10 @@ pi --version
|
|
|
29
30
|
Run `pi` once to configure your provider and model using Pi's normal setup.
|
|
30
31
|
The SDK uses that configuration when it starts Pi.
|
|
31
32
|
|
|
32
|
-
Install the Python package:
|
|
33
|
+
Install the Python package from [PyPI][pypi]:
|
|
33
34
|
|
|
34
35
|
```sh
|
|
35
|
-
python -m pip install pi-agent-python-sdk==0.
|
|
36
|
+
python -m pip install pi-agent-python-sdk==0.2.0
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
The distribution is named `pi-agent-python-sdk`; import it as
|
|
@@ -50,6 +51,19 @@ The context manager starts and closes Pi. `run()` waits for the conversation to
|
|
|
50
51
|
settle, including retries and queued follow-ups. The result includes finalized
|
|
51
52
|
messages, session identity, elapsed time, and observed assistant usage.
|
|
52
53
|
|
|
54
|
+
Continue the conversation with another call on the same client:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from pi_agent import PiClient
|
|
58
|
+
|
|
59
|
+
with PiClient() as pi:
|
|
60
|
+
print(pi.run("Explain this project's entry points without editing files.").text)
|
|
61
|
+
print(pi.run("Which of those entry points handles configuration?").text)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Pi keeps the conversation context. Each result contains only the messages and
|
|
65
|
+
answer from that call; a call with no assistant output has empty text.
|
|
66
|
+
|
|
53
67
|
## What you can do
|
|
54
68
|
|
|
55
69
|
- **Run and stream:** get a final answer or consume text, thinking, and tool events.
|
|
@@ -74,8 +88,7 @@ with PiClient() as pi:
|
|
|
74
88
|
```
|
|
75
89
|
|
|
76
90
|
`event.raw` contains the full Pi event, including fields the SDK does not yet
|
|
77
|
-
recognize. Keep the stream inside its context: leaving early
|
|
78
|
-
and aborts the work it owns. See [errors and cancellation][errors] for
|
|
91
|
+
recognize. Keep the stream inside its context: leaving early cleans up unfinished work. See [errors and cancellation][errors] for
|
|
79
92
|
handling timeouts and partial results.
|
|
80
93
|
|
|
81
94
|
## Async usage
|
|
@@ -141,6 +154,8 @@ Choose the interface that fits the work:
|
|
|
141
154
|
| A completed conversation result | `run()` |
|
|
142
155
|
| Events while a conversation runs | `stream()` |
|
|
143
156
|
| Prompt acknowledgement and your own event handling | `prompt()` with `events()` |
|
|
157
|
+
| Session events through the next settlement | `prompt_and_wait()` or `collect_events()` |
|
|
158
|
+
| Every event without retaining history | `on_event(callback)` |
|
|
144
159
|
| A specific Pi operation | `get_state()`, `set_model()`, `fork()`, and other command methods |
|
|
145
160
|
| A raw command response envelope | `request()` |
|
|
146
161
|
|
|
@@ -158,10 +173,10 @@ Python arguments use `snake_case`; wire dictionaries retain Pi's `camelCase`
|
|
|
158
173
|
fields. `prompt()` acknowledges submission, which may be handled entirely by an
|
|
159
174
|
extension. It does not wait for a completed answer.
|
|
160
175
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
Use one client per conversation and wait for each `run()` or stream to finish
|
|
177
|
+
before starting the next. After using `prompt()` for your own event handling,
|
|
178
|
+
use a fresh client for `run()` or `stream()` so results cannot include delayed
|
|
179
|
+
events from earlier work. The SDK launches a new process and cannot attach to an existing
|
|
165
180
|
Pi terminal session.
|
|
166
181
|
|
|
167
182
|
See [RPC structure][rpc] for the protocol mapping and module layout, and
|
|
@@ -177,6 +192,7 @@ the [command reference][commands] for every method.
|
|
|
177
192
|
| [RPC structure][rpc] | How the Python client maps to Pi's protocol |
|
|
178
193
|
| [Compatibility][compatibility] | Runtime versions and platform validation |
|
|
179
194
|
| [Examples][examples] | Runnable sync, async, streaming, sessions, steering, and UI examples |
|
|
195
|
+
| [Release notes][release] | Changes included in 0.2.0 |
|
|
180
196
|
|
|
181
197
|
Examples use your configured Pi and may make provider calls. Development checks
|
|
182
198
|
use an isolated local test provider; see [CONTRIBUTING.md][contributing] for
|
|
@@ -189,20 +205,22 @@ Maintainers: [protocol discovery][discovery] ·
|
|
|
189
205
|
|
|
190
206
|
[MIT][license]
|
|
191
207
|
|
|
192
|
-
[
|
|
193
|
-
[
|
|
194
|
-
[
|
|
195
|
-
[
|
|
196
|
-
[
|
|
197
|
-
[
|
|
198
|
-
[
|
|
199
|
-
[
|
|
200
|
-
[
|
|
201
|
-
[
|
|
202
|
-
[
|
|
203
|
-
[
|
|
204
|
-
[
|
|
205
|
-
[
|
|
206
|
-
[
|
|
207
|
-
[
|
|
208
|
-
[
|
|
208
|
+
[pypi]: https://pypi.org/project/pi-agent-python-sdk/
|
|
209
|
+
[release]: https://github.com/cheenulabs/pi-agent-python-sdk/releases/tag/v0.2.0
|
|
210
|
+
[metadata]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/pyproject.toml
|
|
211
|
+
[license]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/LICENSE
|
|
212
|
+
[compatibility]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/compatibility.md
|
|
213
|
+
[errors]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/errors.md
|
|
214
|
+
[stream-example]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/examples/stream.py
|
|
215
|
+
[constructor-options]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/api.md#constructor-options
|
|
216
|
+
[extensions]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/usage.md#using-your-own-extensions
|
|
217
|
+
[ui-example]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/examples/ui.py
|
|
218
|
+
[rpc]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/rpc.md
|
|
219
|
+
[commands]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/api.md#all-33-rpc-commands
|
|
220
|
+
[usage]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/usage.md
|
|
221
|
+
[api]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/api.md
|
|
222
|
+
[examples]: https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.2.0/examples
|
|
223
|
+
[contributing]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/CONTRIBUTING.md
|
|
224
|
+
[discovery]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/discovery.md
|
|
225
|
+
[maintenance]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/maintenance.md
|
|
226
|
+
[releasing]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/docs/releasing.md
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pi-agent-python-sdk"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Typed async and synchronous Python clients for Pi's agent RPC runtime"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -26,9 +26,9 @@ dependencies = []
|
|
|
26
26
|
|
|
27
27
|
[project.urls]
|
|
28
28
|
Homepage = "https://github.com/cheenulabs/pi-agent-python-sdk"
|
|
29
|
-
Documentation = "https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.
|
|
29
|
+
Documentation = "https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.2.0/docs"
|
|
30
30
|
Issues = "https://github.com/cheenulabs/pi-agent-python-sdk/issues"
|
|
31
|
-
Changelog = "https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.
|
|
31
|
+
Changelog = "https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.0/CHANGELOG.md"
|
|
32
32
|
|
|
33
33
|
[dependency-groups]
|
|
34
34
|
dev = [
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Importing this package never launches Pi or changes its configuration.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from ._observation import ObservationStatus, ProcessOutput
|
|
6
7
|
from .client import AsyncPiClient
|
|
7
8
|
from .errors import (
|
|
8
9
|
PiBusyError,
|
|
@@ -27,6 +28,8 @@ __all__ = [
|
|
|
27
28
|
"Event",
|
|
28
29
|
"ImageContent",
|
|
29
30
|
"Limits",
|
|
31
|
+
"ObservationStatus",
|
|
32
|
+
"ProcessOutput",
|
|
30
33
|
"PiBusyError",
|
|
31
34
|
"PiCommandError",
|
|
32
35
|
"PiClient",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Bounded observation through the next settlement, without run ownership."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from ._events import EventSubscription
|
|
9
|
+
from .errors import PiProcessError, PiResultOverflow, PiTimeoutError
|
|
10
|
+
from .types import Event, Limits, _validate_timeout
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def deadline(timeout: float | None) -> float | None:
|
|
14
|
+
if timeout is None:
|
|
15
|
+
return None
|
|
16
|
+
_validate_timeout(timeout, "Collection timeout")
|
|
17
|
+
return asyncio.get_running_loop().time() + timeout
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def start_collection(
|
|
21
|
+
subscription: EventSubscription,
|
|
22
|
+
limits: Limits,
|
|
23
|
+
*,
|
|
24
|
+
timeout: float | None,
|
|
25
|
+
retain: bool,
|
|
26
|
+
) -> asyncio.Task[list[Event] | None]:
|
|
27
|
+
expires = deadline(timeout)
|
|
28
|
+
# Register now, not when the task first runs: callers may submit immediately.
|
|
29
|
+
subscription._enter()
|
|
30
|
+
|
|
31
|
+
async def consume() -> list[Event] | None:
|
|
32
|
+
events: list[Event] = []
|
|
33
|
+
size = 0
|
|
34
|
+
timer = asyncio.timeout_at(expires)
|
|
35
|
+
try:
|
|
36
|
+
async with timer:
|
|
37
|
+
async for event in subscription:
|
|
38
|
+
if retain:
|
|
39
|
+
size += len(json.dumps(event.raw).encode("utf-8"))
|
|
40
|
+
if (
|
|
41
|
+
len(events) >= limits.collection_event_count
|
|
42
|
+
or size > limits.collection_event_bytes
|
|
43
|
+
):
|
|
44
|
+
raise PiResultOverflow("Event collection exceeded its count/byte limit")
|
|
45
|
+
events.append(event)
|
|
46
|
+
if event.type == "agent_settled":
|
|
47
|
+
return events if retain else None
|
|
48
|
+
raise PiProcessError("Event collection closed before settlement")
|
|
49
|
+
except TimeoutError as exc:
|
|
50
|
+
if not timer.expired():
|
|
51
|
+
raise
|
|
52
|
+
raise PiTimeoutError("Event collection deadline elapsed", uncertain=False) from exc
|
|
53
|
+
finally:
|
|
54
|
+
await subscription.aclose()
|
|
55
|
+
|
|
56
|
+
task = asyncio.create_task(consume(), name="pi-event-collection")
|
|
57
|
+
# Cancellation before the task's first instruction cannot run its finally.
|
|
58
|
+
task.add_done_callback(lambda _: subscription._discard())
|
|
59
|
+
return task
|
|
@@ -6,13 +6,15 @@ import asyncio
|
|
|
6
6
|
from collections import deque
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from types import TracebackType
|
|
9
|
-
from typing import Self
|
|
9
|
+
from typing import Generic, Self, TypeVar
|
|
10
10
|
|
|
11
11
|
from .errors import PiSubscriptionOverflow
|
|
12
12
|
from .types import Event, Limits
|
|
13
13
|
|
|
14
|
+
T = TypeVar("T")
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
class _Subscription(Generic[T]):
|
|
16
18
|
"""Enter before submitting work to observe its events without a history buffer.
|
|
17
19
|
|
|
18
20
|
A slow consumer receives PiSubscriptionOverflow rather than silently losing
|
|
@@ -22,13 +24,13 @@ class EventSubscription:
|
|
|
22
24
|
def __init__(
|
|
23
25
|
self,
|
|
24
26
|
limits: Limits,
|
|
25
|
-
register: Callable[[
|
|
26
|
-
unregister: Callable[[
|
|
27
|
+
register: Callable[[], None],
|
|
28
|
+
unregister: Callable[[], None],
|
|
27
29
|
) -> None:
|
|
28
30
|
self._limits = limits
|
|
29
31
|
self._register = register
|
|
30
32
|
self._unregister = unregister
|
|
31
|
-
self._records: deque[tuple[
|
|
33
|
+
self._records: deque[tuple[T, int]] = deque()
|
|
32
34
|
self._bytes = 0
|
|
33
35
|
self._ready = asyncio.Event()
|
|
34
36
|
self._entered = False
|
|
@@ -37,9 +39,12 @@ class EventSubscription:
|
|
|
37
39
|
self._reading = False
|
|
38
40
|
|
|
39
41
|
async def __aenter__(self) -> Self:
|
|
42
|
+
return self._enter()
|
|
43
|
+
|
|
44
|
+
def _enter(self) -> Self:
|
|
40
45
|
if self._entered or self._closed:
|
|
41
46
|
raise RuntimeError("Event subscriptions are single-use")
|
|
42
|
-
self._register(
|
|
47
|
+
self._register()
|
|
43
48
|
self._entered = True
|
|
44
49
|
return self
|
|
45
50
|
|
|
@@ -53,22 +58,29 @@ class EventSubscription:
|
|
|
53
58
|
|
|
54
59
|
async def aclose(self) -> None:
|
|
55
60
|
"""Unsubscribe and release queued payloads; safe to call repeatedly."""
|
|
61
|
+
self._discard()
|
|
62
|
+
|
|
63
|
+
def _discard(self, *, buffered: bool = False) -> None:
|
|
64
|
+
"""Also used by the sync facade after closing the subscription."""
|
|
56
65
|
self._finish()
|
|
57
66
|
self._records.clear()
|
|
58
67
|
self._bytes = 0
|
|
68
|
+
self._error = None
|
|
59
69
|
|
|
60
70
|
def _finish(self, error: Exception | None = None) -> None:
|
|
61
71
|
if self._closed:
|
|
62
72
|
return
|
|
63
73
|
self._closed = True
|
|
64
74
|
self._error = error
|
|
65
|
-
self._unregister(
|
|
66
|
-
if error
|
|
75
|
+
self._unregister()
|
|
76
|
+
if isinstance(error, PiSubscriptionOverflow):
|
|
77
|
+
# Overflow is loss, not a complete prefix ending at a process failure.
|
|
78
|
+
# Keep it immediate; a healthy terminal queue can still be drained.
|
|
67
79
|
self._records.clear()
|
|
68
80
|
self._bytes = 0
|
|
69
81
|
self._ready.set()
|
|
70
82
|
|
|
71
|
-
def _put(self, event:
|
|
83
|
+
def _put(self, event: T, size: int) -> Exception | None:
|
|
72
84
|
if self._closed:
|
|
73
85
|
return self._error
|
|
74
86
|
if (
|
|
@@ -86,7 +98,7 @@ class EventSubscription:
|
|
|
86
98
|
def __aiter__(self) -> Self:
|
|
87
99
|
return self
|
|
88
100
|
|
|
89
|
-
async def __anext__(self) ->
|
|
101
|
+
async def __anext__(self) -> T:
|
|
90
102
|
if not self._entered:
|
|
91
103
|
raise RuntimeError("Enter the event subscription context before iterating")
|
|
92
104
|
if self._reading:
|
|
@@ -94,15 +106,44 @@ class EventSubscription:
|
|
|
94
106
|
self._reading = True
|
|
95
107
|
try:
|
|
96
108
|
while True:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if self._records:
|
|
100
|
-
event, size = self._records.popleft()
|
|
101
|
-
self._bytes -= size
|
|
109
|
+
event = self._next_nowait()
|
|
110
|
+
if event is not None:
|
|
102
111
|
return event
|
|
103
|
-
if self._closed:
|
|
104
|
-
raise StopAsyncIteration
|
|
105
112
|
self._ready.clear()
|
|
106
113
|
await self._ready.wait()
|
|
107
114
|
finally:
|
|
108
115
|
self._reading = False
|
|
116
|
+
|
|
117
|
+
async def _next_batch(self) -> list[T]:
|
|
118
|
+
records = [await self.__anext__()]
|
|
119
|
+
# Transfer only records already buffered; never wait to fill a batch.
|
|
120
|
+
# This retains at most one queue's byte budget in the blocking facade.
|
|
121
|
+
while self._records and len(records) < 64:
|
|
122
|
+
record = self._next_nowait()
|
|
123
|
+
assert record is not None
|
|
124
|
+
records.append(record)
|
|
125
|
+
return records
|
|
126
|
+
|
|
127
|
+
def _next_nowait(self) -> T | None:
|
|
128
|
+
"""Read on the owning loop, or after that loop has completely stopped."""
|
|
129
|
+
if self._records:
|
|
130
|
+
event, size = self._records.popleft()
|
|
131
|
+
self._bytes -= size
|
|
132
|
+
return event
|
|
133
|
+
if self._error is not None:
|
|
134
|
+
raise self._error
|
|
135
|
+
if self._closed:
|
|
136
|
+
raise StopAsyncIteration
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class EventSubscription(_Subscription[Event]):
|
|
141
|
+
"""Bounded future events; enter before startup to include startup events."""
|
|
142
|
+
|
|
143
|
+
def __init__(
|
|
144
|
+
self,
|
|
145
|
+
limits: Limits,
|
|
146
|
+
register: Callable[[EventSubscription], None],
|
|
147
|
+
unregister: Callable[[EventSubscription], None],
|
|
148
|
+
) -> None:
|
|
149
|
+
super().__init__(limits, lambda: register(self), lambda: unregister(self))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Opt-in process output delivery; storage and forwarding belong to callers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from ._events import _Subscription
|
|
11
|
+
from .errors import PiSubscriptionOverflow
|
|
12
|
+
from .types import Limits
|
|
13
|
+
|
|
14
|
+
OutputSource = Literal["stderr", "stdout", "rpc"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ProcessOutput:
|
|
19
|
+
"""Original bytes or a parsed object from the RPC child, excluding the version probe."""
|
|
20
|
+
|
|
21
|
+
source: OutputSource
|
|
22
|
+
time_ns: int
|
|
23
|
+
data: bytes | dict[str, Any] = field(repr=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ObservationStatus:
|
|
28
|
+
"""Delivery coverage, independent of RPC success and consumer storage."""
|
|
29
|
+
|
|
30
|
+
started_at_ns: int | None = None
|
|
31
|
+
ended_at_ns: int | None = None
|
|
32
|
+
from_start: bool = False
|
|
33
|
+
complete: bool = False
|
|
34
|
+
lost: bool = False
|
|
35
|
+
stderr_eof: bool = False
|
|
36
|
+
stdout_eof: bool = False
|
|
37
|
+
rpc_complete: bool = True
|
|
38
|
+
error: Exception | None = field(default=None, repr=False)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProcessObservation(_Subscription[ProcessOutput]):
|
|
42
|
+
"""Bounded output iterator. Check status after draining to detect incomplete output."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
limits: Limits,
|
|
47
|
+
register: Callable[[ProcessObservation], None],
|
|
48
|
+
unregister: Callable[[ProcessObservation], None],
|
|
49
|
+
sources: frozenset[OutputSource],
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(limits, lambda: register(self), lambda: unregister(self))
|
|
52
|
+
self._status = ObservationStatus()
|
|
53
|
+
self._sources = sources
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def status(self) -> ObservationStatus:
|
|
57
|
+
return self._status
|
|
58
|
+
|
|
59
|
+
def _finish(self, error: Exception | None = None) -> None:
|
|
60
|
+
if not self._closed and self._status.ended_at_ns is None:
|
|
61
|
+
self._status = ObservationStatus(
|
|
62
|
+
started_at_ns=self._status.started_at_ns,
|
|
63
|
+
ended_at_ns=time.time_ns(),
|
|
64
|
+
from_start=self._status.from_start,
|
|
65
|
+
lost=self._status.lost or isinstance(error, PiSubscriptionOverflow),
|
|
66
|
+
error=error,
|
|
67
|
+
)
|
|
68
|
+
super()._finish(error)
|
|
69
|
+
|
|
70
|
+
def _discard(self, *, buffered: bool = False) -> None:
|
|
71
|
+
if self._records or buffered:
|
|
72
|
+
self._status = replace(self._status, complete=False, lost=True)
|
|
73
|
+
super()._discard()
|