pi-agent-python-sdk 0.2.0__tar.gz → 0.2.1__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 (19) hide show
  1. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/CHANGELOG.md +26 -0
  2. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/PKG-INFO +94 -40
  3. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/README.md +91 -37
  4. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/pyproject.toml +3 -3
  5. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/_events.py +4 -2
  6. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/_launch.py +1 -1
  7. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/_observation.py +28 -6
  8. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/_runs.py +4 -4
  9. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/_transport.py +11 -2
  10. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/client.py +27 -8
  11. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/sync.py +10 -0
  12. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/types.py +41 -3
  13. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/.gitignore +0 -0
  14. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/LICENSE +0 -0
  15. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/__init__.py +0 -0
  16. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/_collections.py +0 -0
  17. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/_usage.py +0 -0
  18. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/errors.py +0 -0
  19. {pi_agent_python_sdk-0.2.0 → pi_agent_python_sdk-0.2.1}/src/pi_agent/py.typed +0 -0
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.1
6
+
7
+ ### Added
8
+
9
+ - Stop process observations without closing Pi, then drain buffered records;
10
+ report scoped stop, discard, overflow, and process termination separately.
11
+
12
+ ### Fixed
13
+
14
+ - Drain the ready queue in each blocking iterator transfer to avoid finite-burst
15
+ overflow caused by repeated partial transfers. Queue limits remain unchanged;
16
+ the additional batch is bounded by the same count and byte budgets.
17
+ - Allow sequential async stream iterator reuse while retaining concurrent-reader
18
+ and result-drain protections.
19
+ - Keep a healthy client usable after locally rejected, unsent input, without
20
+ relaxing ownership or cleanup for commands that may have reached Pi.
21
+
22
+ ### Changed
23
+
24
+ - Update the tested Pi runtime to 0.86.0, retaining 0.85.1 as the minimum.
25
+ - Clarify Python interface choice, async event consumption, and structured-output
26
+ boundaries; demonstrate thinking, tool events, and unknown metadata.
27
+ - Describe system transcript messages, session usage entries, compaction system
28
+ snapshots, and model prompt-cache metadata in the wire annotations. Preserve
29
+ legacy optional fields and raw metadata; Pi still owns transcript and cache behavior.
30
+
5
31
  ## 0.2.0
6
32
 
7
33
  ### Breaking changes
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pi-agent-python-sdk
3
- Version: 0.2.0
3
+ Version: 0.2.1
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.2.0/docs
6
+ Project-URL: Documentation, https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.2.1/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.2.0/CHANGELOG.md
8
+ Project-URL: Changelog, https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/CHANGELOG.md
9
9
  Author: cheenulabs
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
@@ -23,30 +23,60 @@ Description-Content-Type: text/markdown
23
23
 
24
24
  # Pi Agent Python SDK
25
25
 
