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.
Files changed (90) hide show
  1. pi_durable/__init__.py +320 -0
  2. pi_durable/context.py +328 -0
  3. pi_durable/delta.py +269 -0
  4. pi_durable/documents.py +366 -0
  5. pi_durable/entries.py +66 -0
  6. pi_durable/env/__init__.py +408 -0
  7. pi_durable/env/decode.py +48 -0
  8. pi_durable/env/line_scan.py +190 -0
  9. pi_durable/env/posix.py +1547 -0
  10. pi_durable/errors.py +89 -0
  11. pi_durable/harness/__init__.py +1 -0
  12. pi_durable/harness/agent.py +344 -0
  13. pi_durable/harness/compaction.py +673 -0
  14. pi_durable/harness/context.py +515 -0
  15. pi_durable/harness/define.py +111 -0
  16. pi_durable/harness/events.py +766 -0
  17. pi_durable/harness/generation.py +1079 -0
  18. pi_durable/harness/harness.py +570 -0
  19. pi_durable/harness/inbox.py +255 -0
  20. pi_durable/harness/live.py +374 -0
  21. pi_durable/harness/output.py +467 -0
  22. pi_durable/harness/prompt.py +288 -0
  23. pi_durable/harness/provider.py +78 -0
  24. pi_durable/harness/registry.py +136 -0
  25. pi_durable/harness/scheduler.py +1977 -0
  26. pi_durable/harness/submissions.py +357 -0
  27. pi_durable/harness/task_graph.py +334 -0
  28. pi_durable/harness/tool.py +1365 -0
  29. pi_durable/harness/types.py +772 -0
  30. pi_durable/harness/usage.py +88 -0
  31. pi_durable/harness/util.py +204 -0
  32. pi_durable/harness/validation.py +460 -0
  33. pi_durable/harness/view.py +324 -0
  34. pi_durable/ids.py +63 -0
  35. pi_durable/json.py +186 -0
  36. pi_durable/providers/__init__.py +362 -0
  37. pi_durable/providers/anthropic.py +603 -0
  38. pi_durable/providers/base.py +97 -0
  39. pi_durable/providers/json_parse.py +117 -0
  40. pi_durable/providers/openai.py +574 -0
  41. pi_durable/providers/request.py +35 -0
  42. pi_durable/providers/retry.py +127 -0
  43. pi_durable/providers/stream.py +183 -0
  44. pi_durable/providers/transcript.py +224 -0
  45. pi_durable/providers/usage.py +58 -0
  46. pi_durable/py.typed +0 -0
  47. pi_durable/search.py +115 -0
  48. pi_durable/session/__init__.py +5 -0
  49. pi_durable/session/drafts.py +762 -0
  50. pi_durable/session/forks.py +109 -0
  51. pi_durable/session/observation.py +374 -0
  52. pi_durable/session/session.py +1193 -0
  53. pi_durable/session/transaction.py +1088 -0
  54. pi_durable/storage/__init__.py +5 -0
  55. pi_durable/storage/jsonl/__init__.py +15 -0
  56. pi_durable/storage/jsonl/storage.py +958 -0
  57. pi_durable/storage/memory.py +889 -0
  58. pi_durable/storage/scan.py +57 -0
  59. pi_durable/storage/sqlite/__init__.py +26 -0
  60. pi_durable/storage/sqlite/database.py +62 -0
  61. pi_durable/storage/sqlite/migrations.py +191 -0
  62. pi_durable/storage/sqlite/posix.py +314 -0
  63. pi_durable/storage/sqlite/schema.py +97 -0
  64. pi_durable/storage/sqlite/storage.py +1130 -0
  65. pi_durable/tasks.py +89 -0
  66. pi_durable/testing/__init__.py +41 -0
  67. pi_durable/testing/assertions.py +44 -0
  68. pi_durable/testing/env_conformance.py +828 -0
  69. pi_durable/testing/runner.py +64 -0
  70. pi_durable/testing/storage_conformance.py +2008 -0
  71. pi_durable/testing/types.py +99 -0
  72. pi_durable/tool_search/__init__.py +159 -0
  73. pi_durable/tools/__init__.py +81 -0
  74. pi_durable/tools/_values.py +43 -0
  75. pi_durable/tools/bash.py +250 -0
  76. pi_durable/tools/edit.py +214 -0
  77. pi_durable/tools/edit_diff.py +452 -0
  78. pi_durable/tools/env.py +19 -0
  79. pi_durable/tools/file_mutation_queue.py +68 -0
  80. pi_durable/tools/image.py +248 -0
  81. pi_durable/tools/image_processor.py +77 -0
  82. pi_durable/tools/path_utils.py +111 -0
  83. pi_durable/tools/read.py +506 -0
  84. pi_durable/tools/write.py +71 -0
  85. pi_durable/truncate.py +185 -0
  86. pi_durable/types.py +1297 -0
  87. pi_durable_python-1.3.0.dist-info/METADATA +276 -0
  88. pi_durable_python-1.3.0.dist-info/RECORD +90 -0
  89. pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
  90. 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
+ ]