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,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
+ ]