26
- [![PyPI](https://img.shields.io/pypi/v/pi-agent-python-sdk?style=flat-square)][pypi]
26
+ [![PyPI](https://badge.fury.io/py/pi-agent-python-sdk.svg)][pypi]
27
27
  [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square)][metadata]
28
28
  [![MIT license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)][license]
29
29
 
30
- **Run Pi from Python. Stream responses, continue conversations, and control sessions.**
30
+ **Use your installed Pi coding agent as a Python API. Stream responses,
31
+ continue conversations, and control sessions from Python.**
31
32
 
32
33
  A community Python SDK for [Pi coding agent](https://github.com/earendil-works/pi).
33
- Use synchronous or asynchronous clients with typed access to Pi's RPC commands
34
- and no third-party Python runtime dependencies. Pi runs as a subprocess and keeps
35
- its usual models, authentication, tools, extensions, skills, and configuration.
34
+ It starts Pi as a subprocess and exposes the same agent runtime through a typed
35
+ Python interface for RPC commands, streaming, and session control.
36
36
 
37
37
  > Requires Python **3.11+** and a separate Pi installation. The tested protocol
38
- > baseline is **Pi 0.85.1**; see [compatibility][compatibility] for version and
38
+ > baseline is **Pi 0.86.0**; see [compatibility][compatibility] for version and
39
39
  > platform scope.
40
40
 
41
41
  [Quick start](#quick-start) · [Streaming](#streaming) · [Async](#async-usage) ·
42
- [Configuration](#configuration) · [RPC](#rpc-access) · [Documentation](#documentation)
42
+ [Configuration](#configuration) · [Thinking](#models-and-thinking) ·
43
+ [RPC](#rpc-access) · [Documentation](#documentation)
44
+
45
+ ## Why use this?
46
+
47
+ If you already use Pi from the terminal, this SDK lets you use the same agent
48
+ from Python code. Instead of managing a separate implementation, you can start Pi
49
+ in RPC mode and call it as a programmatic API from scripts, apps, services, or
50
+ background jobs.
51
+
52
+ This is useful when you want to:
53
+
54
+ - Add Pi-powered coding or reasoning to a Python application.
55
+ - Automate project analysis, refactors, or repository workflows.
56
+ - Build a service or CLI around Pi without reimplementing the agent runtime.
57
+ - Stream text, thinking, tool, and session events in Python.
58
+ - Reuse your existing Pi configuration, models, tools, and extensions.
59
+
60
+ The SDK keeps Pi's usual setup and model selection, while exposing a typed Python
61
+ interface for prompts, follow-ups, model control, and RPC access. It launches Pi
62
+ as a subprocess and communicates over its JSONL protocol; it does not attach to
63
+ an already-running interactive Pi terminal session.
64
+
65
+ ## What you can do
66
+
67
+ - **Run and stream:** get a final answer or consume text, thinking, and tool events.
68
+ - **Keep a conversation:** send follow-up prompts, resume sessions, fork, or clone.
69
+ - **Control Pi:** select models, adjust thinking, steer work, compact context, and
70
+ call all 33 RPC commands in the pinned baseline.
71
+ - **Integrate with your application:** use sync or async clients, typed results,
72
+ raw event dictionaries, and extension UI callbacks.
43
73
 
44
74
  ## Quick start
45
75
 
46
76
  Install Pi with Node.js **22.19.0 or newer**:
47
77
 
48
78
  ```sh
49
- npm install -g @earendil-works/pi-coding-agent@0.85.1
79
+ npm install -g @earendil-works/pi-coding-agent@0.86.0
50
80
  pi --version
51
81
  ```
52
82
 
@@ -56,7 +86,7 @@ The SDK uses that configuration when it starts Pi.
56
86
  Install the Python package from [PyPI][pypi]:
57
87
 
58
88
  ```sh
59
- python -m pip install pi-agent-python-sdk==0.2.0
89
+ python -m pip install pi-agent-python-sdk==0.2.1
60
90
  ```
61
91
 
62
92
  The distribution is named `pi-agent-python-sdk`; import it as
@@ -87,15 +117,6 @@ with PiClient() as pi:
87
117
  Pi keeps the conversation context. Each result contains only the messages and
88
118
  answer from that call; a call with no assistant output has empty text.
89
119
 
90
- ## What you can do
91
-
92
- - **Run and stream:** get a final answer or consume text, thinking, and tool events.
93
- - **Keep a conversation:** send follow-up prompts, resume sessions, fork, or clone.
94
- - **Control Pi:** select models, adjust thinking, steer work, compact context, and
95
- call all 33 RPC commands in the pinned baseline.
96
- - **Integrate with your application:** use sync or async clients, typed results,
97
- raw event dictionaries, and extension UI callbacks.
98
-
99
120
  ## Streaming
100
121
 
101
122
  ```python
@@ -159,6 +180,38 @@ pass `extra_args=["--extension", "/absolute/path/to/your-extension.ts"]` to eith
159
180
  client. See [using your own extensions][extensions]
160
181
  for details and [extension UI][ui-example] for an interactive example.
161
182
 
183
+ ## Models and thinking
184
+
185
+ The SDK uses Pi's configured model unless you pass `provider=` and `model=`.
186
+ Pi determines which thinking levels that model supports; query them before
187
+ choosing a level:
188
+
189
+ ```python
190
+ from pi_agent import PiClient
191
+
192
+ with PiClient() as pi:
193
+ levels = pi.get_available_thinking_levels()
194
+ print("Supported thinking levels:", levels)
195
+ if "high" in levels:
196
+ pi.set_thinking_level("high")
197
+ print("Effective thinking level:", pi.get_state()["thinkingLevel"])
198
+
199
+ result = pi.run("Analyse this project's architecture without changing files.")
200
+ print(result.text)
201
+ ```
202
+
203
+ Set the model and thinking level before starting `run()` or `stream()`. Pi can
204
+ adjust unsupported level requests, so read `get_state()["thinkingLevel"]` for the
205
+ effective value. The setting applies to subsequent prompts in the current
206
+ session without changing global defaults. With `AsyncPiClient`, await the same
207
+ methods. See the [model and thinking commands][thinking-reference] for cycling
208
+ levels and other controls.
209
+
210
+ The thinking level controls Pi's reasoning setting. Thinking **output** is
211
+ separate: when Pi emits it, deltas are available in `event.raw` during streaming
212
+ and finalized thinking blocks in `result.messages`. `result.text` contains the
213
+ final assistant text, not thinking.
214
+
162
215
  ## RPC access
163
216
 
164
217
  The SDK speaks Pi's existing JSONL protocol over stdin/stdout:
@@ -215,7 +268,7 @@ the [command reference][commands] for every method.
215
268
  | [RPC structure][rpc] | How the Python client maps to Pi's protocol |
216
269
  | [Compatibility][compatibility] | Runtime versions and platform validation |
217
270
  | [Examples][examples] | Runnable sync, async, streaming, sessions, steering, and UI examples |
218
- | [Release notes][release] | Changes included in 0.2.0 |
271
+ | [Release notes][release] | Changes included in 0.2.1 |
219
272
 
220
273
  Examples use your configured Pi and may make provider calls. Development checks
221
274
  use an isolated local test provider; see [CONTRIBUTING.md][contributing] for
@@ -229,21 +282,22 @@ Maintainers: [protocol discovery][discovery] ·
229
282
  [MIT][license]
230
283
 
231
284
  [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
285
+ [release]: https://github.com/cheenulabs/pi-agent-python-sdk/releases/tag/v0.2.1
286
+ [metadata]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/pyproject.toml
287
+ [license]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/LICENSE
288
+ [compatibility]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/compatibility.md
289
+ [errors]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/errors.md
290
+ [stream-example]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/examples/stream.py
291
+ [constructor-options]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/api.md#constructor-options
292
+ [extensions]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/usage.md#using-your-own-extensions
293
+ [ui-example]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/examples/ui.py
294
+ [rpc]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/rpc.md
295
+ [commands]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/api.md#all-33-rpc-commands
296
+ [thinking-reference]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/api.md#models-state-and-compaction
297
+ [usage]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/usage.md
298
+ [api]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/api.md
299
+ [examples]: https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.2.1/examples
300
+ [contributing]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/CONTRIBUTING.md
301
+ [discovery]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/discovery.md
302
+ [maintenance]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/maintenance.md
303
+ [releasing]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/releasing.md
@@ -1,29 +1,59 @@
1
1
  # Pi Agent Python SDK
2
2
 
3
- [![PyPI](https://img.shields.io/pypi/v/pi-agent-python-sdk?style=flat-square)][pypi]
3
+ [![PyPI](https://badge.fury.io/py/pi-agent-python-sdk.svg)][pypi]
4
4
  [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square)][metadata]
5
5
  [![MIT license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)][license]
6
6
 
7
- **Run Pi from Python. Stream responses, continue conversations, and control sessions.**
7
+ **Use your installed Pi coding agent as a Python API. Stream responses,
8
+ continue conversations, and control sessions from Python.**
8
9
 
9
10
  A community Python SDK for [Pi coding agent](https://github.com/earendil-works/pi).
10
- Use synchronous or asynchronous clients with typed access to Pi's RPC commands
11
- and no third-party Python runtime dependencies. Pi runs as a subprocess and keeps
12
- its usual models, authentication, tools, extensions, skills, and configuration.
11
+ It starts Pi as a subprocess and exposes the same agent runtime through a typed
12
+ Python interface for RPC commands, streaming, and session control.
13
13
 
14
14
  > Requires Python **3.11+** and a separate Pi installation. The tested protocol
15
- > baseline is **Pi 0.85.1**; see [compatibility][compatibility] for version and
15
+ > baseline is **Pi 0.86.0**; see [compatibility][compatibility] for version and
16
16
  > platform scope.
17
17
 
18
18
  [Quick start](#quick-start) · [Streaming](#streaming) · [Async](#async-usage) ·
19
- [Configuration](#configuration) · [RPC](#rpc-access) · [Documentation](#documentation)
19
+ [Configuration](#configuration) · [Thinking](#models-and-thinking) ·
20
+ [RPC](#rpc-access) · [Documentation](#documentation)
21
+
22
+ ## Why use this?
23
+
24
+ If you already use Pi from the terminal, this SDK lets you use the same agent
25
+ from Python code. Instead of managing a separate implementation, you can start Pi
26
+ in RPC mode and call it as a programmatic API from scripts, apps, services, or
27
+ background jobs.
28
+
29
+ This is useful when you want to:
30
+
31
+ - Add Pi-powered coding or reasoning to a Python application.
32
+ - Automate project analysis, refactors, or repository workflows.
33
+ - Build a service or CLI around Pi without reimplementing the agent runtime.
34
+ - Stream text, thinking, tool, and session events in Python.
35
+ - Reuse your existing Pi configuration, models, tools, and extensions.
36
+
37
+ The SDK keeps Pi's usual setup and model selection, while exposing a typed Python
38
+ interface for prompts, follow-ups, model control, and RPC access. It launches Pi
39
+ as a subprocess and communicates over its JSONL protocol; it does not attach to
40
+ an already-running interactive Pi terminal session.
41
+
42
+ ## What you can do
43
+
44
+ - **Run and stream:** get a final answer or consume text, thinking, and tool events.
45
+ - **Keep a conversation:** send follow-up prompts, resume sessions, fork, or clone.
46
+ - **Control Pi:** select models, adjust thinking, steer work, compact context, and
47
+ call all 33 RPC commands in the pinned baseline.
48
+ - **Integrate with your application:** use sync or async clients, typed results,
49
+ raw event dictionaries, and extension UI callbacks.
20
50
 
21
51
  ## Quick start
22
52
 
23
53
  Install Pi with Node.js **22.19.0 or newer**:
24
54
 
25
55
  ```sh
26
- npm install -g @earendil-works/pi-coding-agent@0.85.1
56
+ npm install -g @earendil-works/pi-coding-agent@0.86.0
27
57
  pi --version
28
58
  ```
29
59
 
@@ -33,7 +63,7 @@ The SDK uses that configuration when it starts Pi.
33
63
  Install the Python package from [PyPI][pypi]:
34
64
 
35
65
  ```sh
36
- python -m pip install pi-agent-python-sdk==0.2.0
66
+ python -m pip install pi-agent-python-sdk==0.2.1
37
67
  ```
38
68
 
39
69
  The distribution is named `pi-agent-python-sdk`; import it as
@@ -64,15 +94,6 @@ with PiClient() as pi:
64
94
  Pi keeps the conversation context. Each result contains only the messages and
65
95
  answer from that call; a call with no assistant output has empty text.
66
96
 
67
- ## What you can do
68
-
69
- - **Run and stream:** get a final answer or consume text, thinking, and tool events.
70
- - **Keep a conversation:** send follow-up prompts, resume sessions, fork, or clone.
71
- - **Control Pi:** select models, adjust thinking, steer work, compact context, and
72
- call all 33 RPC commands in the pinned baseline.
73
- - **Integrate with your application:** use sync or async clients, typed results,
74
- raw event dictionaries, and extension UI callbacks.
75
-
76
97
  ## Streaming
77
98
 
78
99
  ```python
@@ -136,6 +157,38 @@ pass `extra_args=["--extension", "/absolute/path/to/your-extension.ts"]` to eith
136
157
  client. See [using your own extensions][extensions]
137
158
  for details and [extension UI][ui-example] for an interactive example.
138
159
 
160
+ ## Models and thinking
161
+
162
+ The SDK uses Pi's configured model unless you pass `provider=` and `model=`.
163
+ Pi determines which thinking levels that model supports; query them before
164
+ choosing a level:
165
+
166
+ ```python
167
+ from pi_agent import PiClient
168
+
169
+ with PiClient() as pi:
170
+ levels = pi.get_available_thinking_levels()
171
+ print("Supported thinking levels:", levels)
172
+ if "high" in levels:
173
+ pi.set_thinking_level("high")
174
+ print("Effective thinking level:", pi.get_state()["thinkingLevel"])
175
+
176
+ result = pi.run("Analyse this project's architecture without changing files.")
177
+ print(result.text)
178
+ ```
179
+
180
+ Set the model and thinking level before starting `run()` or `stream()`. Pi can
181
+ adjust unsupported level requests, so read `get_state()["thinkingLevel"]` for the
182
+ effective value. The setting applies to subsequent prompts in the current
183
+ session without changing global defaults. With `AsyncPiClient`, await the same
184
+ methods. See the [model and thinking commands][thinking-reference] for cycling
185
+ levels and other controls.
186
+
187
+ The thinking level controls Pi's reasoning setting. Thinking **output** is
188
+ separate: when Pi emits it, deltas are available in `event.raw` during streaming
189
+ and finalized thinking blocks in `result.messages`. `result.text` contains the
190
+ final assistant text, not thinking.
191
+
139
192
  ## RPC access
140
193
 
141
194
  The SDK speaks Pi's existing JSONL protocol over stdin/stdout:
@@ -192,7 +245,7 @@ the [command reference][commands] for every method.
192
245
  | [RPC structure][rpc] | How the Python client maps to Pi's protocol |
193
246
  | [Compatibility][compatibility] | Runtime versions and platform validation |
194
247
  | [Examples][examples] | Runnable sync, async, streaming, sessions, steering, and UI examples |
195
- | [Release notes][release] | Changes included in 0.2.0 |
248
+ | [Release notes][release] | Changes included in 0.2.1 |
196
249
 
197
250
  Examples use your configured Pi and may make provider calls. Development checks
198
251
  use an isolated local test provider; see [CONTRIBUTING.md][contributing] for
@@ -206,21 +259,22 @@ Maintainers: [protocol discovery][discovery] ·
206
259
  [MIT][license]
207
260
 
208
261
  [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
262
+ [release]: https://github.com/cheenulabs/pi-agent-python-sdk/releases/tag/v0.2.1
263
+ [metadata]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/pyproject.toml
264
+ [license]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/LICENSE
265
+ [compatibility]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/compatibility.md
266
+ [errors]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/errors.md
267
+ [stream-example]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/examples/stream.py
268
+ [constructor-options]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/api.md#constructor-options
269
+ [extensions]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/usage.md#using-your-own-extensions
270
+ [ui-example]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/examples/ui.py
271
+ [rpc]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/rpc.md
272
+ [commands]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/api.md#all-33-rpc-commands
273
+ [thinking-reference]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/api.md#models-state-and-compaction
274
+ [usage]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/usage.md
275
+ [api]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/api.md
276
+ [examples]: https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.2.1/examples
277
+ [contributing]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/CONTRIBUTING.md
278
+ [discovery]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/discovery.md
279
+ [maintenance]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/docs/maintenance.md
280
+ [releasing]: https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/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.2.0"
7
+ version = "0.2.1"
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.2.0/docs"
29
+ Documentation = "https://github.com/cheenulabs/pi-agent-python-sdk/tree/v0.2.1/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.2.0/CHANGELOG.md"
31
+ Changelog = "https://github.com/cheenulabs/pi-agent-python-sdk/blob/v0.2.1/CHANGELOG.md"
32
32
 
33
33
  [dependency-groups]
34
34
  dev = [
@@ -117,8 +117,10 @@ class _Subscription(Generic[T]):
117
117
  async def _next_batch(self) -> list[T]:
118
118
  records = [await self.__anext__()]
119
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:
120
+ # Drain the ready queue in one crossing so bridge latency does not leave
121
+ # a growing backlog. No await below: count and bytes stay within the
122
+ # existing queue budgets, including the first record just removed.
123
+ while self._records:
122
124
  record = self._next_nowait()
123
125
  assert record is not None
124
126
  records.append(record)
@@ -13,7 +13,7 @@ from ._transport import _close_process_pipes, _wait_for_exit
13
13
  from .errors import PiProcessError, PiVersionError
14
14
 
15
15
  MINIMUM_PI_VERSION = "0.85.1"
16
- TESTED_PI_VERSION = "0.85.1"
16
+ TESTED_PI_VERSION = "0.86.0"
17
17
 
18
18
  # The client owns these options. In particular --print consumes an initial prompt.
19
19
  _RESERVED = {
@@ -36,6 +36,7 @@ class ObservationStatus:
36
36
  stdout_eof: bool = False
37
37
  rpc_complete: bool = True
38
38
  error: Exception | None = field(default=None, repr=False)
39
+ end_reason: Literal["stopped", "closed", "overflow", "process_end"] | None = None
39
40
 
40
41
 
41
42
  class ProcessObservation(_Subscription[ProcessOutput]):
@@ -56,18 +57,39 @@ class ProcessObservation(_Subscription[ProcessOutput]):
56
57
  def status(self) -> ObservationStatus:
57
58
  return self._status
58
59
 
60
+ async def stop(self) -> None:
61
+ """Unsubscribe without discarding queued output or closing the client.
62
+
63
+ Drain the iterator afterwards, then inspect status. Repeated stops retain
64
+ the first termination status, including overflow or process failure.
65
+
66
+ Raises:
67
+ RuntimeError: If the observation context has not been entered.
68
+ """
69
+ if not self._entered:
70
+ raise RuntimeError("Enter the observation context before stopping")
71
+ if not self._closed:
72
+ self._status = replace(self._status, end_reason="stopped")
73
+ self._finish()
74
+
59
75
  def _finish(self, error: Exception | None = None) -> None:
60
76
  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,
77
+ overflow = isinstance(error, PiSubscriptionOverflow)
78
+ self._status = replace(
79
+ self._status,
63
80
  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,
81
+ lost=self._status.lost or overflow,
82
+ error=error or self._status.error,
83
+ end_reason="overflow" if overflow else self._status.end_reason or "closed",
67
84
  )
68
85
  super()._finish(error)
69
86
 
70
87
  def _discard(self, *, buffered: bool = False) -> None:
71
88
  if self._records or buffered:
72
- self._status = replace(self._status, complete=False, lost=True)
89
+ self._status = replace(
90
+ self._status,
91
+ complete=False,
92
+ lost=True,
93
+ end_reason="overflow" if self._status.end_reason == "overflow" else "closed",
94
+ )
73
95
  super()._discard()
@@ -172,6 +172,10 @@ class RunStream:
172
172
  if error is not None:
173
173
  self._fail(error)
174
174
 
175
+ def _mark_submitted(self) -> None:
176
+ self._submitted = True
177
+ self._begin = time.monotonic()
178
+
175
179
  async def _drive(self) -> RunResult:
176
180
  assert self._ready is not None
177
181
  failure: BaseException | None = None
@@ -183,8 +187,6 @@ class RunStream:
183
187
  fields: dict[str, Any] = {"message": self._message}
184
188
  if self._images is not None:
185
189
  fields["images"] = self._images
186
- self._submitted = True
187
- self._begin = time.monotonic()
188
190
  await self._client._request(
189
191
  "prompt", fields, timeout=self._command_timeout, owner=self
190
192
  )
@@ -300,8 +302,6 @@ class RunStream:
300
302
  def __aiter__(self) -> Self:
301
303
  if self._draining:
302
304
  raise PiBusyError("Cannot iterate while result() drains the stream")
303
- if self._iterating:
304
- raise PiBusyError("A stream permits one event iterator")
305
305
  self._iterating = True
306
306
  return self
307
307
 
@@ -124,6 +124,7 @@ class Transport:
124
124
  fields: dict[str, Any] | None = None,
125
125
  *,
126
126
  timeout: float | None = 30, # noqa: ASYNC109 - per-response API, distinct from write bound
127
+ before_write: Callable[[], None] | None = None,
127
128
  ) -> dict[str, Any]:
128
129
  if not isinstance(command, str) or not command:
129
130
  raise ValueError("command must be a nonempty string")
@@ -136,7 +137,9 @@ class Transport:
136
137
  # A fast child can respond while drain() is still suspended.
137
138
  self._pending[request_id] = _Pending(command, future)
138
139
  try:
139
- await self._write({"type": command, "id": request_id, **(fields or {})})
140
+ await self._write(
141
+ {"type": command, "id": request_id, **(fields or {})}, before_write=before_write
142
+ )
140
143
  try:
141
144
  # Keep cancellation on this task: wait_for can lose cancellation
142
145
  # when its separate waiter completes at the same time on Python 3.11.
@@ -169,7 +172,9 @@ class Transport:
169
172
  if not self.running:
170
173
  raise PiProcessError("Pi subprocess is not running")
171
174
 
172
- async def _write(self, record: dict[str, Any]) -> None:
175
+ async def _write(
176
+ self, record: dict[str, Any], *, before_write: Callable[[], None] | None = None
177
+ ) -> None:
173
178
  # Escape lone UTF-16 surrogates, as JSON.stringify does, while keeping
174
179
  # ordinary Unicode as UTF-8. Never emit invalid UTF-8 or replace values.
175
180
  data = json.dumps(
@@ -177,6 +182,10 @@ class Transport:
177
182
  ).encode("utf-8", errors="backslashreplace")
178
183
  if len(data) > self._limits.max_record_bytes:
179
184
  raise ValueError("Outbound JSON record exceeds max_record_bytes")
185
+ # Local validation cannot submit work. Notify ownership before the first
186
+ # suspension so another run cannot race a validated, queued write.
187
+ if before_write is not None:
188
+ before_write()
180
189
  wrote = False
181
190
  try:
182
191
  # Bound lock acquisition too: a wedged pipe must not queue writers forever.
@@ -424,9 +424,9 @@ class AsyncPiClient:
424
424
  """Observe selected process output; enter before start for lifetime coverage.
425
425
 
426
426
  Raises ValueError if no source is selected. Slow consumers raise
427
- PiSubscriptionOverflow. Close the client and drain this iterator before
428
- checking status.complete and status.error; terminal process failures are
429
- reported through status.error after output drains.
427
+ PiSubscriptionOverflow. Stop the observation or close the client, then
428
+ drain this iterator before checking final status. Known terminal process
429
+ failures remain in status.error even when stopped before cleanup ends.
430
430
  """
431
431
  sources: set[OutputSource] = set()
432
432
  if stderr:
@@ -486,6 +486,7 @@ class AsyncPiClient:
486
486
  rpc_complete=rpc_complete,
487
487
  complete=complete,
488
488
  error=self._close_error if self._closed else self._terminal_error or error,
489
+ end_reason="process_end",
489
490
  )
490
491
  observer._finish()
491
492
 
@@ -551,6 +552,13 @@ class AsyncPiClient:
551
552
  error = self._terminal_error
552
553
  self._session = SessionInfo()
553
554
  self._listeners.clear()
555
+ # Observers keep receiving shutdown output, but stop() must retain a
556
+ # failure already known before pipe drainage finishes. Normal close is
557
+ # not a process error for observation consumers.
558
+ for observer in self._observations:
559
+ observer._status = replace(
560
+ observer.status, error=self._close_error if self._closed else error
561
+ )
554
562
  for task in tuple(self._ui_tasks):
555
563
  if task is not asyncio.current_task() and task is not self._close_initiator:
556
564
  task.cancel()
@@ -692,12 +700,23 @@ class AsyncPiClient:
692
700
  )
693
701
  if deadline is not None:
694
702
  _validate_timeout(deadline)
695
- if self._owner is None and command in {"prompt", "steer", "follow_up"}:
696
- # Set before awaiting: input handlers can start work long after acknowledgement.
697
- # Even rejection/cancellation cannot prove arbitrary extension preflight is idle.
698
- self._unowned_submission = True
703
+
704
+ def mark_submission() -> None:
705
+ if owner is not None:
706
+ owner._mark_submitted()
707
+ elif self._owner is None:
708
+ # Even rejection cannot prove extension preflight is idle.
709
+ self._unowned_submission = True
710
+
699
711
  try:
700
- response = await self._transport.request(command, fields, timeout=deadline)
712
+ response = await self._transport.request(
713
+ command,
714
+ fields,
715
+ timeout=deadline,
716
+ before_write=(
717
+ mark_submission if command in {"prompt", "steer", "follow_up"} else None
718
+ ),
719
+ )
701
720
  if self._ui_error is not None:
702
721
  error, self._ui_error = self._ui_error, None
703
722
  raise error
@@ -756,6 +756,16 @@ class SyncProcessObservation(_SyncSubscription[ProcessOutput]):
756
756
  subscription = client._client.observe(stderr=stderr, stdout=stdout, rpc=rpc)
757
757
  super().__init__(client, lambda: subscription)
758
758
 
759
+ def stop(self) -> None:
760
+ """Unsubscribe and preserve buffered output for iteration; keep Pi running.
761
+
762
+ Raises:
763
+ RuntimeError: If the observation context has not been entered.
764
+ """
765
+ if self._subscription is None:
766
+ raise RuntimeError("Enter the observation context before stopping")
767
+ self._client._close_context(cast(ProcessObservation, self._subscription).stop)
768
+
759
769
  @property
760
770
  def status(self) -> ObservationStatus:
761
771
  if self._subscription is None:
@@ -1,4 +1,4 @@
1
- """Pi 0.85.1 wire annotations and small Python conveniences.
1
+ """Pi 0.86.0 wire annotations and small Python conveniences.
2
2
 
3
3
  Wire fields retain Pi's spelling and remain ordinary dictionaries. These
4
4
  annotations describe known shapes, not a recursive runtime validator: extensions
@@ -97,6 +97,26 @@ class AssistantMessageDiagnostic(TypedDict):
97
97
  details: NotRequired[JSONObject]
98
98
 
99
99
 
100
+ class ToolDefinition(TypedDict):
101
+ name: str
102
+ description: str
103
+ parameters: JSONObject
104
+ constrainedSampling: NotRequired[Literal[False] | JSONObject]
105
+
106
+
107
+ class ToolReference(TypedDict):
108
+ name: str
109
+
110
+
111
+ class SystemMessage(TypedDict):
112
+ role: Literal["system"]
113
+ content: str | list[TextContent]
114
+ sections: NotRequired[dict[str, str | None]]
115
+ toolsAdded: NotRequired[list[ToolDefinition]]
116
+ toolsRemoved: NotRequired[list[ToolReference]]
117
+ timestamp: float
118
+
119
+
100
120
  class UserMessage(TypedDict):
101
121
  role: Literal["user"]
102
122
  content: str | list[TextContent | ImageContent]
@@ -129,7 +149,7 @@ class ToolResultMessage(TypedDict):
129
149
  content: list[TextContent | ImageContent]
130
150
  details: NotRequired[JSONValue]
131
151
  usage: NotRequired[Usage]
132
- addedToolNames: NotRequired[list[str]]
152
+ addedToolNames: NotRequired[list[str]] # Legacy Pi 0.85.1 field.
133
153
  isError: bool
134
154
  timestamp: float
135
155
 
@@ -170,7 +190,8 @@ class CompactionSummaryMessage(TypedDict):
170
190
 
171
191
 
172
192
  AgentMessage: TypeAlias = (
173
- UserMessage
193
+ SystemMessage
194
+ | UserMessage
174
195
  | AssistantMessage
175
196
  | ToolResultMessage
176
197
  | BashExecutionMessage
@@ -195,6 +216,11 @@ class ModelCost(ModelCostRates):
195
216
  tiers: NotRequired[list[ModelCostTier]]
196
217
 
197
218
 
219
+ class ModelPromptCache(TypedDict, total=False):
220
+ short: float
221
+ long: float
222
+
223
+
198
224
  class Model(TypedDict):
199
225
  id: str
200
226
  name: str
@@ -210,6 +236,7 @@ class Model(TypedDict):
210
236
  samplingParams: NotRequired[JSONObject]
211
237
  headers: NotRequired[dict[str, str]]
212
238
  compat: NotRequired[JSONObject]
239
+ promptCache: NotRequired[ModelPromptCache]
213
240
 
214
241
 
215
242
  class SessionState(TypedDict):
@@ -308,6 +335,15 @@ class ModelChangeEntry(_EntryBase):
308
335
  modelId: str
309
336
 
310
337
 
338
+ class UsageEntry(_EntryBase):
339
+ type: Literal["usage"]
340
+ kind: str
341
+ provider: str
342
+ model: str
343
+ usage: Usage
344
+ note: NotRequired[str]
345
+
346
+
311
347
  class CompactionEntry(_EntryBase):
312
348
  type: Literal["compaction"]
313
349
  summary: str
@@ -316,6 +352,7 @@ class CompactionEntry(_EntryBase):
316
352
  details: NotRequired[JSONValue]
317
353
  usage: NotRequired[Usage]
318
354
  fromHook: NotRequired[bool]
355
+ systemMessage: NotRequired[SystemMessage]
319
356
 
320
357
 
321
358
  class BranchSummaryEntry(_EntryBase):
@@ -356,6 +393,7 @@ SessionEntry: TypeAlias = (
356
393
  MessageEntry
357
394
  | ThinkingLevelChangeEntry
358
395
  | ModelChangeEntry
396
+ | UsageEntry
359
397
  | CompactionEntry
360
398
  | BranchSummaryEntry
361
399
  | CustomEntry