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
package/sidecar/gateway.py
CHANGED
|
@@ -2,9 +2,12 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
5
7
|
import textwrap
|
|
6
8
|
import time
|
|
7
|
-
|
|
9
|
+
import uuid
|
|
10
|
+
from contextlib import AsyncExitStack, asynccontextmanager, suppress
|
|
8
11
|
from typing import TYPE_CHECKING, Literal, NamedTuple, Protocol, cast
|
|
9
12
|
|
|
10
13
|
import pydantic_monty
|
|
@@ -25,7 +28,12 @@ from .chains import (
|
|
|
25
28
|
SavedChainManifest,
|
|
26
29
|
ScopedChainStore,
|
|
27
30
|
)
|
|
28
|
-
from .executor import
|
|
31
|
+
from .executor import (
|
|
32
|
+
ExecutionContext,
|
|
33
|
+
ExecutionFailureInfo,
|
|
34
|
+
ExecutionResponse,
|
|
35
|
+
MontyExecutor,
|
|
36
|
+
)
|
|
29
37
|
from .mcp_config import NormalizedConfig, load_mcp_json, normalize_mcp_config
|
|
30
38
|
from .models import (
|
|
31
39
|
ExecutionLimitsView,
|
|
@@ -40,9 +48,10 @@ from .models import (
|
|
|
40
48
|
UpstreamStatus,
|
|
41
49
|
UpstreamToolStatus,
|
|
42
50
|
)
|
|
51
|
+
from .refinement_cache import RefinementCache, ResultReferenceError
|
|
43
52
|
from .runtime_paths import resolve_runtime_paths
|
|
44
53
|
from .settings import CodeMcpSettings, load_settings
|
|
45
|
-
from .stats import StatsStore
|
|
54
|
+
from .stats import OperationFailure, OperationObservation, StatsStore
|
|
46
55
|
from .tool_catalog import ToolCatalog, schema_path_summary
|
|
47
56
|
|
|
48
57
|
if TYPE_CHECKING:
|
|
@@ -55,6 +64,7 @@ if TYPE_CHECKING:
|
|
|
55
64
|
from .runtime_paths import RuntimePaths
|
|
56
65
|
|
|
57
66
|
MAX_INSPECT_CALLS = 20
|
|
67
|
+
UPSTREAM_FAILURE_MESSAGE_LIMIT = 500
|
|
58
68
|
type ServerConfig = StdioMCPServer | RemoteMCPServer
|
|
59
69
|
type JsonObject = json_types.JsonObject
|
|
60
70
|
type JsonValue = json_types.JsonValue
|
|
@@ -127,7 +137,19 @@ class ServerHandle:
|
|
|
127
137
|
) -> mcp_types.CallToolResult:
|
|
128
138
|
async with self._lock:
|
|
129
139
|
client = await self._connect_locked()
|
|
130
|
-
|
|
140
|
+
try:
|
|
141
|
+
return await client.call_tool_mcp(name, arguments, timeout=timeout_seconds)
|
|
142
|
+
except Exception as error:
|
|
143
|
+
if is_unusable_connection_error(error):
|
|
144
|
+
await self._discard_client(client)
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
async def _discard_client(self, client: Client[ClientTransport]) -> None:
|
|
148
|
+
async with self._lock:
|
|
149
|
+
if self._client is not client:
|
|
150
|
+
return
|
|
151
|
+
with suppress(Exception):
|
|
152
|
+
await self._disconnect_locked()
|
|
131
153
|
|
|
132
154
|
async def close(self) -> None:
|
|
133
155
|
async with self._lock:
|
|
@@ -171,24 +193,30 @@ class SaveChainHandler(Protocol):
|
|
|
171
193
|
code: str,
|
|
172
194
|
input_schema: JsonObject,
|
|
173
195
|
output_schema: JsonObject,
|
|
196
|
+
trace_id: str,
|
|
174
197
|
) -> SaveChainResponse: ...
|
|
175
198
|
|
|
176
199
|
|
|
177
200
|
class SavedChainHandlers(NamedTuple):
|
|
178
|
-
execute: Callable[[str, JsonObject], Awaitable[ExecutionResponse]]
|
|
201
|
+
execute: Callable[[str, JsonObject, str], Awaitable[ExecutionResponse]]
|
|
179
202
|
save: SaveChainHandler
|
|
180
|
-
list: Callable[[], ChainListResponse]
|
|
181
|
-
set_enabled: Callable[[str, ChainScope, bool], Awaitable[ChainStatusView]]
|
|
182
|
-
revalidate: Callable[[str, ChainScope], Awaitable[ChainStatusView]]
|
|
183
|
-
delete: Callable[[str, ChainScope], Awaitable[ChainListResponse]]
|
|
203
|
+
list: Callable[[str], ChainListResponse]
|
|
204
|
+
set_enabled: Callable[[str, ChainScope, bool, str], Awaitable[ChainStatusView]]
|
|
205
|
+
revalidate: Callable[[str, ChainScope, str], Awaitable[ChainStatusView]]
|
|
206
|
+
delete: Callable[[str, ChainScope, str], Awaitable[ChainListResponse]]
|
|
184
207
|
|
|
185
208
|
|
|
186
209
|
class SavedChainRuntime:
|
|
187
210
|
def __init__(self, handlers: SavedChainHandlers) -> None:
|
|
188
211
|
self.handlers = handlers
|
|
189
212
|
|
|
190
|
-
async def execute(
|
|
191
|
-
|
|
213
|
+
async def execute(
|
|
214
|
+
self,
|
|
215
|
+
name: str,
|
|
216
|
+
arguments: JsonObject,
|
|
217
|
+
trace_id: str,
|
|
218
|
+
) -> ExecutionResponse:
|
|
219
|
+
return await self.handlers.execute(name, arguments, trace_id)
|
|
192
220
|
|
|
193
221
|
async def save(
|
|
194
222
|
self,
|
|
@@ -199,6 +227,7 @@ class SavedChainRuntime:
|
|
|
199
227
|
code: str,
|
|
200
228
|
input_schema: JsonObject,
|
|
201
229
|
output_schema: JsonObject,
|
|
230
|
+
trace_id: str,
|
|
202
231
|
) -> SaveChainResponse:
|
|
203
232
|
return await self.handlers.save(
|
|
204
233
|
scope=scope,
|
|
@@ -207,24 +236,36 @@ class SavedChainRuntime:
|
|
|
207
236
|
code=code,
|
|
208
237
|
input_schema=input_schema,
|
|
209
238
|
output_schema=output_schema,
|
|
239
|
+
trace_id=trace_id,
|
|
210
240
|
)
|
|
211
241
|
|
|
212
|
-
def list(self) -> ChainListResponse:
|
|
213
|
-
return self.handlers.list()
|
|
242
|
+
def list(self, trace_id: str) -> ChainListResponse:
|
|
243
|
+
return self.handlers.list(trace_id)
|
|
214
244
|
|
|
215
245
|
async def set_enabled(
|
|
216
246
|
self,
|
|
217
247
|
name: str,
|
|
218
248
|
scope: ChainScope,
|
|
219
249
|
enabled: bool,
|
|
250
|
+
trace_id: str,
|
|
220
251
|
) -> ChainStatusView:
|
|
221
|
-
return await self.handlers.set_enabled(name, scope, enabled)
|
|
252
|
+
return await self.handlers.set_enabled(name, scope, enabled, trace_id)
|
|
222
253
|
|
|
223
|
-
async def revalidate(
|
|
224
|
-
|
|
254
|
+
async def revalidate(
|
|
255
|
+
self,
|
|
256
|
+
name: str,
|
|
257
|
+
scope: ChainScope,
|
|
258
|
+
trace_id: str,
|
|
259
|
+
) -> ChainStatusView:
|
|
260
|
+
return await self.handlers.revalidate(name, scope, trace_id)
|
|
225
261
|
|
|
226
|
-
async def delete(
|
|
227
|
-
|
|
262
|
+
async def delete(
|
|
263
|
+
self,
|
|
264
|
+
name: str,
|
|
265
|
+
scope: ChainScope,
|
|
266
|
+
trace_id: str,
|
|
267
|
+
) -> ChainListResponse:
|
|
268
|
+
return await self.handlers.delete(name, scope, trace_id)
|
|
228
269
|
|
|
229
270
|
|
|
230
271
|
class GatewayRuntime:
|
|
@@ -248,7 +289,11 @@ class GatewayRuntime:
|
|
|
248
289
|
self.chain_store = chain_store
|
|
249
290
|
self.catalog = catalog
|
|
250
291
|
self.executor = executor
|
|
251
|
-
self.
|
|
292
|
+
self.refinement_cache = RefinementCache()
|
|
293
|
+
self.stats_store = StatsStore(
|
|
294
|
+
settings_path.parent / "stats.sqlite3",
|
|
295
|
+
package_version=os.environ.get("PI_CODEMCP_PACKAGE_VERSION", "unknown"),
|
|
296
|
+
)
|
|
252
297
|
for handle in handles.values():
|
|
253
298
|
self.stats_store.record_cache(hit=handle.tools is not None)
|
|
254
299
|
self.chains = SavedChainRuntime(
|
|
@@ -315,6 +360,7 @@ class GatewayRuntime:
|
|
|
315
360
|
)
|
|
316
361
|
|
|
317
362
|
async def close(self) -> None:
|
|
363
|
+
self.refinement_cache.clear()
|
|
318
364
|
await asyncio.gather(
|
|
319
365
|
*(handle.close() for handle in self.handles.values()),
|
|
320
366
|
return_exceptions=True,
|
|
@@ -329,6 +375,8 @@ class GatewayRuntime:
|
|
|
329
375
|
detail: SearchDetail = "signatures",
|
|
330
376
|
mode: SearchMode = "search",
|
|
331
377
|
cursor: int = 0,
|
|
378
|
+
*,
|
|
379
|
+
trace_id: str,
|
|
332
380
|
) -> SearchResponse:
|
|
333
381
|
started = time.perf_counter()
|
|
334
382
|
input_bytes = len(
|
|
@@ -346,18 +394,26 @@ class GatewayRuntime:
|
|
|
346
394
|
except BaseException:
|
|
347
395
|
self.stats_store.record_operation(
|
|
348
396
|
"search",
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
397
|
+
OperationObservation(
|
|
398
|
+
duration_ms=_elapsed_ms(started),
|
|
399
|
+
success=False,
|
|
400
|
+
input_bytes=input_bytes,
|
|
401
|
+
failure=OperationFailure(
|
|
402
|
+
stage="error",
|
|
403
|
+
subtype="search_error",
|
|
404
|
+
trace_id=trace_id,
|
|
405
|
+
),
|
|
406
|
+
),
|
|
353
407
|
)
|
|
354
408
|
raise
|
|
355
409
|
self.stats_store.record_operation(
|
|
356
410
|
"search",
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
411
|
+
OperationObservation(
|
|
412
|
+
duration_ms=_elapsed_ms(started),
|
|
413
|
+
success=True,
|
|
414
|
+
input_bytes=input_bytes,
|
|
415
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
416
|
+
),
|
|
361
417
|
)
|
|
362
418
|
return response
|
|
363
419
|
|
|
@@ -450,25 +506,37 @@ class GatewayRuntime:
|
|
|
450
506
|
results=results,
|
|
451
507
|
)
|
|
452
508
|
|
|
453
|
-
async def inspect(
|
|
509
|
+
async def inspect(
|
|
510
|
+
self,
|
|
511
|
+
calls: list[str],
|
|
512
|
+
trace_id: str,
|
|
513
|
+
) -> InspectResponse:
|
|
454
514
|
started = time.perf_counter()
|
|
455
515
|
try:
|
|
456
516
|
response = await self._inspect_impl(calls)
|
|
457
517
|
except BaseException:
|
|
458
518
|
self.stats_store.record_operation(
|
|
459
519
|
"inspect",
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
520
|
+
OperationObservation(
|
|
521
|
+
duration_ms=_elapsed_ms(started),
|
|
522
|
+
success=False,
|
|
523
|
+
input_bytes=len(to_json(calls)),
|
|
524
|
+
failure=OperationFailure(
|
|
525
|
+
stage="error",
|
|
526
|
+
subtype="inspect_error",
|
|
527
|
+
trace_id=trace_id,
|
|
528
|
+
),
|
|
529
|
+
),
|
|
464
530
|
)
|
|
465
531
|
raise
|
|
466
532
|
self.stats_store.record_operation(
|
|
467
533
|
"inspect",
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
534
|
+
OperationObservation(
|
|
535
|
+
duration_ms=_elapsed_ms(started),
|
|
536
|
+
success=True,
|
|
537
|
+
input_bytes=len(to_json(calls)),
|
|
538
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
539
|
+
),
|
|
472
540
|
)
|
|
473
541
|
return response
|
|
474
542
|
|
|
@@ -521,9 +589,37 @@ class GatewayRuntime:
|
|
|
521
589
|
result_limit_bytes=settings.result_byte_limit,
|
|
522
590
|
)
|
|
523
591
|
|
|
524
|
-
async def execute(
|
|
592
|
+
async def execute(
|
|
593
|
+
self,
|
|
594
|
+
code: str,
|
|
595
|
+
trace_id: str,
|
|
596
|
+
input_ref: str | None = None,
|
|
597
|
+
) -> ExecutionResponse:
|
|
525
598
|
started = time.perf_counter()
|
|
526
|
-
input_bytes = len(code.encode())
|
|
599
|
+
input_bytes = len(code.encode()) + len((input_ref or "").encode())
|
|
600
|
+
input_value: JsonValue = None
|
|
601
|
+
if input_ref is not None:
|
|
602
|
+
try:
|
|
603
|
+
input_value = self.refinement_cache.resolve(input_ref)
|
|
604
|
+
except ResultReferenceError as error:
|
|
605
|
+
message = str(error)
|
|
606
|
+
response = ExecutionResponse.failed(
|
|
607
|
+
failure_stage="preflight",
|
|
608
|
+
error=message,
|
|
609
|
+
failure=ExecutionFailureInfo(
|
|
610
|
+
kind="result_reference",
|
|
611
|
+
retryable=False,
|
|
612
|
+
message=message,
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
self._record_execution(
|
|
616
|
+
"execute",
|
|
617
|
+
started,
|
|
618
|
+
input_bytes,
|
|
619
|
+
response,
|
|
620
|
+
trace_id,
|
|
621
|
+
)
|
|
622
|
+
return response
|
|
527
623
|
discovery_started = time.perf_counter()
|
|
528
624
|
try:
|
|
529
625
|
await self._ensure_servers_discovered(self._required_servers_for_code(code))
|
|
@@ -534,26 +630,39 @@ class GatewayRuntime:
|
|
|
534
630
|
started,
|
|
535
631
|
input_bytes,
|
|
536
632
|
failure_stage="discovery",
|
|
633
|
+
failure_subtype="discovery",
|
|
634
|
+
trace_id=trace_id,
|
|
537
635
|
)
|
|
538
636
|
raise
|
|
539
637
|
self.stats_store.record_phase("discovery", _elapsed_ms(discovery_started))
|
|
540
638
|
self.executor.update_catalog(self.catalog)
|
|
541
639
|
try:
|
|
542
|
-
response = await self.executor.execute_graph(
|
|
640
|
+
response = await self.executor.execute_graph(
|
|
641
|
+
code,
|
|
642
|
+
self._dispatch,
|
|
643
|
+
input_value=input_value,
|
|
644
|
+
retain_result=self.refinement_cache.retain,
|
|
645
|
+
)
|
|
543
646
|
except BaseException as error:
|
|
647
|
+
cancelled = isinstance(error, asyncio.CancelledError)
|
|
544
648
|
self._record_operation_exception(
|
|
545
649
|
"execute",
|
|
546
650
|
started,
|
|
547
651
|
input_bytes,
|
|
548
|
-
failure_stage=
|
|
549
|
-
|
|
550
|
-
|
|
652
|
+
failure_stage="cancelled" if cancelled else "error",
|
|
653
|
+
failure_subtype="cancelled" if cancelled else "internal_error",
|
|
654
|
+
trace_id=trace_id,
|
|
551
655
|
)
|
|
552
656
|
raise
|
|
553
|
-
self._record_execution("execute", started, input_bytes, response)
|
|
657
|
+
self._record_execution("execute", started, input_bytes, response, trace_id)
|
|
554
658
|
return response
|
|
555
659
|
|
|
556
|
-
async def _execute_chain(
|
|
660
|
+
async def _execute_chain(
|
|
661
|
+
self,
|
|
662
|
+
name: str,
|
|
663
|
+
arguments: JsonObject,
|
|
664
|
+
trace_id: str,
|
|
665
|
+
) -> ExecutionResponse:
|
|
557
666
|
started = time.perf_counter()
|
|
558
667
|
input_bytes = len(to_json(arguments))
|
|
559
668
|
try:
|
|
@@ -564,19 +673,27 @@ class GatewayRuntime:
|
|
|
564
673
|
started,
|
|
565
674
|
input_bytes,
|
|
566
675
|
failure_stage="preflight",
|
|
676
|
+
failure_subtype="saved_chain_lookup",
|
|
677
|
+
trace_id=trace_id,
|
|
567
678
|
)
|
|
568
679
|
raise
|
|
569
680
|
if not chain.enabled:
|
|
570
|
-
|
|
571
|
-
|
|
681
|
+
message = f"Saved chain is disabled: {name}"
|
|
682
|
+
response = ExecutionResponse.failed(
|
|
572
683
|
failure_stage="preflight",
|
|
573
|
-
error=
|
|
684
|
+
error=message,
|
|
685
|
+
failure=ExecutionFailureInfo(
|
|
686
|
+
kind="preflight",
|
|
687
|
+
retryable=False,
|
|
688
|
+
message=message,
|
|
689
|
+
),
|
|
574
690
|
)
|
|
575
691
|
self._record_execution(
|
|
576
692
|
"execute_chain",
|
|
577
693
|
started,
|
|
578
694
|
input_bytes,
|
|
579
695
|
response,
|
|
696
|
+
trace_id,
|
|
580
697
|
)
|
|
581
698
|
return response
|
|
582
699
|
discovery_started = time.perf_counter()
|
|
@@ -589,20 +706,28 @@ class GatewayRuntime:
|
|
|
589
706
|
started,
|
|
590
707
|
input_bytes,
|
|
591
708
|
failure_stage="discovery",
|
|
709
|
+
failure_subtype="discovery",
|
|
710
|
+
trace_id=trace_id,
|
|
592
711
|
)
|
|
593
712
|
raise
|
|
594
713
|
self.stats_store.record_phase("discovery", _elapsed_ms(discovery_started))
|
|
595
714
|
self.executor.update_catalog(self.catalog)
|
|
596
715
|
try:
|
|
597
|
-
response = await self.executor.execute_saved_chain(
|
|
716
|
+
response = await self.executor.execute_saved_chain(
|
|
717
|
+
chain,
|
|
718
|
+
arguments,
|
|
719
|
+
self._dispatch,
|
|
720
|
+
retain_result=self.refinement_cache.retain,
|
|
721
|
+
)
|
|
598
722
|
except BaseException as error:
|
|
723
|
+
cancelled = isinstance(error, asyncio.CancelledError)
|
|
599
724
|
self._record_operation_exception(
|
|
600
725
|
"execute_chain",
|
|
601
726
|
started,
|
|
602
727
|
input_bytes,
|
|
603
|
-
failure_stage=
|
|
604
|
-
|
|
605
|
-
|
|
728
|
+
failure_stage="cancelled" if cancelled else "error",
|
|
729
|
+
failure_subtype="cancelled" if cancelled else "internal_error",
|
|
730
|
+
trace_id=trace_id,
|
|
606
731
|
)
|
|
607
732
|
raise
|
|
608
733
|
self._record_execution(
|
|
@@ -610,6 +735,7 @@ class GatewayRuntime:
|
|
|
610
735
|
started,
|
|
611
736
|
input_bytes,
|
|
612
737
|
response,
|
|
738
|
+
trace_id,
|
|
613
739
|
)
|
|
614
740
|
return response
|
|
615
741
|
|
|
@@ -620,13 +746,21 @@ class GatewayRuntime:
|
|
|
620
746
|
input_bytes: int,
|
|
621
747
|
*,
|
|
622
748
|
failure_stage: str,
|
|
749
|
+
failure_subtype: str,
|
|
750
|
+
trace_id: str,
|
|
623
751
|
) -> None:
|
|
624
752
|
self.stats_store.record_operation(
|
|
625
753
|
operation,
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
754
|
+
OperationObservation(
|
|
755
|
+
duration_ms=_elapsed_ms(started),
|
|
756
|
+
success=False,
|
|
757
|
+
input_bytes=input_bytes,
|
|
758
|
+
failure=OperationFailure(
|
|
759
|
+
stage=failure_stage,
|
|
760
|
+
subtype=failure_subtype,
|
|
761
|
+
trace_id=trace_id,
|
|
762
|
+
),
|
|
763
|
+
),
|
|
630
764
|
)
|
|
631
765
|
|
|
632
766
|
def _record_execution(
|
|
@@ -635,16 +769,28 @@ class GatewayRuntime:
|
|
|
635
769
|
started: float,
|
|
636
770
|
input_bytes: int,
|
|
637
771
|
response: ExecutionResponse,
|
|
772
|
+
trace_id: str,
|
|
638
773
|
) -> None:
|
|
774
|
+
failure = (
|
|
775
|
+
OperationFailure(
|
|
776
|
+
stage=response.failure_stage or "error",
|
|
777
|
+
subtype=_execution_failure_subtype(response),
|
|
778
|
+
trace_id=trace_id,
|
|
779
|
+
)
|
|
780
|
+
if not response.ok
|
|
781
|
+
else None
|
|
782
|
+
)
|
|
639
783
|
self.stats_store.record_operation(
|
|
640
784
|
operation,
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
785
|
+
OperationObservation(
|
|
786
|
+
duration_ms=_elapsed_ms(started),
|
|
787
|
+
success=response.ok,
|
|
788
|
+
input_bytes=input_bytes,
|
|
789
|
+
output_bytes=response.metrics.result_bytes,
|
|
790
|
+
calls=response.calls_made,
|
|
791
|
+
chain_calls=response.chain_calls,
|
|
792
|
+
failure=failure,
|
|
793
|
+
),
|
|
648
794
|
)
|
|
649
795
|
metrics = response.metrics
|
|
650
796
|
for phase, duration in (
|
|
@@ -664,6 +810,7 @@ class GatewayRuntime:
|
|
|
664
810
|
code: str,
|
|
665
811
|
input_schema: JsonObject,
|
|
666
812
|
output_schema: JsonObject,
|
|
813
|
+
trace_id: str,
|
|
667
814
|
) -> SaveChainResponse:
|
|
668
815
|
started = time.perf_counter()
|
|
669
816
|
input_bytes = len(
|
|
@@ -688,18 +835,26 @@ class GatewayRuntime:
|
|
|
688
835
|
except BaseException:
|
|
689
836
|
self.stats_store.record_operation(
|
|
690
837
|
"save_chain",
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
838
|
+
OperationObservation(
|
|
839
|
+
duration_ms=_elapsed_ms(started),
|
|
840
|
+
success=False,
|
|
841
|
+
input_bytes=input_bytes,
|
|
842
|
+
failure=OperationFailure(
|
|
843
|
+
stage="validation",
|
|
844
|
+
subtype="saved_chain_validation",
|
|
845
|
+
trace_id=trace_id,
|
|
846
|
+
),
|
|
847
|
+
),
|
|
695
848
|
)
|
|
696
849
|
raise
|
|
697
850
|
self.stats_store.record_operation(
|
|
698
851
|
"save_chain",
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
852
|
+
OperationObservation(
|
|
853
|
+
duration_ms=_elapsed_ms(started),
|
|
854
|
+
success=True,
|
|
855
|
+
input_bytes=input_bytes,
|
|
856
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
857
|
+
),
|
|
703
858
|
)
|
|
704
859
|
return response
|
|
705
860
|
|
|
@@ -768,14 +923,31 @@ class GatewayRuntime:
|
|
|
768
923
|
created=previous is None,
|
|
769
924
|
)
|
|
770
925
|
|
|
771
|
-
def _list_chains(self) -> ChainListResponse:
|
|
926
|
+
def _list_chains(self, trace_id: str) -> ChainListResponse:
|
|
772
927
|
started = time.perf_counter()
|
|
773
|
-
|
|
928
|
+
try:
|
|
929
|
+
response = ChainListResponse(chains=self._chain_views())
|
|
930
|
+
except BaseException:
|
|
931
|
+
self.stats_store.record_operation(
|
|
932
|
+
"list_chains",
|
|
933
|
+
OperationObservation(
|
|
934
|
+
duration_ms=_elapsed_ms(started),
|
|
935
|
+
success=False,
|
|
936
|
+
failure=OperationFailure(
|
|
937
|
+
stage="error",
|
|
938
|
+
subtype="saved_chain_list",
|
|
939
|
+
trace_id=trace_id,
|
|
940
|
+
),
|
|
941
|
+
),
|
|
942
|
+
)
|
|
943
|
+
raise
|
|
774
944
|
self.stats_store.record_operation(
|
|
775
945
|
"list_chains",
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
946
|
+
OperationObservation(
|
|
947
|
+
duration_ms=_elapsed_ms(started),
|
|
948
|
+
success=True,
|
|
949
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
950
|
+
),
|
|
779
951
|
)
|
|
780
952
|
return response
|
|
781
953
|
|
|
@@ -784,6 +956,7 @@ class GatewayRuntime:
|
|
|
784
956
|
name: str,
|
|
785
957
|
scope: ChainScope,
|
|
786
958
|
enabled: bool,
|
|
959
|
+
trace_id: str,
|
|
787
960
|
) -> ChainStatusView:
|
|
788
961
|
started = time.perf_counter()
|
|
789
962
|
try:
|
|
@@ -793,36 +966,57 @@ class GatewayRuntime:
|
|
|
793
966
|
except BaseException:
|
|
794
967
|
self.stats_store.record_operation(
|
|
795
968
|
"set_chain_enabled",
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
969
|
+
OperationObservation(
|
|
970
|
+
duration_ms=_elapsed_ms(started),
|
|
971
|
+
success=False,
|
|
972
|
+
failure=OperationFailure(
|
|
973
|
+
stage="error",
|
|
974
|
+
subtype="saved_chain_update",
|
|
975
|
+
trace_id=trace_id,
|
|
976
|
+
),
|
|
977
|
+
),
|
|
799
978
|
)
|
|
800
979
|
raise
|
|
801
980
|
self.stats_store.record_operation(
|
|
802
981
|
"set_chain_enabled",
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
982
|
+
OperationObservation(
|
|
983
|
+
duration_ms=_elapsed_ms(started),
|
|
984
|
+
success=True,
|
|
985
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
986
|
+
),
|
|
806
987
|
)
|
|
807
988
|
return response
|
|
808
989
|
|
|
809
|
-
async def _revalidate_chain(
|
|
990
|
+
async def _revalidate_chain(
|
|
991
|
+
self,
|
|
992
|
+
name: str,
|
|
993
|
+
scope: ChainScope,
|
|
994
|
+
trace_id: str,
|
|
995
|
+
) -> ChainStatusView:
|
|
810
996
|
started = time.perf_counter()
|
|
811
997
|
try:
|
|
812
998
|
response = await self._revalidate_chain_impl(name, scope)
|
|
813
999
|
except BaseException:
|
|
814
1000
|
self.stats_store.record_operation(
|
|
815
1001
|
"revalidate_chain",
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1002
|
+
OperationObservation(
|
|
1003
|
+
duration_ms=_elapsed_ms(started),
|
|
1004
|
+
success=False,
|
|
1005
|
+
failure=OperationFailure(
|
|
1006
|
+
stage="validation",
|
|
1007
|
+
subtype="saved_chain_validation",
|
|
1008
|
+
trace_id=trace_id,
|
|
1009
|
+
),
|
|
1010
|
+
),
|
|
819
1011
|
)
|
|
820
1012
|
raise
|
|
821
1013
|
self.stats_store.record_operation(
|
|
822
1014
|
"revalidate_chain",
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1015
|
+
OperationObservation(
|
|
1016
|
+
duration_ms=_elapsed_ms(started),
|
|
1017
|
+
success=True,
|
|
1018
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
1019
|
+
),
|
|
826
1020
|
)
|
|
827
1021
|
return response
|
|
828
1022
|
|
|
@@ -864,27 +1058,45 @@ class GatewayRuntime:
|
|
|
864
1058
|
await self._rebuild_catalog()
|
|
865
1059
|
return self._chain_view(name, scope)
|
|
866
1060
|
|
|
867
|
-
async def _delete_chain(
|
|
1061
|
+
async def _delete_chain(
|
|
1062
|
+
self,
|
|
1063
|
+
name: str,
|
|
1064
|
+
scope: ChainScope,
|
|
1065
|
+
trace_id: str,
|
|
1066
|
+
) -> ChainListResponse:
|
|
868
1067
|
started = time.perf_counter()
|
|
869
1068
|
try:
|
|
870
|
-
response = await self._delete_chain_impl(name, scope)
|
|
1069
|
+
response = await self._delete_chain_impl(name, scope, trace_id)
|
|
871
1070
|
except BaseException:
|
|
872
1071
|
self.stats_store.record_operation(
|
|
873
1072
|
"delete_chain",
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1073
|
+
OperationObservation(
|
|
1074
|
+
duration_ms=_elapsed_ms(started),
|
|
1075
|
+
success=False,
|
|
1076
|
+
failure=OperationFailure(
|
|
1077
|
+
stage="error",
|
|
1078
|
+
subtype="saved_chain_delete",
|
|
1079
|
+
trace_id=trace_id,
|
|
1080
|
+
),
|
|
1081
|
+
),
|
|
877
1082
|
)
|
|
878
1083
|
raise
|
|
879
1084
|
self.stats_store.record_operation(
|
|
880
1085
|
"delete_chain",
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1086
|
+
OperationObservation(
|
|
1087
|
+
duration_ms=_elapsed_ms(started),
|
|
1088
|
+
success=True,
|
|
1089
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
1090
|
+
),
|
|
884
1091
|
)
|
|
885
1092
|
return response
|
|
886
1093
|
|
|
887
|
-
async def _delete_chain_impl(
|
|
1094
|
+
async def _delete_chain_impl(
|
|
1095
|
+
self,
|
|
1096
|
+
name: str,
|
|
1097
|
+
scope: ChainScope,
|
|
1098
|
+
trace_id: str,
|
|
1099
|
+
) -> ChainListResponse:
|
|
888
1100
|
effective = self.chain_store.get(name)
|
|
889
1101
|
called_by = self._called_by().get(name, []) if effective.scope == scope else []
|
|
890
1102
|
if called_by:
|
|
@@ -893,7 +1105,7 @@ class GatewayRuntime:
|
|
|
893
1105
|
)
|
|
894
1106
|
self.chain_store.delete(scope, name)
|
|
895
1107
|
await self._rebuild_catalog()
|
|
896
|
-
return self._list_chains()
|
|
1108
|
+
return self._list_chains(trace_id)
|
|
897
1109
|
|
|
898
1110
|
async def _dispatch(
|
|
899
1111
|
self,
|
|
@@ -919,7 +1131,22 @@ class GatewayRuntime:
|
|
|
919
1131
|
),
|
|
920
1132
|
)
|
|
921
1133
|
normalized = context.catalog.normalize_result(public_name, result)
|
|
922
|
-
except BaseException:
|
|
1134
|
+
except BaseException as error:
|
|
1135
|
+
if isinstance(error, Exception) and context.failure is None:
|
|
1136
|
+
status = _upstream_status(error)
|
|
1137
|
+
transport_failure = is_unusable_connection_error(error)
|
|
1138
|
+
context.failure = ExecutionFailureInfo(
|
|
1139
|
+
kind="upstream_transport" if transport_failure else "upstream",
|
|
1140
|
+
server=spec.server,
|
|
1141
|
+
tool=spec.backend_name,
|
|
1142
|
+
retryable=transport_failure or _retryable_status(status),
|
|
1143
|
+
status=status,
|
|
1144
|
+
message=(
|
|
1145
|
+
"Upstream connection closed"
|
|
1146
|
+
if transport_failure
|
|
1147
|
+
else _upstream_failure_message(error)
|
|
1148
|
+
),
|
|
1149
|
+
)
|
|
923
1150
|
self.stats_store.record_upstream(
|
|
924
1151
|
spec.server,
|
|
925
1152
|
spec.backend_name,
|
|
@@ -1213,6 +1440,104 @@ def _elapsed_ms(started: float) -> float:
|
|
|
1213
1440
|
return (time.perf_counter() - started) * 1_000
|
|
1214
1441
|
|
|
1215
1442
|
|
|
1443
|
+
def new_trace_id(source: str) -> str:
|
|
1444
|
+
return f"{source}:{uuid.uuid4()}"
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def _exception_messages(error: BaseException) -> list[str]:
|
|
1448
|
+
messages: list[str] = []
|
|
1449
|
+
pending = [error]
|
|
1450
|
+
seen: set[int] = set()
|
|
1451
|
+
while pending:
|
|
1452
|
+
current = pending.pop()
|
|
1453
|
+
identity = id(current)
|
|
1454
|
+
if identity in seen:
|
|
1455
|
+
continue
|
|
1456
|
+
seen.add(identity)
|
|
1457
|
+
rendered = str(current)
|
|
1458
|
+
messages.append(
|
|
1459
|
+
f"{type(current).__name__}: {rendered}" if rendered else type(current).__name__
|
|
1460
|
+
)
|
|
1461
|
+
if current.__cause__ is not None:
|
|
1462
|
+
pending.append(current.__cause__)
|
|
1463
|
+
if current.__context__ is not None:
|
|
1464
|
+
pending.append(current.__context__)
|
|
1465
|
+
if isinstance(current, BaseExceptionGroup):
|
|
1466
|
+
pending.extend(current.exceptions)
|
|
1467
|
+
return messages
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def is_unusable_connection_error(error: BaseException) -> bool:
|
|
1471
|
+
markers = (
|
|
1472
|
+
"connection closed",
|
|
1473
|
+
"connection reset",
|
|
1474
|
+
"broken pipe",
|
|
1475
|
+
"brokenresourceerror",
|
|
1476
|
+
"closedresourceerror",
|
|
1477
|
+
"endofstream",
|
|
1478
|
+
"client is not connected",
|
|
1479
|
+
"session terminated",
|
|
1480
|
+
"transport is closed",
|
|
1481
|
+
)
|
|
1482
|
+
return any(
|
|
1483
|
+
marker in message.lower() for message in _exception_messages(error) for marker in markers
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
def _upstream_status(error: BaseException) -> int | None:
|
|
1488
|
+
for message in _exception_messages(error):
|
|
1489
|
+
match = re.search(
|
|
1490
|
+
r"(?i)\b(?:http(?: status)?|status(?: code)?)[\"'\s:=]+([1-5]\d{2})\b",
|
|
1491
|
+
message,
|
|
1492
|
+
)
|
|
1493
|
+
if match is not None:
|
|
1494
|
+
return int(match.group(1))
|
|
1495
|
+
return None
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
def _retryable_status(status: int | None) -> bool:
|
|
1499
|
+
return status in {408, 409, 425, 429, 500, 502, 503, 504}
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
def _upstream_failure_message(error: BaseException) -> str:
|
|
1503
|
+
message = next((item.strip() for item in _exception_messages(error) if item.strip()), "")
|
|
1504
|
+
compact = " ".join((message or type(error).__name__).split())
|
|
1505
|
+
if len(compact) <= UPSTREAM_FAILURE_MESSAGE_LIMIT:
|
|
1506
|
+
return compact
|
|
1507
|
+
return f"{compact[: UPSTREAM_FAILURE_MESSAGE_LIMIT - 1].rstrip()}…"
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
def _execution_failure_subtype(response: ExecutionResponse) -> str:
|
|
1511
|
+
failure = response.failure
|
|
1512
|
+
if failure is not None and failure.kind in {
|
|
1513
|
+
"result_reference",
|
|
1514
|
+
"sandbox_timeout",
|
|
1515
|
+
"upstream",
|
|
1516
|
+
"upstream_transport",
|
|
1517
|
+
"upstream_timeout",
|
|
1518
|
+
}:
|
|
1519
|
+
return failure.kind
|
|
1520
|
+
stage = response.failure_stage or "error"
|
|
1521
|
+
error = response.error or ""
|
|
1522
|
+
if stage == "preflight":
|
|
1523
|
+
return "preflight_typecheck"
|
|
1524
|
+
if stage == "result" and error.startswith("Returned value is "):
|
|
1525
|
+
return "result_too_large"
|
|
1526
|
+
if stage == "result":
|
|
1527
|
+
return "result_validation"
|
|
1528
|
+
if stage == "timeout":
|
|
1529
|
+
return "upstream_timeout"
|
|
1530
|
+
if stage == "cancelled":
|
|
1531
|
+
return "cancelled"
|
|
1532
|
+
if "Connection closed" in error:
|
|
1533
|
+
return "upstream_transport"
|
|
1534
|
+
if error.startswith("ValueError: inspect_json"):
|
|
1535
|
+
return "sandbox_contract"
|
|
1536
|
+
if stage == "runtime":
|
|
1537
|
+
return "upstream_runtime"
|
|
1538
|
+
return "internal_error"
|
|
1539
|
+
|
|
1540
|
+
|
|
1216
1541
|
def _discovery_error_message(error: Exception) -> str:
|
|
1217
1542
|
return str(error).strip() or type(error).__name__
|
|
1218
1543
|
|
|
@@ -1293,6 +1618,8 @@ mcp = FastMCP(
|
|
|
1293
1618
|
|
|
1294
1619
|
@mcp.tool
|
|
1295
1620
|
async def search(
|
|
1621
|
+
trace_id: str,
|
|
1622
|
+
*,
|
|
1296
1623
|
query: str | None = None,
|
|
1297
1624
|
limit: int = 5,
|
|
1298
1625
|
server: str | None = None,
|
|
@@ -1301,13 +1628,21 @@ async def search(
|
|
|
1301
1628
|
cursor: int = 0,
|
|
1302
1629
|
) -> SearchResponse:
|
|
1303
1630
|
"""Search or page through configured upstream MCP tools and saved chains."""
|
|
1304
|
-
return await _require_runtime().search(
|
|
1631
|
+
return await _require_runtime().search(
|
|
1632
|
+
query,
|
|
1633
|
+
limit,
|
|
1634
|
+
server,
|
|
1635
|
+
detail,
|
|
1636
|
+
mode,
|
|
1637
|
+
cursor,
|
|
1638
|
+
trace_id=trace_id,
|
|
1639
|
+
)
|
|
1305
1640
|
|
|
1306
1641
|
|
|
1307
1642
|
@mcp.tool
|
|
1308
|
-
async def inspect(calls: list[str]) -> InspectResponse:
|
|
1643
|
+
async def inspect(trace_id: str, calls: list[str]) -> InspectResponse:
|
|
1309
1644
|
"""Return exact typed SDK stubs for selected call identifiers."""
|
|
1310
|
-
return await _require_runtime().inspect(calls)
|
|
1645
|
+
return await _require_runtime().inspect(calls, trace_id)
|
|
1311
1646
|
|
|
1312
1647
|
|
|
1313
1648
|
@mcp.tool
|
|
@@ -1324,22 +1659,29 @@ async def reload_settings() -> StatusResponse:
|
|
|
1324
1659
|
|
|
1325
1660
|
@mcp.tool
|
|
1326
1661
|
async def set_chain_enabled(
|
|
1662
|
+
trace_id: str,
|
|
1327
1663
|
name: str,
|
|
1328
1664
|
scope: ChainScope,
|
|
1329
1665
|
enabled: bool,
|
|
1330
1666
|
) -> ChainStatusView:
|
|
1331
1667
|
"""Enable or disable one saved chain in its storage scope."""
|
|
1332
|
-
return await _require_runtime().chains.set_enabled(name, scope, enabled)
|
|
1668
|
+
return await _require_runtime().chains.set_enabled(name, scope, enabled, trace_id)
|
|
1333
1669
|
|
|
1334
1670
|
|
|
1335
1671
|
@mcp.tool
|
|
1336
|
-
async def execute(
|
|
1672
|
+
async def execute(
|
|
1673
|
+
trace_id: str,
|
|
1674
|
+
code: str,
|
|
1675
|
+
input_ref: str | None = None,
|
|
1676
|
+
) -> ExecutionResponse:
|
|
1337
1677
|
"""Type-check and run one sandboxed Python MCP SDK chain."""
|
|
1338
|
-
return await _require_runtime().execute(code)
|
|
1678
|
+
return await _require_runtime().execute(code, trace_id, input_ref)
|
|
1339
1679
|
|
|
1340
1680
|
|
|
1341
1681
|
@mcp.tool
|
|
1342
1682
|
async def save_chain(
|
|
1683
|
+
trace_id: str,
|
|
1684
|
+
*,
|
|
1343
1685
|
name: str,
|
|
1344
1686
|
description: str,
|
|
1345
1687
|
code: str,
|
|
@@ -1355,38 +1697,51 @@ async def save_chain(
|
|
|
1355
1697
|
code=code,
|
|
1356
1698
|
input_schema=json_types.JSON_OBJECT_ADAPTER.validate_python(input_schema),
|
|
1357
1699
|
output_schema=json_types.JSON_OBJECT_ADAPTER.validate_python(output_schema),
|
|
1700
|
+
trace_id=trace_id,
|
|
1358
1701
|
)
|
|
1359
1702
|
|
|
1360
1703
|
|
|
1361
1704
|
@mcp.tool
|
|
1362
|
-
def list_chains() -> ChainListResponse:
|
|
1705
|
+
def list_chains(trace_id: str) -> ChainListResponse:
|
|
1363
1706
|
"""List saved chains and their dependency state."""
|
|
1364
|
-
return _require_runtime().chains.list()
|
|
1707
|
+
return _require_runtime().chains.list(trace_id)
|
|
1365
1708
|
|
|
1366
1709
|
|
|
1367
1710
|
@mcp.tool
|
|
1368
|
-
async def execute_chain(
|
|
1711
|
+
async def execute_chain(
|
|
1712
|
+
trace_id: str,
|
|
1713
|
+
name: str,
|
|
1714
|
+
arguments: JsonObject,
|
|
1715
|
+
) -> ExecutionResponse:
|
|
1369
1716
|
"""Execute one saved chain through its typed input contract."""
|
|
1370
1717
|
validated_arguments = json_types.JSON_OBJECT_ADAPTER.validate_python(arguments)
|
|
1371
|
-
return await _require_runtime().chains.execute(name, validated_arguments)
|
|
1718
|
+
return await _require_runtime().chains.execute(name, validated_arguments, trace_id)
|
|
1372
1719
|
|
|
1373
1720
|
|
|
1374
1721
|
@mcp.tool
|
|
1375
|
-
async def revalidate_chain(
|
|
1722
|
+
async def revalidate_chain(
|
|
1723
|
+
trace_id: str,
|
|
1724
|
+
name: str,
|
|
1725
|
+
scope: ChainScope,
|
|
1726
|
+
) -> ChainStatusView:
|
|
1376
1727
|
"""Revalidate one scoped saved chain against the current callable catalog."""
|
|
1377
|
-
return await _require_runtime().chains.revalidate(name, scope)
|
|
1728
|
+
return await _require_runtime().chains.revalidate(name, scope, trace_id)
|
|
1378
1729
|
|
|
1379
1730
|
|
|
1380
1731
|
@mcp.tool
|
|
1381
|
-
async def delete_chain(
|
|
1732
|
+
async def delete_chain(
|
|
1733
|
+
trace_id: str,
|
|
1734
|
+
name: str,
|
|
1735
|
+
scope: ChainScope,
|
|
1736
|
+
) -> ChainListResponse:
|
|
1382
1737
|
"""Delete an unused saved chain from its storage scope."""
|
|
1383
|
-
return await _require_runtime().chains.delete(name, scope)
|
|
1738
|
+
return await _require_runtime().chains.delete(name, scope, trace_id)
|
|
1384
1739
|
|
|
1385
1740
|
|
|
1386
1741
|
@mcp.tool
|
|
1387
|
-
def stats() -> JsonObject:
|
|
1742
|
+
async def stats() -> JsonObject:
|
|
1388
1743
|
"""Return bounded local CodeMCP telemetry rollups."""
|
|
1389
|
-
return _require_runtime().stats_store.snapshot()
|
|
1744
|
+
return await _require_runtime().stats_store.snapshot()
|
|
1390
1745
|
|
|
1391
1746
|
|
|
1392
1747
|
@mcp.tool
|