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
pi_durable/truncate.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Shared truncation utilities for tool outputs.
|
|
2
|
+
|
|
3
|
+
Port of the v1.3.0 ``truncate.ts``: two independent limits, whichever is hit first wins, and no
|
|
4
|
+
partial lines. Tool output streams are bounded by :class:`~pi_durable.harness.output.OutputBuffer`
|
|
5
|
+
instead.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
DEFAULT_MAX_LINES = 2000
|
|
13
|
+
DEFAULT_MAX_BYTES = 50 * 1024
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _well_formed(text: str) -> str:
|
|
17
|
+
"""Combine surrogate pairs and replace lone surrogates, like a WHATWG ``TextEncoder``."""
|
|
18
|
+
|
|
19
|
+
if not any(0xD800 <= ord(character) <= 0xDFFF for character in text):
|
|
20
|
+
return text
|
|
21
|
+
characters: list[str] = []
|
|
22
|
+
index = 0
|
|
23
|
+
while index < len(text):
|
|
24
|
+
code = ord(text[index])
|
|
25
|
+
if 0xD800 <= code <= 0xDBFF and index + 1 < len(text):
|
|
26
|
+
low = ord(text[index + 1])
|
|
27
|
+
if 0xDC00 <= low <= 0xDFFF:
|
|
28
|
+
characters.append(chr(0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00)))
|
|
29
|
+
index += 2
|
|
30
|
+
continue
|
|
31
|
+
if 0xD800 <= code <= 0xDFFF:
|
|
32
|
+
characters.append("\ufffd")
|
|
33
|
+
else:
|
|
34
|
+
characters.append(text[index])
|
|
35
|
+
index += 1
|
|
36
|
+
return "".join(characters)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def utf8_byte_length(text: str) -> int:
|
|
40
|
+
"""UTF-8 byte length of ``text``; a lone surrogate counts as three bytes."""
|
|
41
|
+
|
|
42
|
+
return len(_well_formed(text).encode("utf-8"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class TruncationOptions:
|
|
47
|
+
max_lines: int = DEFAULT_MAX_LINES
|
|
48
|
+
max_bytes: int = DEFAULT_MAX_BYTES
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class TruncationResult:
|
|
53
|
+
content: str
|
|
54
|
+
truncated: bool
|
|
55
|
+
truncated_by: str | None
|
|
56
|
+
total_lines: int
|
|
57
|
+
total_bytes: int
|
|
58
|
+
output_lines: int
|
|
59
|
+
output_bytes: int
|
|
60
|
+
last_line_partial: bool
|
|
61
|
+
first_line_exceeds_limit: bool
|
|
62
|
+
max_lines: int
|
|
63
|
+
max_bytes: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _split_lines_for_counting(content: str) -> list[str]:
|
|
67
|
+
if content == "":
|
|
68
|
+
return []
|
|
69
|
+
lines = content.split("\n")
|
|
70
|
+
if content.endswith("\n"):
|
|
71
|
+
lines.pop()
|
|
72
|
+
return lines
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_size(bytes_: int) -> str:
|
|
76
|
+
"""Format bytes as a human-readable size."""
|
|
77
|
+
|
|
78
|
+
if bytes_ < 1024:
|
|
79
|
+
return f"{bytes_}B"
|
|
80
|
+
if bytes_ < 1024 * 1024:
|
|
81
|
+
return f"{bytes_ / 1024:.1f}KB"
|
|
82
|
+
return f"{bytes_ / (1024 * 1024):.1f}MB"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def truncate_head(content: str, options: TruncationOptions | None = None) -> TruncationResult:
|
|
86
|
+
"""Keep the first complete lines that fit both limits."""
|
|
87
|
+
|
|
88
|
+
return truncate_head_of(
|
|
89
|
+
content,
|
|
90
|
+
(len(_split_lines_for_counting(content)), utf8_byte_length(content)),
|
|
91
|
+
options,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def truncate_head_of(
|
|
96
|
+
prefix: str,
|
|
97
|
+
totals: tuple[int, int],
|
|
98
|
+
options: TruncationOptions | None = None,
|
|
99
|
+
) -> TruncationResult:
|
|
100
|
+
"""``truncate_head`` of a text known by a prefix and its totals.
|
|
101
|
+
|
|
102
|
+
``prefix`` must be the whole text, or longer than ``max_bytes + 1`` UTF-8 bytes, or hold at
|
|
103
|
+
least ``max_lines`` newlines; then the result equals ``truncate_head`` of the whole text.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
limits = options or TruncationOptions()
|
|
107
|
+
max_lines = limits.max_lines
|
|
108
|
+
max_bytes = limits.max_bytes
|
|
109
|
+
total_lines, total_bytes = totals
|
|
110
|
+
lines = _split_lines_for_counting(prefix)
|
|
111
|
+
|
|
112
|
+
if total_lines <= max_lines and total_bytes <= max_bytes:
|
|
113
|
+
return TruncationResult(
|
|
114
|
+
content=prefix,
|
|
115
|
+
truncated=False,
|
|
116
|
+
truncated_by=None,
|
|
117
|
+
total_lines=total_lines,
|
|
118
|
+
total_bytes=total_bytes,
|
|
119
|
+
output_lines=total_lines,
|
|
120
|
+
output_bytes=total_bytes,
|
|
121
|
+
last_line_partial=False,
|
|
122
|
+
first_line_exceeds_limit=False,
|
|
123
|
+
max_lines=max_lines,
|
|
124
|
+
max_bytes=max_bytes,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
first_line_bytes = utf8_byte_length(lines[0])
|
|
128
|
+
if first_line_bytes > max_bytes:
|
|
129
|
+
return TruncationResult(
|
|
130
|
+
content="",
|
|
131
|
+
truncated=True,
|
|
132
|
+
truncated_by="bytes",
|
|
133
|
+
total_lines=total_lines,
|
|
134
|
+
total_bytes=total_bytes,
|
|
135
|
+
output_lines=0,
|
|
136
|
+
output_bytes=0,
|
|
137
|
+
last_line_partial=False,
|
|
138
|
+
first_line_exceeds_limit=True,
|
|
139
|
+
max_lines=max_lines,
|
|
140
|
+
max_bytes=max_bytes,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
output_lines: list[str] = []
|
|
144
|
+
output_bytes_count = 0
|
|
145
|
+
truncated_by: str = "lines"
|
|
146
|
+
for index, line in enumerate(lines):
|
|
147
|
+
if index >= max_lines:
|
|
148
|
+
break
|
|
149
|
+
line_bytes = utf8_byte_length(line) + (1 if index > 0 else 0)
|
|
150
|
+
if output_bytes_count + line_bytes > max_bytes:
|
|
151
|
+
truncated_by = "bytes"
|
|
152
|
+
break
|
|
153
|
+
output_lines.append(line)
|
|
154
|
+
output_bytes_count += line_bytes
|
|
155
|
+
|
|
156
|
+
# Without a byte break, only omitted lines prove the line limit was reached.
|
|
157
|
+
if truncated_by != "bytes":
|
|
158
|
+
truncated_by = "lines" if len(output_lines) < total_lines else "bytes"
|
|
159
|
+
|
|
160
|
+
output_content = "\n".join(output_lines)
|
|
161
|
+
return TruncationResult(
|
|
162
|
+
content=output_content,
|
|
163
|
+
truncated=True,
|
|
164
|
+
truncated_by=truncated_by,
|
|
165
|
+
total_lines=total_lines,
|
|
166
|
+
total_bytes=total_bytes,
|
|
167
|
+
output_lines=len(output_lines),
|
|
168
|
+
output_bytes=utf8_byte_length(output_content),
|
|
169
|
+
last_line_partial=False,
|
|
170
|
+
first_line_exceeds_limit=False,
|
|
171
|
+
max_lines=max_lines,
|
|
172
|
+
max_bytes=max_bytes,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
__all__ = [
|
|
177
|
+
"DEFAULT_MAX_BYTES",
|
|
178
|
+
"DEFAULT_MAX_LINES",
|
|
179
|
+
"TruncationOptions",
|
|
180
|
+
"TruncationResult",
|
|
181
|
+
"format_size",
|
|
182
|
+
"truncate_head",
|
|
183
|
+
"truncate_head_of",
|
|
184
|
+
"utf8_byte_length",
|
|
185
|
+
]
|