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
@@ -0,0 +1,88 @@
1
+ """Built-in ``pi.usage``: the ledger of one conversation's own spend.
2
+
3
+ Port of v1.3.0 ``src/harness/usage.ts``. Every built-in writer of an assistant or tool-result entry
4
+ adds that message's usage in the same commit; compaction summarization attempts add theirs without
5
+ an entry. ``Harness.usage()`` sums every conversation's document into the Session total.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping, MutableMapping
11
+ from typing import Any, TypedDict, cast
12
+
13
+ from ..documents import define_doc
14
+ from ..ids import ConversationId
15
+ from ..json import JsonValue, copy_json
16
+ from ..session.transaction import Transaction
17
+
18
+
19
+ class UsageState(TypedDict):
20
+ """Spend keyed by ``provider/modelId`` and by tool name."""
21
+
22
+ models: dict[str, dict[str, JsonValue]]
23
+ tools: dict[str, dict[str, JsonValue]]
24
+
25
+
26
+ UsageDoc = define_doc(
27
+ {
28
+ "kind": "pi.usage",
29
+ "version": 1,
30
+ "scope": "conversation",
31
+ "history": "latest",
32
+ "fork": "initial",
33
+ "initial": lambda: {"models": {}, "tools": {}},
34
+ "checkpoint_when": lambda _state, _ops, _info: True,
35
+ }
36
+ )
37
+
38
+
39
+ async def record_usage(
40
+ tx: Transaction,
41
+ conversation_id: ConversationId,
42
+ bucket: str,
43
+ key: str,
44
+ usage: Mapping[str, Any],
45
+ ) -> None:
46
+ """Add ``usage`` to one bucket of the conversation's ``pi.usage``."""
47
+
48
+ totals = (await tx.doc(UsageDoc, conversation_id))[bucket]
49
+ current = totals.get(key) if hasattr(totals, "get") else None
50
+ if current is None:
51
+ totals[key] = _strict_usage(usage)
52
+ else:
53
+ add_usage(cast(MutableMapping[str, Any], current), usage)
54
+
55
+
56
+ def add_usage(total: MutableMapping[str, Any], usage: Mapping[str, Any]) -> None:
57
+ """Add every counter of ``usage`` to ``total``; optional counters add once either side reports them."""
58
+
59
+ for key in ("input", "output", "cacheRead", "cacheWrite", "totalTokens"):
60
+ total[key] = int(total.get(key, 0)) + int(usage.get(key, 0))
61
+ for key in ("cacheWrite1h", "reasoning"):
62
+ if usage.get(key) is not None:
63
+ total[key] = int(total.get(key, 0)) + int(usage[key])
64
+ if "cost" not in total:
65
+ total["cost"] = {}
66
+ total_cost = cast(MutableMapping[str, Any], total["cost"])
67
+ usage_cost = cast(Mapping[str, Any], usage.get("cost") or {})
68
+ for key in ("input", "output", "cacheRead", "cacheWrite", "total"):
69
+ total_cost[key] = total_cost.get(key, 0) + usage_cost.get(key, 0)
70
+
71
+
72
+ def add_usage_state(sum_value: UsageState, state: Mapping[str, Any]) -> None:
73
+ """Add every bucket of ``state`` into ``sum``."""
74
+
75
+ for bucket in ("models", "tools"):
76
+ destination = sum_value[bucket]
77
+ for key, value in cast(Mapping[str, Any], state.get(bucket) or {}).items():
78
+ if key in destination:
79
+ add_usage(cast(MutableMapping[str, Any], destination[key]), value)
80
+ else:
81
+ destination[key] = cast(dict[str, JsonValue], copy_json(value))
82
+
83
+
84
+ def _strict_usage(usage: Mapping[str, Any]) -> dict[str, JsonValue]:
85
+ return cast(dict[str, JsonValue], copy_json(dict(usage)))
86
+
87
+
88
+ __all__ = ["UsageDoc", "UsageState", "add_usage", "add_usage_state", "record_usage"]
@@ -0,0 +1,204 @@
1
+ """Scheduler support: pending waits, paginated scans, and closed-Harness errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ from collections.abc import Awaitable, Callable, Hashable, Mapping
8
+ from typing import Any, Generic, TypeVar, cast
9
+
10
+ from ..context import AbortError, AbortSignal, Context
11
+ from ..errors import SessionFailed
12
+ from ..json import JsonObject
13
+ from ..types import Cursor, Page, TaskRecord
14
+
15
+ T = TypeVar("T")
16
+ K = TypeVar("K", bound=Hashable)
17
+
18
+
19
+ def cancellation_error(signal: AbortSignal) -> BaseException:
20
+ """Return what an aborted context rejects with: its reason if it is an error, else ``AbortError``."""
21
+
22
+ reason = signal.reason
23
+ if isinstance(reason, BaseException):
24
+ return reason
25
+ return AbortError(reason)
26
+
27
+
28
+ class Waiters(Generic[K, T]):
29
+ """Pending waits by key. Each settles once: through ``resolve``, ``reject_all``, or its cancellation."""
30
+
31
+ def __init__(self) -> None:
32
+ self._sets: dict[K, set[asyncio.Future[T]]] = {}
33
+
34
+ def add(self, key: K, context: Context) -> Awaitable[T]:
35
+ signal = context.abort_signal
36
+ if signal is not None and signal.aborted:
37
+ return rejected(cancellation_error(signal))
38
+ loop = asyncio.get_running_loop()
39
+ waiter: asyncio.Future[T] = loop.create_future()
40
+ waiter.add_done_callback(lambda future: future.exception() if not future.cancelled() else None)
41
+ self._sets.setdefault(key, set()).add(waiter)
42
+ if signal is not None:
43
+
44
+ def cancel() -> None:
45
+ values = self._sets.get(key)
46
+ if values is not None:
47
+ values.discard(waiter)
48
+ if not values and self._sets.get(key) is values:
49
+ del self._sets[key]
50
+ if not waiter.done():
51
+ waiter.set_exception(cancellation_error(signal))
52
+
53
+ remove = signal.add_listener(cancel)
54
+ waiter.add_done_callback(lambda _: remove())
55
+ return waiter
56
+
57
+ def keys(self) -> list[K]:
58
+ return list(self._sets)
59
+
60
+ def resolve(self, key: K, value: T) -> None:
61
+ values = self._sets.pop(key, None)
62
+ for waiter in values or ():
63
+ if not waiter.done():
64
+ waiter.set_result(value)
65
+
66
+ def reject_all(self, error: BaseException) -> None:
67
+ sets = list(self._sets.values())
68
+ self._sets.clear()
69
+ for values in sets:
70
+ for waiter in values:
71
+ if not waiter.done():
72
+ waiter.set_exception(error)
73
+
74
+
75
+ def rejected(error: BaseException) -> asyncio.Future[Any]:
76
+ future: asyncio.Future[Any] = asyncio.get_running_loop().create_future()
77
+ future.set_exception(error)
78
+ future.add_done_callback(lambda f: f.exception() if not f.cancelled() else None)
79
+ return future
80
+
81
+
82
+ def resolved(value: T) -> asyncio.Future[T]:
83
+ future: asyncio.Future[T] = asyncio.get_running_loop().create_future()
84
+ future.set_result(value)
85
+ return future
86
+
87
+
88
+ async def scan_all(scan: Callable[[Cursor | None], Awaitable[Page[Any, Cursor]]]) -> list[Any]:
89
+ """Every item of a paginated scan, in page order."""
90
+
91
+ items: list[Any] = []
92
+ cursor: Cursor | None = None
93
+ while True:
94
+ page = await scan(cursor)
95
+ items.extend(page["items"])
96
+ cursor = page.get("next")
97
+ if cursor is None:
98
+ return items
99
+
100
+
101
+ def closed_error(session: Any) -> BaseException:
102
+ """What a closed Harness rejects with: ``SessionFailed`` when it closed because the Session failed."""
103
+
104
+ failure = session.failure
105
+ return RuntimeError("Harness is closed") if failure is None else SessionFailed(failure)
106
+
107
+
108
+ def call_with_args(handler: Callable[..., Any], *args: Any) -> Any:
109
+ """Call ``handler`` with only the arguments it declares, as JavaScript ignores extras."""
110
+
111
+ try:
112
+ parameters = list(inspect.signature(handler).parameters.values())
113
+ except (TypeError, ValueError):
114
+ return handler(*args)
115
+ if any(parameter.kind is inspect.Parameter.VAR_POSITIONAL for parameter in parameters):
116
+ return handler(*args)
117
+ positional = [
118
+ parameter
119
+ for parameter in parameters
120
+ if parameter.kind
121
+ in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
122
+ ]
123
+ return handler(*args[: len(positional)])
124
+
125
+
126
+ def assign_json(target: Any, key: Any, value: Any) -> None:
127
+ """Assign ``value`` at ``target[key]`` leaf by leaf, so a string leaf becomes an append."""
128
+
129
+ slots = target
130
+ current = slots.get(key) if hasattr(slots, "get") else slots[key]
131
+ if _is_record(current) and _is_record(value):
132
+ for name in list(current.keys()):
133
+ if name not in value:
134
+ del current[name]
135
+ for name, child in value.items():
136
+ assign_json(current, name, child)
137
+ return
138
+ if _is_array(current) and _is_array(value) and len(current) <= len(value):
139
+ for index in range(len(value)):
140
+ if index < len(current):
141
+ assign_json(current, index, value[index])
142
+ else:
143
+ current.append(value[index])
144
+ return
145
+ if current != value:
146
+ slots[key] = value
147
+
148
+
149
+ def _is_record(value: Any) -> bool:
150
+ from ..session.drafts import DraftObject
151
+
152
+ return isinstance(value, (dict, DraftObject))
153
+
154
+
155
+ def _is_array(value: Any) -> bool:
156
+ from ..session.drafts import DraftList
157
+
158
+ return isinstance(value, (list, DraftList))
159
+
160
+
161
+ #: Host-facing settings use PEP 8 names; provider options and persisted checkpoints use the
162
+ #: camelCase pi-ai request shape.
163
+ _STREAM_OPTION_KEYS = {
164
+ "timeout_ms": "timeoutMs",
165
+ "max_retries": "maxRetries",
166
+ "max_retry_delay_ms": "maxRetryDelayMs",
167
+ "cache_retention": "cacheRetention",
168
+ }
169
+
170
+
171
+ def task_checkpoint(task: TaskRecord) -> dict[str, Any]:
172
+ """The checkpoint a task's current phase reads."""
173
+
174
+ return cast(dict[str, Any], task["state"]["checkpoint"])
175
+
176
+
177
+ async def call_hook(handler: Callable[..., Any], *args: Any) -> Any:
178
+ """Call a hook with only the arguments it declares and await a returned awaitable."""
179
+
180
+ result = call_with_args(handler, *args)
181
+ if hasattr(result, "__await__"):
182
+ return await cast(Awaitable[Any], result)
183
+ return result
184
+
185
+
186
+ def request_stream_options(stream: Mapping[str, Any]) -> JsonObject:
187
+ """Map Python-facing stream settings to the camelCase provider request shape."""
188
+
189
+ return cast(JsonObject, {_STREAM_OPTION_KEYS.get(key, key): value for key, value in stream.items()})
190
+
191
+
192
+ __all__ = [
193
+ "Waiters",
194
+ "assign_json",
195
+ "call_hook",
196
+ "call_with_args",
197
+ "cancellation_error",
198
+ "closed_error",
199
+ "rejected",
200
+ "request_stream_options",
201
+ "resolved",
202
+ "scan_all",
203
+ "task_checkpoint",
204
+ ]