pi-python-core 0.10.2__py3-none-any.whl → 0.11.0__py3-none-any.whl
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_python/_version.py +1 -1
- pi_python/agent.py +35 -0
- pi_python/features.py +1 -0
- pi_python/loop.py +8 -0
- pi_python/provider.py +18 -0
- pi_python/providers/openai.py +23 -11
- {pi_python_core-0.10.2.dist-info → pi_python_core-0.11.0.dist-info}/METADATA +2 -2
- {pi_python_core-0.10.2.dist-info → pi_python_core-0.11.0.dist-info}/RECORD +11 -11
- {pi_python_core-0.10.2.dist-info → pi_python_core-0.11.0.dist-info}/WHEEL +0 -0
- {pi_python_core-0.10.2.dist-info → pi_python_core-0.11.0.dist-info}/licenses/LICENSE +0 -0
- {pi_python_core-0.10.2.dist-info → pi_python_core-0.11.0.dist-info}/licenses/NOTICE +0 -0
pi_python/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.11.0"
|
pi_python/agent.py
CHANGED
|
@@ -69,6 +69,8 @@ class Agent:
|
|
|
69
69
|
thinking_budgets: dict[str, int] | None = None,
|
|
70
70
|
transport: str | None = None,
|
|
71
71
|
session_id: str | None = None,
|
|
72
|
+
auto_session_id: bool = True,
|
|
73
|
+
cache_session_id: str | None = None,
|
|
72
74
|
get_api_key: Callable | None = None,
|
|
73
75
|
on_payload: Callable | None = None,
|
|
74
76
|
on_response: Callable | None = None,
|
|
@@ -87,6 +89,21 @@ class Agent:
|
|
|
87
89
|
if type(include_tool_origin) is not bool:
|
|
88
90
|
raise ConfigurationError("include_tool_origin must be a boolean")
|
|
89
91
|
self.include_tool_origin = include_tool_origin
|
|
92
|
+
if type(auto_session_id) is not bool:
|
|
93
|
+
raise ConfigurationError("auto_session_id must be a boolean")
|
|
94
|
+
if cache_session_id is not None:
|
|
95
|
+
if (
|
|
96
|
+
not isinstance(cache_session_id, str)
|
|
97
|
+
or not 1 <= len(cache_session_id) <= 64
|
|
98
|
+
or any(not "!" <= char <= "~" for char in cache_session_id)
|
|
99
|
+
):
|
|
100
|
+
raise ConfigurationError(
|
|
101
|
+
"cache_session_id must contain 1–64 printable ASCII characters without spaces"
|
|
102
|
+
)
|
|
103
|
+
if not auto_session_id:
|
|
104
|
+
raise ConfigurationError("cache_session_id requires auto_session_id=True")
|
|
105
|
+
self._auto_session_id = auto_session_id
|
|
106
|
+
self._cache_session_id = cache_session_id
|
|
90
107
|
self._queues = MessageQueues(steering_mode, follow_up_mode)
|
|
91
108
|
if provider is not None and stream_fn is not None:
|
|
92
109
|
raise ConfigurationError("Pass provider or stream_fn, not both")
|
|
@@ -148,6 +165,23 @@ class Agent:
|
|
|
148
165
|
self._settled = asyncio.Event()
|
|
149
166
|
self._settled.set()
|
|
150
167
|
|
|
168
|
+
@property
|
|
169
|
+
def cache_session_id(self) -> str | None:
|
|
170
|
+
"""Agent-managed Codex cache fallback, independent of explicit session_id.
|
|
171
|
+
|
|
172
|
+
Save this alongside history and restore it with cache_session_id=... .
|
|
173
|
+
Reading it does not enable connection reuse. Explicit session_id takes
|
|
174
|
+
precedence on requests; cache_retention='none' suppresses both on the wire.
|
|
175
|
+
"""
|
|
176
|
+
if (
|
|
177
|
+
not self._auto_session_id
|
|
178
|
+
or getattr(self.provider, "supports_agent_cache_session", False) is not True
|
|
179
|
+
):
|
|
180
|
+
return None
|
|
181
|
+
if self._cache_session_id is None:
|
|
182
|
+
self._cache_session_id = str(uuid4())
|
|
183
|
+
return self._cache_session_id
|
|
184
|
+
|
|
151
185
|
@property
|
|
152
186
|
def steering_mode(self) -> str:
|
|
153
187
|
return self._queues.steering_mode
|
|
@@ -265,6 +299,7 @@ class Agent:
|
|
|
265
299
|
raise AgentBusyError("Cannot reset while running")
|
|
266
300
|
baseline = current_system_message(self._messages)
|
|
267
301
|
self._messages = [baseline] if baseline else []
|
|
302
|
+
self._cache_session_id = None
|
|
268
303
|
self._last_error = None
|
|
269
304
|
self._run = None
|
|
270
305
|
self.clear_all_queues()
|
pi_python/features.py
CHANGED
pi_python/loop.py
CHANGED
|
@@ -105,6 +105,14 @@ async def stream_response(run: Run) -> tuple[AssistantMessage, bool]:
|
|
|
105
105
|
deepcopy(context.options),
|
|
106
106
|
model_info=deepcopy(model) if isinstance(model, ModelInfo) else None,
|
|
107
107
|
)
|
|
108
|
+
if (
|
|
109
|
+
getattr(run.provider, "supports_agent_cache_session", False) is True
|
|
110
|
+
and not request.options.get("session_id")
|
|
111
|
+
and request.options.get("cache_retention") != "none"
|
|
112
|
+
):
|
|
113
|
+
cache_session_id = run.agent.cache_session_id
|
|
114
|
+
if cache_session_id is not None:
|
|
115
|
+
request._set_cache_session_id(cache_session_id)
|
|
108
116
|
if run.hooks.get_api_key:
|
|
109
117
|
request.api_key = await run.hook(
|
|
110
118
|
"get_api_key", getattr(run.provider, "name", request.model)
|
pi_python/provider.py
CHANGED
|
@@ -28,6 +28,20 @@ class ModelRequest:
|
|
|
28
28
|
on_response: Callable | None = field(default=None, repr=False)
|
|
29
29
|
on_provider_stream_event: Callable | None = field(default=None, repr=False)
|
|
30
30
|
|
|
31
|
+
def _set_cache_session_id(self, value: str) -> None:
|
|
32
|
+
# Per-request metadata, deliberately outside the public dataclass fields
|
|
33
|
+
# and options. It must never be interpreted as a transport session_id.
|
|
34
|
+
self._agent_cache_session_id = value
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def cache_session_id(self) -> str | None:
|
|
38
|
+
"""Optional Agent-owned cache affinity, never a transport session.
|
|
39
|
+
|
|
40
|
+
Providers opt in with supports_agent_cache_session=True. This property
|
|
41
|
+
is not a dataclass field and does not change serialized request options.
|
|
42
|
+
"""
|
|
43
|
+
return getattr(self, "_agent_cache_session_id", None)
|
|
44
|
+
|
|
31
45
|
|
|
32
46
|
@dataclass
|
|
33
47
|
class ModelEvent:
|
|
@@ -109,6 +123,10 @@ def has_default_stream() -> bool:
|
|
|
109
123
|
|
|
110
124
|
|
|
111
125
|
class DefaultProvider:
|
|
126
|
+
@property
|
|
127
|
+
def supports_agent_cache_session(self) -> bool:
|
|
128
|
+
return getattr(_default_stream, "supports_agent_cache_session", False) is True
|
|
129
|
+
|
|
112
130
|
@property
|
|
113
131
|
def name(self) -> str:
|
|
114
132
|
return getattr(_default_stream, "name", "custom")
|
pi_python/providers/openai.py
CHANGED
|
@@ -318,6 +318,14 @@ class OpenAIProvider(RemoteProvider):
|
|
|
318
318
|
position += 1
|
|
319
319
|
return items, transcript
|
|
320
320
|
|
|
321
|
+
def _cache_key(self, request: ModelRequest) -> str | None:
|
|
322
|
+
if request.options.get("cache_retention", "short") == "none":
|
|
323
|
+
return None
|
|
324
|
+
key = request.options.get("session_id")
|
|
325
|
+
if not key and getattr(self, "supports_agent_cache_session", False) is True:
|
|
326
|
+
key = request.cache_session_id
|
|
327
|
+
return key[:64] if isinstance(key, str) else key
|
|
328
|
+
|
|
321
329
|
def build_request(self, request: ModelRequest, chatgpt_sign_in: bool = False) -> dict[str, Any]:
|
|
322
330
|
model = self.model_info(request)
|
|
323
331
|
compat = model.compat
|
|
@@ -334,9 +342,7 @@ class OpenAIProvider(RemoteProvider):
|
|
|
334
342
|
retention = options.get("cache_retention", "short")
|
|
335
343
|
if retention not in {"none", "short", "long"}:
|
|
336
344
|
raise ConfigurationError("Invalid cache_retention")
|
|
337
|
-
key =
|
|
338
|
-
if isinstance(key, str) and len(key) > 64:
|
|
339
|
-
key = key[:64]
|
|
345
|
+
key = self._cache_key(request)
|
|
340
346
|
body: dict[str, Any] = {
|
|
341
347
|
"model": request.model,
|
|
342
348
|
"input": items,
|
|
@@ -436,13 +442,17 @@ class OpenAIProvider(RemoteProvider):
|
|
|
436
442
|
"OpenAI-Beta": "responses=experimental",
|
|
437
443
|
}
|
|
438
444
|
)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
445
|
+
cache_key = self._cache_key(request)
|
|
446
|
+
if cache_key:
|
|
447
|
+
if request.options.get("session_id"):
|
|
448
|
+
# Preserve the existing precedence of explicit session_id.
|
|
449
|
+
headers["session-id"] = headers["x-client-request-id"] = cache_key
|
|
450
|
+
else:
|
|
451
|
+
# Automatic defaults must not overwrite application headers.
|
|
452
|
+
supplied = {name.lower() for name in headers}
|
|
453
|
+
for name in ("session-id", "x-client-request-id"):
|
|
454
|
+
if name not in supplied:
|
|
455
|
+
headers[name] = cache_key
|
|
446
456
|
if (
|
|
447
457
|
self.name == "openai"
|
|
448
458
|
and request.options.get("session_id")
|
|
@@ -458,7 +468,8 @@ class OpenAIProvider(RemoteProvider):
|
|
|
458
468
|
body = await self.payload(request, self.build_request(request, chatgpt))
|
|
459
469
|
if transport == "websocket-cached" and not request.options.get("session_id"):
|
|
460
470
|
raise ConfigurationError("websocket-cached requires session_id")
|
|
461
|
-
#
|
|
471
|
+
# Only an application-supplied session_id can opt into connection reuse
|
|
472
|
+
# and response continuation. The Agent's cache fallback never reaches here.
|
|
462
473
|
cached = (
|
|
463
474
|
request.options.get("session_id")
|
|
464
475
|
if request.options.get("cache_retention") != "none"
|
|
@@ -645,6 +656,7 @@ class OpenAIProvider(RemoteProvider):
|
|
|
645
656
|
class OpenAICodexProvider(OpenAIProvider):
|
|
646
657
|
name = "openai-codex"
|
|
647
658
|
api = "openai-codex-responses"
|
|
659
|
+
supports_agent_cache_session = True
|
|
648
660
|
|
|
649
661
|
def __init__(
|
|
650
662
|
self, *, base_url: str = "https://chatgpt.com/backend-api/codex", **kwargs: Any
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pi-python-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: An embeddable asyncio agent core with opt-in Claude and OpenAI providers
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -111,7 +111,7 @@ Apart from `provider_chat`, which needs real credentials, every example runs off
|
|
|
111
111
|
|
|
112
112
|
The run loop, event order, hooks, queues, and the handling of errors and cancellation all match Pi, and this is checked differentially: the same inputs go to the pinned upstream code and to this library, and the model requests, tool calls, events and final transcript are compared item by item. All cases currently agree: 25 for the core loop, 50 for model connectors, 2 for multi-turn WebSocket, and 44 error-classification samples. The plugin rules for skills, prompt templates and frontmatter are compared the same way against Pi's coding-agent code.
|
|
113
113
|
|
|
114
|
-
A few differences are deliberate, such as strict tool-argument validation without type coercion, and returning copies of state to callers. Others are additions for Python users, such as `@tool`, blocking calls, and calling `continue_run()` directly after a failure. Each one is recorded in the [coverage map](compat/COVERAGE.md) (Chinese). Features Pi keeps in its application layer (terminal UI, session file format, context compaction) are not in this core; compaction can be built with hooks, and an example shows the full approach. Plugins are
|
|
114
|
+
A few differences are deliberate, such as strict tool-argument validation without type coercion, and returning copies of state to callers. Others are additions for Python users, such as `@tool`, blocking calls, and calling `continue_run()` directly after a failure. Each one is recorded in the [coverage map](compat/COVERAGE.md) (Chinese). Features Pi keeps in its application layer (terminal UI, session file format, context compaction) are not in this core; compaction can be built with hooks, and an example shows the full approach. The Agent also supplies cache-only session IDs for Codex, a responsibility upstream assigns to its coding-agent layer; see [cache sessions and the opt-out](docs/PROVIDERS.md#automatic-codex-cache-sessions). Plugins are another piece taken from that layer: an optional module that reads Pi's package format and builds an ordinary Agent from it. This project uses its own version numbers and is not an official Pi release.
|
|
115
115
|
|
|
116
116
|
## Documentation
|
|
117
117
|
|
|
@@ -3,22 +3,22 @@ pi_python/_mcp_host.py,sha256=CXio-tiEeUCUfdl65tatwSrzIOYdgecdS00sO6tNy5k,8630
|
|
|
3
3
|
pi_python/_mcp_interaction.py,sha256=jPL-C8wLjBDV9tOdvGVFfXOl8-1FQIDJLq-MojlzXaM,43851
|
|
4
4
|
pi_python/_mcp_process.py,sha256=4szi5gMEP_0GUGOcV-oLPUuVQAUSb0ToAaiM979zHME,6324
|
|
5
5
|
pi_python/_mcp_sampling.py,sha256=BcWCrPgjsRC-GNx65fIB-JPq2WzWnoBK2w9iTnyPUww,12148
|
|
6
|
-
pi_python/_version.py,sha256=
|
|
7
|
-
pi_python/agent.py,sha256=
|
|
6
|
+
pi_python/_version.py,sha256=raMu9XA9JEjvdoTmFqcOw7qhJX24rYDP7XmS59TAO-Q,23
|
|
7
|
+
pi_python/agent.py,sha256=5yd5D_hDGwp74dtvH1SCQUQdLFpc1nxTqAEKBbleSDQ,19797
|
|
8
8
|
pi_python/cancellation.py,sha256=DoQVdX7DcQdVLkl4OvhcpMhKZJEW0Kscl2-xxF6i6Y0,608
|
|
9
9
|
pi_python/errors.py,sha256=65lT0nYfzqxRfUlFG-Y9vgYxfDHEuFiBfgqmFnX9_O8,675
|
|
10
10
|
pi_python/estimate.py,sha256=dQQJ9UEGp57F3wO_CcU2oubgPe841ttgpUseK3Fu_0w,5048
|
|
11
11
|
pi_python/events.py,sha256=HFYaPB9-yjOvmuM2Ly7SgfWDMJJfX9e1Q7ethqOHSB8,4626
|
|
12
|
-
pi_python/features.py,sha256=
|
|
12
|
+
pi_python/features.py,sha256=Q7X1ke7FcGCyb8XURw4iDa2-wfbaWQQCdbR4-Zh5fsk,1720
|
|
13
13
|
pi_python/function_tools.py,sha256=DO8fZD-n5i62w22zLIjblCOTwnG8N-HIoSUo4yOpRM4,18862
|
|
14
14
|
pi_python/hooks.py,sha256=jm6ep78NjdHxIPZvcOXTZIPxkfBMkffVTNzLvnHS9mM,1321
|
|
15
15
|
pi_python/limits.py,sha256=K1aP1cFH4fVhtWIXqpWMmpkB9AI0wK-bz3Xd2hxnOs4,1157
|
|
16
|
-
pi_python/loop.py,sha256=
|
|
16
|
+
pi_python/loop.py,sha256=Q67rzG0eERY3Y_NMDdCIbKZiadgdcF2sSB83qni92ms,19263
|
|
17
17
|
pi_python/lowlevel.py,sha256=56wtwqD0Ur1AGAXGa7zclb1IpmRPFpSEss_nspkAdrI,5514
|
|
18
18
|
pi_python/mcp.py,sha256=f7VOiOeBwJ48SrowJWmqtH3d-iavrv2GZo2KH45qeTA,16713
|
|
19
19
|
pi_python/messages.py,sha256=jUoQxR-UZbIQYWu257dGiCx8K5z_RV89fJpHgv4t-Ww,18068
|
|
20
20
|
pi_python/models.py,sha256=IejtaWvbURM9vv_2kkoDzfto2TR0OSLjuQeOh3IJZ1k,6094
|
|
21
|
-
pi_python/provider.py,sha256=
|
|
21
|
+
pi_python/provider.py,sha256=WH1-8YQKuV0PrBTLdNJKN9oXYnnOQiCxt4kD2BL-ebw,5047
|
|
22
22
|
pi_python/proxy.py,sha256=kpEUoE1oDkJW7VVaV72slN3uJUFgYR4Q6jtYOEe8LmQ,12159
|
|
23
23
|
pi_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
pi_python/queues.py,sha256=nQ5xRSA3MMOHMs_ZJnG9sT3-d_yKn72oSnxXqiKPrcI,3032
|
|
@@ -41,10 +41,10 @@ pi_python/providers/anthropic.py,sha256=PCH9h9WHNXbz7Pv01NM1d8NtHANthLRaU1mZUVUc
|
|
|
41
41
|
pi_python/providers/common.py,sha256=z8dB2pIc-dRscPAppnA3ZwF_wJygunQb9pI9Wl4CvaA,8051
|
|
42
42
|
pi_python/providers/completions.py,sha256=vcqoLfJ-9quMREEEAJcHYs3Oq-A0Sz8Q1NV1QLd3OJA,51362
|
|
43
43
|
pi_python/providers/oauth.py,sha256=ZY8j7P-I-R6hZ8sInkGao957KWNGsqfvMF0F-YzlSgU,22786
|
|
44
|
-
pi_python/providers/openai.py,sha256=
|
|
44
|
+
pi_python/providers/openai.py,sha256=c2xzfRd0RgYxbtgl-ruA0yLySq5W63mmwyu_HFQm2g0,30931
|
|
45
45
|
pi_python/providers/transport.py,sha256=B1mGKlVEqYGVK1MlcA9opwRhnuUYGuaYuTslNC3O8xs,26772
|
|
46
|
-
pi_python_core-0.
|
|
47
|
-
pi_python_core-0.
|
|
48
|
-
pi_python_core-0.
|
|
49
|
-
pi_python_core-0.
|
|
50
|
-
pi_python_core-0.
|
|
46
|
+
pi_python_core-0.11.0.dist-info/METADATA,sha256=m36iv6YTPLV0GsXN29gp-rIwgRQSnO8m-uAtgvsovYs,9146
|
|
47
|
+
pi_python_core-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
48
|
+
pi_python_core-0.11.0.dist-info/licenses/LICENSE,sha256=BFf1vOw7OyEWBd-10aSQQv1jjzaGpBD-CZwkolrxPEg,1069
|
|
49
|
+
pi_python_core-0.11.0.dist-info/licenses/NOTICE,sha256=fJna16fm1ECHN7zVZhtyNmjLw8KGxGp8mL9blISF1K0,633
|
|
50
|
+
pi_python_core-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|