pi-codemcp 1.2.2 → 1.3.1

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.
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import time
5
+ import uuid
6
+ from collections import OrderedDict
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Literal
9
+
10
+ from .json_types import JSON_VALUE_ADAPTER, JsonValue
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+ DEFAULT_REFINEMENT_ENTRY_LIMIT = 8
16
+ DEFAULT_REFINEMENT_ENTRY_BYTES = 1024 * 1024
17
+ DEFAULT_REFINEMENT_TOTAL_BYTES = 4 * 1024 * 1024
18
+ DEFAULT_REFINEMENT_TTL_SECONDS = 300.0
19
+
20
+
21
+ type ReferenceFailureReason = Literal[
22
+ "expired",
23
+ "evicted",
24
+ "cross_sidecar",
25
+ "unknown",
26
+ ]
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class RetainedResult:
31
+ reference: str
32
+ expires_in_seconds: int
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class _CacheEntry:
37
+ payload: bytes
38
+ expires_at: float
39
+
40
+
41
+ class ResultReferenceError(ValueError):
42
+ def __init__(self, reason: ReferenceFailureReason, reference: str) -> None:
43
+ self.reason = reason
44
+ self.reference = reference
45
+ messages = {
46
+ "expired": "Result reference has expired",
47
+ "evicted": "Result reference was evicted",
48
+ "cross_sidecar": "Result reference belongs to another sidecar",
49
+ "unknown": "Unknown result reference",
50
+ }
51
+ super().__init__(f"{messages[reason]}: {reference}")
52
+
53
+
54
+ class RefinementCache:
55
+ def __init__(
56
+ self,
57
+ *,
58
+ entry_limit: int = DEFAULT_REFINEMENT_ENTRY_LIMIT,
59
+ entry_byte_limit: int = DEFAULT_REFINEMENT_ENTRY_BYTES,
60
+ total_byte_limit: int = DEFAULT_REFINEMENT_TOTAL_BYTES,
61
+ ttl_seconds: float = DEFAULT_REFINEMENT_TTL_SECONDS,
62
+ clock: Callable[[], float] = time.monotonic,
63
+ ) -> None:
64
+ if entry_limit < 1:
65
+ raise ValueError("entry_limit must be positive")
66
+ if entry_byte_limit < 1:
67
+ raise ValueError("entry_byte_limit must be positive")
68
+ if total_byte_limit < entry_byte_limit:
69
+ raise ValueError("total_byte_limit must be at least entry_byte_limit")
70
+ if ttl_seconds <= 0:
71
+ raise ValueError("ttl_seconds must be positive")
72
+ self.entry_limit = entry_limit
73
+ self.entry_byte_limit = entry_byte_limit
74
+ self.total_byte_limit = total_byte_limit
75
+ self.ttl_seconds = ttl_seconds
76
+ self._clock = clock
77
+ self._instance_id = uuid.uuid4().hex[:12]
78
+ self._prefix = f"result_{self._instance_id}_"
79
+ self._entries: OrderedDict[str, _CacheEntry] = OrderedDict()
80
+ self._invalid: OrderedDict[str, ReferenceFailureReason] = OrderedDict()
81
+ self._total_bytes = 0
82
+
83
+ @property
84
+ def entry_count(self) -> int:
85
+ return len(self._entries)
86
+
87
+ @property
88
+ def total_bytes(self) -> int:
89
+ return self._total_bytes
90
+
91
+ def retain(self, value: JsonValue) -> RetainedResult | None:
92
+ payload = JSON_VALUE_ADAPTER.dump_json(value)
93
+ payload_bytes = len(payload)
94
+ if payload_bytes > self.entry_byte_limit or payload_bytes > self.total_byte_limit:
95
+ return None
96
+ now = self._clock()
97
+ self._prune_expired(now)
98
+ while (
99
+ len(self._entries) >= self.entry_limit
100
+ or self._total_bytes + payload_bytes > self.total_byte_limit
101
+ ):
102
+ self._evict_oldest()
103
+ reference = f"{self._prefix}{uuid.uuid4().hex[:16]}"
104
+ self._entries[reference] = _CacheEntry(
105
+ payload=payload,
106
+ expires_at=now + self.ttl_seconds,
107
+ )
108
+ self._total_bytes += payload_bytes
109
+ return RetainedResult(
110
+ reference=reference,
111
+ expires_in_seconds=math.ceil(self.ttl_seconds),
112
+ )
113
+
114
+ def resolve(self, reference: str) -> JsonValue:
115
+ now = self._clock()
116
+ self._prune_expired(now)
117
+ entry = self._entries.get(reference)
118
+ if entry is not None:
119
+ self._entries.move_to_end(reference)
120
+ return JSON_VALUE_ADAPTER.validate_json(entry.payload)
121
+ reason = self._invalid.get(reference)
122
+ if reason is not None:
123
+ raise ResultReferenceError(reason, reference)
124
+ if not reference.startswith(self._prefix):
125
+ raise ResultReferenceError("cross_sidecar", reference)
126
+ raise ResultReferenceError("unknown", reference)
127
+
128
+ def clear(self) -> None:
129
+ self._entries.clear()
130
+ self._invalid.clear()
131
+ self._total_bytes = 0
132
+
133
+ def _prune_expired(self, now: float) -> None:
134
+ expired = [
135
+ reference for reference, entry in self._entries.items() if entry.expires_at <= now
136
+ ]
137
+ for reference in expired:
138
+ self._remove(reference, "expired")
139
+
140
+ def _evict_oldest(self) -> None:
141
+ reference = next(iter(self._entries))
142
+ self._remove(reference, "evicted")
143
+
144
+ def _remove(
145
+ self,
146
+ reference: str,
147
+ reason: Literal["expired", "evicted"],
148
+ ) -> None:
149
+ entry = self._entries.pop(reference)
150
+ self._total_bytes -= len(entry.payload)
151
+ self._invalid[reference] = reason
152
+ tombstone_limit = self.entry_limit * 4
153
+ while len(self._invalid) > tombstone_limit:
154
+ self._invalid.popitem(last=False)
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ INSPECT_JSON_NAME = "inspect_json"
4
+ EXPECT_OBJECT_NAME = "expect_object"
5
+ EXPECT_LIST_NAME = "expect_list"
6
+ EXPECT_STRING_NAME = "expect_string"
7
+ EXPECT_INTEGER_NAME = "expect_integer"
8
+
9
+ SANDBOX_FUNCTION_EXTERNALS = {
10
+ INSPECT_JSON_NAME: "__codemcp_inspect_json",
11
+ EXPECT_OBJECT_NAME: "__codemcp_expect_object",
12
+ EXPECT_LIST_NAME: "__codemcp_expect_list",
13
+ EXPECT_STRING_NAME: "__codemcp_expect_string",
14
+ EXPECT_INTEGER_NAME: "__codemcp_expect_integer",
15
+ }
16
+
17
+ STUB_IMPORTS = "from typing import Literal, Never, NotRequired, TypeAlias, TypedDict"
18
+ JSON_TYPE_STUBS = (
19
+ "JsonScalar: TypeAlias = bool | int | float | str | None",
20
+ 'JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]',
21
+ )
22
+ SANDBOX_CAPABILITY_STUB = (
23
+ "# Prebound SDK/helpers. Supported: import asyncio; asyncio.gather.\n"
24
+ "# Unavailable: collections.Counter, base64, gzip, asyncio.create_task,\n"
25
+ "# __import__. Use dict counts."
26
+ )
27
+ INSPECT_JSON_STUB = (
28
+ "def inspect_json(value: JsonValue, *, "
29
+ "samples: Literal[1, 2, 3] = 2, "
30
+ "max_depth: Literal[1, 2, 3, 4, 5, 6] = 3) -> JsonValue: ..."
31
+ )
32
+ NARROWING_HELPER_STUBS = (
33
+ "def expect_object(value: JsonValue) -> dict[str, JsonValue]: ...",
34
+ "def expect_list(value: JsonValue) -> list[JsonValue]: ...",
35
+ "def expect_string(value: JsonValue) -> str: ...",
36
+ "def expect_integer(value: JsonValue) -> int: ...",
37
+ )
38
+ STUB_PRELUDE = "\n\n".join((
39
+ STUB_IMPORTS,
40
+ *JSON_TYPE_STUBS,
41
+ SANDBOX_CAPABILITY_STUB,
42
+ INSPECT_JSON_STUB,
43
+ *NARROWING_HELPER_STUBS,
44
+ ))