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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.10.2"
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
@@ -20,6 +20,7 @@ FEATURES = MCP_FEATURES | frozenset(
20
20
  "loop-portal-v1",
21
21
  "nested-agent-ownership-v1",
22
22
  "application-tool-batches-v1",
23
+ "codex-auto-cache-session-v1",
23
24
  "plugin-requires-v1",
24
25
  }
25
26
  )
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")
@@ -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 = options.get("session_id") if retention != "none" else None
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
- if (
440
- request.options.get("session_id")
441
- and request.options.get("cache_retention") != "none"
442
- ):
443
- headers["session-id"] = headers["x-client-request-id"] = request.options[
444
- "session_id"
445
- ][:64]
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
- # Pi caches a connection per session unless caching is disabled for the request.
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.10.2
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 the one 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.
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=A_AARqtxTOj_AQTpjpgOxNx-UOBio5wYFfZ2mrdMKfs,23
7
- pi_python/agent.py,sha256=86_mWt2oysz0Llv0LkQRnGKjYsloVElkQFnrkE5ZXv0,18206
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=V9Yhh1APuN8KaLMrAx8s00xeDLAF0ydh5jI5FCT9ZQg,1681
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=nr3P6fXnrG21DFXPCGMGTKssE30jJmMoWi4bKIWxOu0,18904
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=k_VfJu5WOQla_LkglTDAq5s-77ejCj4YRTyhJ8onZBo,4259
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=p0WHDnojAYb7mUX8EbComwEU_XskcYoMhMIR07QlSfc,30196
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.10.2.dist-info/METADATA,sha256=SGNj48Q5U-Q-Z0AVANvz5CQoJVcLqYkMjEkXZN3AFA4,8939
47
- pi_python_core-0.10.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
48
- pi_python_core-0.10.2.dist-info/licenses/LICENSE,sha256=BFf1vOw7OyEWBd-10aSQQv1jjzaGpBD-CZwkolrxPEg,1069
49
- pi_python_core-0.10.2.dist-info/licenses/NOTICE,sha256=fJna16fm1ECHN7zVZhtyNmjLw8KGxGp8mL9blISF1K0,633
50
- pi_python_core-0.10.2.dist-info/RECORD,,
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,,