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.
- package/README.md +5 -2
- package/extensions/index.ts +12 -8
- package/package.json +1 -1
- package/sidecar/cli.py +27 -7
- package/sidecar/executor.py +207 -35
- package/sidecar/gateway.py +470 -115
- package/sidecar/refinement_cache.py +154 -0
- package/sidecar/sandbox_api.py +44 -0
- package/sidecar/stats.py +801 -194
- package/sidecar/tool_catalog.py +43 -26
- package/src/chains.ts +53 -13
- package/src/mcp-client.ts +1 -0
- package/src/modal.ts +11 -0
- package/src/prompts.ts +3 -3
- package/src/tools.ts +37 -14
|
@@ -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
|
+
))
|