pi-durable-python 1.3.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_durable/__init__.py +320 -0
- pi_durable/context.py +328 -0
- pi_durable/delta.py +269 -0
- pi_durable/documents.py +366 -0
- pi_durable/entries.py +66 -0
- pi_durable/env/__init__.py +408 -0
- pi_durable/env/decode.py +48 -0
- pi_durable/env/line_scan.py +190 -0
- pi_durable/env/posix.py +1547 -0
- pi_durable/errors.py +89 -0
- pi_durable/harness/__init__.py +1 -0
- pi_durable/harness/agent.py +344 -0
- pi_durable/harness/compaction.py +673 -0
- pi_durable/harness/context.py +515 -0
- pi_durable/harness/define.py +111 -0
- pi_durable/harness/events.py +766 -0
- pi_durable/harness/generation.py +1079 -0
- pi_durable/harness/harness.py +570 -0
- pi_durable/harness/inbox.py +255 -0
- pi_durable/harness/live.py +374 -0
- pi_durable/harness/output.py +467 -0
- pi_durable/harness/prompt.py +288 -0
- pi_durable/harness/provider.py +78 -0
- pi_durable/harness/registry.py +136 -0
- pi_durable/harness/scheduler.py +1977 -0
- pi_durable/harness/submissions.py +357 -0
- pi_durable/harness/task_graph.py +334 -0
- pi_durable/harness/tool.py +1365 -0
- pi_durable/harness/types.py +772 -0
- pi_durable/harness/usage.py +88 -0
- pi_durable/harness/util.py +204 -0
- pi_durable/harness/validation.py +460 -0
- pi_durable/harness/view.py +324 -0
- pi_durable/ids.py +63 -0
- pi_durable/json.py +186 -0
- pi_durable/providers/__init__.py +362 -0
- pi_durable/providers/anthropic.py +603 -0
- pi_durable/providers/base.py +97 -0
- pi_durable/providers/json_parse.py +117 -0
- pi_durable/providers/openai.py +574 -0
- pi_durable/providers/request.py +35 -0
- pi_durable/providers/retry.py +127 -0
- pi_durable/providers/stream.py +183 -0
- pi_durable/providers/transcript.py +224 -0
- pi_durable/providers/usage.py +58 -0
- pi_durable/py.typed +0 -0
- pi_durable/search.py +115 -0
- pi_durable/session/__init__.py +5 -0
- pi_durable/session/drafts.py +762 -0
- pi_durable/session/forks.py +109 -0
- pi_durable/session/observation.py +374 -0
- pi_durable/session/session.py +1193 -0
- pi_durable/session/transaction.py +1088 -0
- pi_durable/storage/__init__.py +5 -0
- pi_durable/storage/jsonl/__init__.py +15 -0
- pi_durable/storage/jsonl/storage.py +958 -0
- pi_durable/storage/memory.py +889 -0
- pi_durable/storage/scan.py +57 -0
- pi_durable/storage/sqlite/__init__.py +26 -0
- pi_durable/storage/sqlite/database.py +62 -0
- pi_durable/storage/sqlite/migrations.py +191 -0
- pi_durable/storage/sqlite/posix.py +314 -0
- pi_durable/storage/sqlite/schema.py +97 -0
- pi_durable/storage/sqlite/storage.py +1130 -0
- pi_durable/tasks.py +89 -0
- pi_durable/testing/__init__.py +41 -0
- pi_durable/testing/assertions.py +44 -0
- pi_durable/testing/env_conformance.py +828 -0
- pi_durable/testing/runner.py +64 -0
- pi_durable/testing/storage_conformance.py +2008 -0
- pi_durable/testing/types.py +99 -0
- pi_durable/tool_search/__init__.py +159 -0
- pi_durable/tools/__init__.py +81 -0
- pi_durable/tools/_values.py +43 -0
- pi_durable/tools/bash.py +250 -0
- pi_durable/tools/edit.py +214 -0
- pi_durable/tools/edit_diff.py +452 -0
- pi_durable/tools/env.py +19 -0
- pi_durable/tools/file_mutation_queue.py +68 -0
- pi_durable/tools/image.py +248 -0
- pi_durable/tools/image_processor.py +77 -0
- pi_durable/tools/path_utils.py +111 -0
- pi_durable/tools/read.py +506 -0
- pi_durable/tools/write.py +71 -0
- pi_durable/truncate.py +185 -0
- pi_durable/types.py +1297 -0
- pi_durable_python-1.3.0.dist-info/METADATA +276 -0
- pi_durable_python-1.3.0.dist-info/RECORD +90 -0
- pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
- pi_durable_python-1.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Catalog lookup and non-streaming conveniences shared by the model adapters.
|
|
2
|
+
|
|
3
|
+
Both adapters resolve models through a caller-supplied catalog and implement the same non-streaming
|
|
4
|
+
and deferred halves of the :class:`~pi_durable.providers.Models` protocol. Only their SDK client and
|
|
5
|
+
stream translation differ, so those stay in the adapter modules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Awaitable, Mapping, Sequence
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from . import Model
|
|
14
|
+
from .stream import ProviderStream, complete_stream
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProviderModels:
|
|
18
|
+
"""The part of the ``Models`` protocol every adapter shares.
|
|
19
|
+
|
|
20
|
+
Subclasses set :attr:`api`, implement :meth:`stream_simple` and :meth:`stream`, and own their
|
|
21
|
+
SDK client. Deferred provider responses are unsupported by the Chat Completions and Messages
|
|
22
|
+
adapters, so the base rejects them with a clear error.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
api = "model"
|
|
26
|
+
|
|
27
|
+
def __init__(self, models: Sequence[Model] = ()) -> None:
|
|
28
|
+
self._models = list(models)
|
|
29
|
+
|
|
30
|
+
def get_model(self, provider: str, model_id: str) -> Model | None:
|
|
31
|
+
"""Resolve one catalog entry by provider and model ID."""
|
|
32
|
+
|
|
33
|
+
return next(
|
|
34
|
+
(
|
|
35
|
+
model
|
|
36
|
+
for model in self._models
|
|
37
|
+
if model.provider == provider and model.model_id == model_id
|
|
38
|
+
),
|
|
39
|
+
None,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def stream_simple(
|
|
43
|
+
self,
|
|
44
|
+
model: Model,
|
|
45
|
+
context: Mapping[str, Any],
|
|
46
|
+
options: Mapping[str, Any] | None = None,
|
|
47
|
+
) -> ProviderStream:
|
|
48
|
+
raise NotImplementedError
|
|
49
|
+
|
|
50
|
+
def stream(
|
|
51
|
+
self,
|
|
52
|
+
model: Model,
|
|
53
|
+
context: Mapping[str, Any],
|
|
54
|
+
options: Mapping[str, Any] | None = None,
|
|
55
|
+
) -> ProviderStream:
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
def complete_simple(
|
|
59
|
+
self,
|
|
60
|
+
model: Model,
|
|
61
|
+
context: Mapping[str, Any],
|
|
62
|
+
options: Mapping[str, Any] | None = None,
|
|
63
|
+
) -> Awaitable[dict[str, Any]]:
|
|
64
|
+
"""Run one simple request to completion and return its terminal message."""
|
|
65
|
+
|
|
66
|
+
return complete_stream(self.stream_simple(model, context, options))
|
|
67
|
+
|
|
68
|
+
def complete(
|
|
69
|
+
self,
|
|
70
|
+
model: Model,
|
|
71
|
+
context: Mapping[str, Any],
|
|
72
|
+
options: Mapping[str, Any] | None = None,
|
|
73
|
+
) -> Awaitable[dict[str, Any]]:
|
|
74
|
+
"""Run one full-option request to completion and return its terminal message."""
|
|
75
|
+
|
|
76
|
+
return complete_stream(self.stream(model, context, options))
|
|
77
|
+
|
|
78
|
+
async def fetch_deferred(
|
|
79
|
+
self,
|
|
80
|
+
model: Model,
|
|
81
|
+
handle: Mapping[str, Any],
|
|
82
|
+
options: Mapping[str, Any] | None = None,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
del model, handle, options
|
|
85
|
+
raise NotImplementedError(f"The {self.api} adapter has no deferred responses")
|
|
86
|
+
|
|
87
|
+
async def cancel_deferred(
|
|
88
|
+
self,
|
|
89
|
+
model: Model,
|
|
90
|
+
handle: Mapping[str, Any],
|
|
91
|
+
options: Mapping[str, Any] | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
del model, handle, options
|
|
94
|
+
raise NotImplementedError(f"The {self.api} adapter has no deferred responses")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["ProviderModels"]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Tool-call argument parsing shared by the model adapters.
|
|
2
|
+
|
|
3
|
+
Port of pi-ai 1.3.0 ``utils/json-parse.ts``. Streaming providers send a call's arguments as JSON
|
|
4
|
+
text split across deltas, and some models emit raw control characters inside JSON strings. The
|
|
5
|
+
repair pass escapes those, and a finished call whose text is still not a valid JSON object reports
|
|
6
|
+
``argumentsError`` instead of pretending the call was empty.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
_VALID_ESCAPES = frozenset('"\\/bfnrt')
|
|
15
|
+
_CONTROL_ESCAPES = {
|
|
16
|
+
"\b": "\\b",
|
|
17
|
+
"\f": "\\f",
|
|
18
|
+
"\n": "\\n",
|
|
19
|
+
"\r": "\\r",
|
|
20
|
+
"\t": "\\t",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def repair_json(text: str) -> str:
|
|
25
|
+
"""Escape raw control characters and double backslashes before invalid escapes, inside strings."""
|
|
26
|
+
|
|
27
|
+
repaired: list[str] = []
|
|
28
|
+
in_string = False
|
|
29
|
+
index = 0
|
|
30
|
+
while index < len(text):
|
|
31
|
+
char = text[index]
|
|
32
|
+
if not in_string:
|
|
33
|
+
repaired.append(char)
|
|
34
|
+
if char == '"':
|
|
35
|
+
in_string = True
|
|
36
|
+
index += 1
|
|
37
|
+
continue
|
|
38
|
+
if char == '"':
|
|
39
|
+
repaired.append(char)
|
|
40
|
+
in_string = False
|
|
41
|
+
index += 1
|
|
42
|
+
continue
|
|
43
|
+
if char == "\\":
|
|
44
|
+
following = text[index + 1] if index + 1 < len(text) else None
|
|
45
|
+
if following is None:
|
|
46
|
+
repaired.append("\\\\")
|
|
47
|
+
index += 1
|
|
48
|
+
continue
|
|
49
|
+
if following == "u":
|
|
50
|
+
digits = text[index + 2 : index + 6]
|
|
51
|
+
if len(digits) == 4 and all(digit in "0123456789abcdefABCDEF" for digit in digits):
|
|
52
|
+
repaired.append(f"\\u{digits}")
|
|
53
|
+
index += 6
|
|
54
|
+
continue
|
|
55
|
+
if following in _VALID_ESCAPES:
|
|
56
|
+
repaired.append(f"\\{following}")
|
|
57
|
+
index += 2
|
|
58
|
+
continue
|
|
59
|
+
repaired.append("\\\\")
|
|
60
|
+
index += 1
|
|
61
|
+
continue
|
|
62
|
+
if ord(char) <= 0x1F:
|
|
63
|
+
repaired.append(_CONTROL_ESCAPES.get(char, f"\\u{ord(char):04x}"))
|
|
64
|
+
else:
|
|
65
|
+
repaired.append(char)
|
|
66
|
+
index += 1
|
|
67
|
+
return "".join(repaired)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_json_with_repair(text: str) -> Any:
|
|
71
|
+
"""Parse ``text``, retrying once with :func:`repair_json`."""
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
return json.loads(text)
|
|
75
|
+
except ValueError:
|
|
76
|
+
repaired = repair_json(text)
|
|
77
|
+
if repaired == text:
|
|
78
|
+
raise
|
|
79
|
+
return json.loads(repaired)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_tool_call_arguments(text: str | None) -> tuple[dict[str, Any], str | None]:
|
|
83
|
+
"""Parse a finished call's argument text into an object, or report why it is not one."""
|
|
84
|
+
|
|
85
|
+
if text is None or not text.strip():
|
|
86
|
+
return {}, None
|
|
87
|
+
try:
|
|
88
|
+
parsed = parse_json_with_repair(text)
|
|
89
|
+
if not isinstance(parsed, dict):
|
|
90
|
+
raise ValueError("Tool arguments must be a JSON object")
|
|
91
|
+
return parsed, None
|
|
92
|
+
except ValueError as error:
|
|
93
|
+
return {}, f"{error}\n\nReceived arguments:\n{text}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_streaming_json(text: str | None) -> dict[str, Any]:
|
|
97
|
+
"""Parse a still-growing argument text, returning ``{}`` until it is valid JSON.
|
|
98
|
+
|
|
99
|
+
The port does not carry pi-ai's partial-JSON dependency; a partial call therefore shows no
|
|
100
|
+
arguments until its text parses, and the finished call is the authoritative value.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
if text is None or not text.strip():
|
|
104
|
+
return {}
|
|
105
|
+
try:
|
|
106
|
+
parsed = parse_json_with_repair(text)
|
|
107
|
+
except ValueError:
|
|
108
|
+
return {}
|
|
109
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
__all__ = [
|
|
113
|
+
"parse_json_with_repair",
|
|
114
|
+
"parse_streaming_json",
|
|
115
|
+
"parse_tool_call_arguments",
|
|
116
|
+
"repair_json",
|
|
117
|
+
]
|