pi-codemcp 1.2.1 → 1.3.0
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 +6 -3
- package/extensions/index.ts +67 -77
- package/package.json +1 -1
- package/sidecar/chains.py +17 -44
- package/sidecar/cli.py +27 -7
- package/sidecar/executor.py +207 -35
- package/sidecar/gateway.py +505 -141
- package/sidecar/mcp_config.py +27 -2
- 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 +1 -9
- package/src/chains.ts +63 -34
- package/src/config.ts +8 -39
- package/src/mcp-client.ts +2 -1
- package/src/modal.ts +87 -248
- package/src/prompts.ts +3 -3
- package/src/tools.ts +37 -17
package/sidecar/gateway.py
CHANGED
|
@@ -2,15 +2,17 @@ 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
|
|
11
14
|
from fastmcp import Client, FastMCP
|
|
12
15
|
from fastmcp.mcp_config import RemoteMCPServer, StdioMCPServer
|
|
13
|
-
from pydantic import BaseModel, ConfigDict
|
|
14
16
|
from pydantic_core import to_json
|
|
15
17
|
from rapidfuzz import fuzz, process
|
|
16
18
|
|
|
@@ -18,7 +20,6 @@ from . import json_types
|
|
|
18
20
|
from .catalog_cache import CatalogCache
|
|
19
21
|
from .chains import (
|
|
20
22
|
ChainDependency,
|
|
21
|
-
ChainEnabledChange,
|
|
22
23
|
ChainListResponse,
|
|
23
24
|
ChainScope,
|
|
24
25
|
ChainStatusView,
|
|
@@ -27,7 +28,12 @@ from .chains import (
|
|
|
27
28
|
SavedChainManifest,
|
|
28
29
|
ScopedChainStore,
|
|
29
30
|
)
|
|
30
|
-
from .executor import
|
|
31
|
+
from .executor import (
|
|
32
|
+
ExecutionContext,
|
|
33
|
+
ExecutionFailureInfo,
|
|
34
|
+
ExecutionResponse,
|
|
35
|
+
MontyExecutor,
|
|
36
|
+
)
|
|
31
37
|
from .mcp_config import NormalizedConfig, load_mcp_json, normalize_mcp_config
|
|
32
38
|
from .models import (
|
|
33
39
|
ExecutionLimitsView,
|
|
@@ -42,9 +48,10 @@ from .models import (
|
|
|
42
48
|
UpstreamStatus,
|
|
43
49
|
UpstreamToolStatus,
|
|
44
50
|
)
|
|
51
|
+
from .refinement_cache import RefinementCache, ResultReferenceError
|
|
45
52
|
from .runtime_paths import resolve_runtime_paths
|
|
46
53
|
from .settings import CodeMcpSettings, load_settings
|
|
47
|
-
from .stats import StatsStore
|
|
54
|
+
from .stats import OperationFailure, OperationObservation, StatsStore
|
|
48
55
|
from .tool_catalog import ToolCatalog, schema_path_summary
|
|
49
56
|
|
|
50
57
|
if TYPE_CHECKING:
|
|
@@ -57,18 +64,12 @@ if TYPE_CHECKING:
|
|
|
57
64
|
from .runtime_paths import RuntimePaths
|
|
58
65
|
|
|
59
66
|
MAX_INSPECT_CALLS = 20
|
|
67
|
+
UPSTREAM_FAILURE_MESSAGE_LIMIT = 500
|
|
60
68
|
type ServerConfig = StdioMCPServer | RemoteMCPServer
|
|
61
69
|
type JsonObject = json_types.JsonObject
|
|
62
70
|
type JsonValue = json_types.JsonValue
|
|
63
71
|
|
|
64
72
|
|
|
65
|
-
class ManagerApplyResponse(BaseModel):
|
|
66
|
-
model_config = ConfigDict(extra="forbid", strict=True)
|
|
67
|
-
|
|
68
|
-
status: StatusResponse
|
|
69
|
-
chains: list[ChainStatusView]
|
|
70
|
-
|
|
71
|
-
|
|
72
73
|
class ServerHandle:
|
|
73
74
|
def __init__(
|
|
74
75
|
self,
|
|
@@ -136,7 +137,19 @@ class ServerHandle:
|
|
|
136
137
|
) -> mcp_types.CallToolResult:
|
|
137
138
|
async with self._lock:
|
|
138
139
|
client = await self._connect_locked()
|
|
139
|
-
|
|
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()
|
|
140
153
|
|
|
141
154
|
async def close(self) -> None:
|
|
142
155
|
async with self._lock:
|
|
@@ -180,23 +193,30 @@ class SaveChainHandler(Protocol):
|
|
|
180
193
|
code: str,
|
|
181
194
|
input_schema: JsonObject,
|
|
182
195
|
output_schema: JsonObject,
|
|
196
|
+
trace_id: str,
|
|
183
197
|
) -> SaveChainResponse: ...
|
|
184
198
|
|
|
185
199
|
|
|
186
200
|
class SavedChainHandlers(NamedTuple):
|
|
187
|
-
execute: Callable[[str, JsonObject], Awaitable[ExecutionResponse]]
|
|
201
|
+
execute: Callable[[str, JsonObject, str], Awaitable[ExecutionResponse]]
|
|
188
202
|
save: SaveChainHandler
|
|
189
|
-
list: Callable[[], ChainListResponse]
|
|
190
|
-
|
|
191
|
-
|
|
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]]
|
|
192
207
|
|
|
193
208
|
|
|
194
209
|
class SavedChainRuntime:
|
|
195
210
|
def __init__(self, handlers: SavedChainHandlers) -> None:
|
|
196
211
|
self.handlers = handlers
|
|
197
212
|
|
|
198
|
-
async def execute(
|
|
199
|
-
|
|
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)
|
|
200
220
|
|
|
201
221
|
async def save(
|
|
202
222
|
self,
|
|
@@ -207,6 +227,7 @@ class SavedChainRuntime:
|
|
|
207
227
|
code: str,
|
|
208
228
|
input_schema: JsonObject,
|
|
209
229
|
output_schema: JsonObject,
|
|
230
|
+
trace_id: str,
|
|
210
231
|
) -> SaveChainResponse:
|
|
211
232
|
return await self.handlers.save(
|
|
212
233
|
scope=scope,
|
|
@@ -215,16 +236,36 @@ class SavedChainRuntime:
|
|
|
215
236
|
code=code,
|
|
216
237
|
input_schema=input_schema,
|
|
217
238
|
output_schema=output_schema,
|
|
239
|
+
trace_id=trace_id,
|
|
218
240
|
)
|
|
219
241
|
|
|
220
|
-
def list(self) -> ChainListResponse:
|
|
221
|
-
return self.handlers.list()
|
|
242
|
+
def list(self, trace_id: str) -> ChainListResponse:
|
|
243
|
+
return self.handlers.list(trace_id)
|
|
244
|
+
|
|
245
|
+
async def set_enabled(
|
|
246
|
+
self,
|
|
247
|
+
name: str,
|
|
248
|
+
scope: ChainScope,
|
|
249
|
+
enabled: bool,
|
|
250
|
+
trace_id: str,
|
|
251
|
+
) -> ChainStatusView:
|
|
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(
|
|
@@ -256,6 +301,7 @@ class GatewayRuntime:
|
|
|
256
301
|
execute=self._execute_chain,
|
|
257
302
|
save=self._save_chain,
|
|
258
303
|
list=self._list_chains,
|
|
304
|
+
set_enabled=self._set_chain_enabled,
|
|
259
305
|
revalidate=self._revalidate_chain,
|
|
260
306
|
delete=self._delete_chain,
|
|
261
307
|
)
|
|
@@ -314,6 +360,7 @@ class GatewayRuntime:
|
|
|
314
360
|
)
|
|
315
361
|
|
|
316
362
|
async def close(self) -> None:
|
|
363
|
+
self.refinement_cache.clear()
|
|
317
364
|
await asyncio.gather(
|
|
318
365
|
*(handle.close() for handle in self.handles.values()),
|
|
319
366
|
return_exceptions=True,
|
|
@@ -328,6 +375,8 @@ class GatewayRuntime:
|
|
|
328
375
|
detail: SearchDetail = "signatures",
|
|
329
376
|
mode: SearchMode = "search",
|
|
330
377
|
cursor: int = 0,
|
|
378
|
+
*,
|
|
379
|
+
trace_id: str,
|
|
331
380
|
) -> SearchResponse:
|
|
332
381
|
started = time.perf_counter()
|
|
333
382
|
input_bytes = len(
|
|
@@ -345,18 +394,26 @@ class GatewayRuntime:
|
|
|
345
394
|
except BaseException:
|
|
346
395
|
self.stats_store.record_operation(
|
|
347
396
|
"search",
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
),
|
|
352
407
|
)
|
|
353
408
|
raise
|
|
354
409
|
self.stats_store.record_operation(
|
|
355
410
|
"search",
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
),
|
|
360
417
|
)
|
|
361
418
|
return response
|
|
362
419
|
|
|
@@ -449,25 +506,37 @@ class GatewayRuntime:
|
|
|
449
506
|
results=results,
|
|
450
507
|
)
|
|
451
508
|
|
|
452
|
-
async def inspect(
|
|
509
|
+
async def inspect(
|
|
510
|
+
self,
|
|
511
|
+
calls: list[str],
|
|
512
|
+
trace_id: str,
|
|
513
|
+
) -> InspectResponse:
|
|
453
514
|
started = time.perf_counter()
|
|
454
515
|
try:
|
|
455
516
|
response = await self._inspect_impl(calls)
|
|
456
517
|
except BaseException:
|
|
457
518
|
self.stats_store.record_operation(
|
|
458
519
|
"inspect",
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
),
|
|
463
530
|
)
|
|
464
531
|
raise
|
|
465
532
|
self.stats_store.record_operation(
|
|
466
533
|
"inspect",
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
),
|
|
471
540
|
)
|
|
472
541
|
return response
|
|
473
542
|
|
|
@@ -520,9 +589,37 @@ class GatewayRuntime:
|
|
|
520
589
|
result_limit_bytes=settings.result_byte_limit,
|
|
521
590
|
)
|
|
522
591
|
|
|
523
|
-
async def execute(
|
|
592
|
+
async def execute(
|
|
593
|
+
self,
|
|
594
|
+
code: str,
|
|
595
|
+
trace_id: str,
|
|
596
|
+
input_ref: str | None = None,
|
|
597
|
+
) -> ExecutionResponse:
|
|
524
598
|
started = time.perf_counter()
|
|
525
|
-
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
|
|
526
623
|
discovery_started = time.perf_counter()
|
|
527
624
|
try:
|
|
528
625
|
await self._ensure_servers_discovered(self._required_servers_for_code(code))
|
|
@@ -533,26 +630,39 @@ class GatewayRuntime:
|
|
|
533
630
|
started,
|
|
534
631
|
input_bytes,
|
|
535
632
|
failure_stage="discovery",
|
|
633
|
+
failure_subtype="discovery",
|
|
634
|
+
trace_id=trace_id,
|
|
536
635
|
)
|
|
537
636
|
raise
|
|
538
637
|
self.stats_store.record_phase("discovery", _elapsed_ms(discovery_started))
|
|
539
638
|
self.executor.update_catalog(self.catalog)
|
|
540
639
|
try:
|
|
541
|
-
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
|
+
)
|
|
542
646
|
except BaseException as error:
|
|
647
|
+
cancelled = isinstance(error, asyncio.CancelledError)
|
|
543
648
|
self._record_operation_exception(
|
|
544
649
|
"execute",
|
|
545
650
|
started,
|
|
546
651
|
input_bytes,
|
|
547
|
-
failure_stage=
|
|
548
|
-
|
|
549
|
-
|
|
652
|
+
failure_stage="cancelled" if cancelled else "error",
|
|
653
|
+
failure_subtype="cancelled" if cancelled else "internal_error",
|
|
654
|
+
trace_id=trace_id,
|
|
550
655
|
)
|
|
551
656
|
raise
|
|
552
|
-
self._record_execution("execute", started, input_bytes, response)
|
|
657
|
+
self._record_execution("execute", started, input_bytes, response, trace_id)
|
|
553
658
|
return response
|
|
554
659
|
|
|
555
|
-
async def _execute_chain(
|
|
660
|
+
async def _execute_chain(
|
|
661
|
+
self,
|
|
662
|
+
name: str,
|
|
663
|
+
arguments: JsonObject,
|
|
664
|
+
trace_id: str,
|
|
665
|
+
) -> ExecutionResponse:
|
|
556
666
|
started = time.perf_counter()
|
|
557
667
|
input_bytes = len(to_json(arguments))
|
|
558
668
|
try:
|
|
@@ -563,19 +673,27 @@ class GatewayRuntime:
|
|
|
563
673
|
started,
|
|
564
674
|
input_bytes,
|
|
565
675
|
failure_stage="preflight",
|
|
676
|
+
failure_subtype="saved_chain_lookup",
|
|
677
|
+
trace_id=trace_id,
|
|
566
678
|
)
|
|
567
679
|
raise
|
|
568
680
|
if not chain.enabled:
|
|
569
|
-
|
|
570
|
-
|
|
681
|
+
message = f"Saved chain is disabled: {name}"
|
|
682
|
+
response = ExecutionResponse.failed(
|
|
571
683
|
failure_stage="preflight",
|
|
572
|
-
error=
|
|
684
|
+
error=message,
|
|
685
|
+
failure=ExecutionFailureInfo(
|
|
686
|
+
kind="preflight",
|
|
687
|
+
retryable=False,
|
|
688
|
+
message=message,
|
|
689
|
+
),
|
|
573
690
|
)
|
|
574
691
|
self._record_execution(
|
|
575
692
|
"execute_chain",
|
|
576
693
|
started,
|
|
577
694
|
input_bytes,
|
|
578
695
|
response,
|
|
696
|
+
trace_id,
|
|
579
697
|
)
|
|
580
698
|
return response
|
|
581
699
|
discovery_started = time.perf_counter()
|
|
@@ -588,20 +706,28 @@ class GatewayRuntime:
|
|
|
588
706
|
started,
|
|
589
707
|
input_bytes,
|
|
590
708
|
failure_stage="discovery",
|
|
709
|
+
failure_subtype="discovery",
|
|
710
|
+
trace_id=trace_id,
|
|
591
711
|
)
|
|
592
712
|
raise
|
|
593
713
|
self.stats_store.record_phase("discovery", _elapsed_ms(discovery_started))
|
|
594
714
|
self.executor.update_catalog(self.catalog)
|
|
595
715
|
try:
|
|
596
|
-
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
|
+
)
|
|
597
722
|
except BaseException as error:
|
|
723
|
+
cancelled = isinstance(error, asyncio.CancelledError)
|
|
598
724
|
self._record_operation_exception(
|
|
599
725
|
"execute_chain",
|
|
600
726
|
started,
|
|
601
727
|
input_bytes,
|
|
602
|
-
failure_stage=
|
|
603
|
-
|
|
604
|
-
|
|
728
|
+
failure_stage="cancelled" if cancelled else "error",
|
|
729
|
+
failure_subtype="cancelled" if cancelled else "internal_error",
|
|
730
|
+
trace_id=trace_id,
|
|
605
731
|
)
|
|
606
732
|
raise
|
|
607
733
|
self._record_execution(
|
|
@@ -609,6 +735,7 @@ class GatewayRuntime:
|
|
|
609
735
|
started,
|
|
610
736
|
input_bytes,
|
|
611
737
|
response,
|
|
738
|
+
trace_id,
|
|
612
739
|
)
|
|
613
740
|
return response
|
|
614
741
|
|
|
@@ -619,13 +746,21 @@ class GatewayRuntime:
|
|
|
619
746
|
input_bytes: int,
|
|
620
747
|
*,
|
|
621
748
|
failure_stage: str,
|
|
749
|
+
failure_subtype: str,
|
|
750
|
+
trace_id: str,
|
|
622
751
|
) -> None:
|
|
623
752
|
self.stats_store.record_operation(
|
|
624
753
|
operation,
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
+
),
|
|
629
764
|
)
|
|
630
765
|
|
|
631
766
|
def _record_execution(
|
|
@@ -634,16 +769,28 @@ class GatewayRuntime:
|
|
|
634
769
|
started: float,
|
|
635
770
|
input_bytes: int,
|
|
636
771
|
response: ExecutionResponse,
|
|
772
|
+
trace_id: str,
|
|
637
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
|
+
)
|
|
638
783
|
self.stats_store.record_operation(
|
|
639
784
|
operation,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
),
|
|
647
794
|
)
|
|
648
795
|
metrics = response.metrics
|
|
649
796
|
for phase, duration in (
|
|
@@ -663,6 +810,7 @@ class GatewayRuntime:
|
|
|
663
810
|
code: str,
|
|
664
811
|
input_schema: JsonObject,
|
|
665
812
|
output_schema: JsonObject,
|
|
813
|
+
trace_id: str,
|
|
666
814
|
) -> SaveChainResponse:
|
|
667
815
|
started = time.perf_counter()
|
|
668
816
|
input_bytes = len(
|
|
@@ -687,18 +835,26 @@ class GatewayRuntime:
|
|
|
687
835
|
except BaseException:
|
|
688
836
|
self.stats_store.record_operation(
|
|
689
837
|
"save_chain",
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
+
),
|
|
694
848
|
)
|
|
695
849
|
raise
|
|
696
850
|
self.stats_store.record_operation(
|
|
697
851
|
"save_chain",
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
+
),
|
|
702
858
|
)
|
|
703
859
|
return response
|
|
704
860
|
|
|
@@ -767,34 +923,100 @@ class GatewayRuntime:
|
|
|
767
923
|
created=previous is None,
|
|
768
924
|
)
|
|
769
925
|
|
|
770
|
-
def _list_chains(self) -> ChainListResponse:
|
|
926
|
+
def _list_chains(self, trace_id: str) -> ChainListResponse:
|
|
771
927
|
started = time.perf_counter()
|
|
772
|
-
|
|
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
|
|
773
944
|
self.stats_store.record_operation(
|
|
774
945
|
"list_chains",
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
946
|
+
OperationObservation(
|
|
947
|
+
duration_ms=_elapsed_ms(started),
|
|
948
|
+
success=True,
|
|
949
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
950
|
+
),
|
|
951
|
+
)
|
|
952
|
+
return response
|
|
953
|
+
|
|
954
|
+
async def _set_chain_enabled(
|
|
955
|
+
self,
|
|
956
|
+
name: str,
|
|
957
|
+
scope: ChainScope,
|
|
958
|
+
enabled: bool,
|
|
959
|
+
trace_id: str,
|
|
960
|
+
) -> ChainStatusView:
|
|
961
|
+
started = time.perf_counter()
|
|
962
|
+
try:
|
|
963
|
+
self.chain_store.set_enabled(scope, name, enabled)
|
|
964
|
+
await self._rebuild_catalog()
|
|
965
|
+
response = self._chain_view(name, scope)
|
|
966
|
+
except BaseException:
|
|
967
|
+
self.stats_store.record_operation(
|
|
968
|
+
"set_chain_enabled",
|
|
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
|
+
),
|
|
978
|
+
)
|
|
979
|
+
raise
|
|
980
|
+
self.stats_store.record_operation(
|
|
981
|
+
"set_chain_enabled",
|
|
982
|
+
OperationObservation(
|
|
983
|
+
duration_ms=_elapsed_ms(started),
|
|
984
|
+
success=True,
|
|
985
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
986
|
+
),
|
|
778
987
|
)
|
|
779
988
|
return response
|
|
780
989
|
|
|
781
|
-
async def _revalidate_chain(
|
|
990
|
+
async def _revalidate_chain(
|
|
991
|
+
self,
|
|
992
|
+
name: str,
|
|
993
|
+
scope: ChainScope,
|
|
994
|
+
trace_id: str,
|
|
995
|
+
) -> ChainStatusView:
|
|
782
996
|
started = time.perf_counter()
|
|
783
997
|
try:
|
|
784
998
|
response = await self._revalidate_chain_impl(name, scope)
|
|
785
999
|
except BaseException:
|
|
786
1000
|
self.stats_store.record_operation(
|
|
787
1001
|
"revalidate_chain",
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
+
),
|
|
791
1011
|
)
|
|
792
1012
|
raise
|
|
793
1013
|
self.stats_store.record_operation(
|
|
794
1014
|
"revalidate_chain",
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1015
|
+
OperationObservation(
|
|
1016
|
+
duration_ms=_elapsed_ms(started),
|
|
1017
|
+
success=True,
|
|
1018
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
1019
|
+
),
|
|
798
1020
|
)
|
|
799
1021
|
return response
|
|
800
1022
|
|
|
@@ -836,27 +1058,45 @@ class GatewayRuntime:
|
|
|
836
1058
|
await self._rebuild_catalog()
|
|
837
1059
|
return self._chain_view(name, scope)
|
|
838
1060
|
|
|
839
|
-
async def _delete_chain(
|
|
1061
|
+
async def _delete_chain(
|
|
1062
|
+
self,
|
|
1063
|
+
name: str,
|
|
1064
|
+
scope: ChainScope,
|
|
1065
|
+
trace_id: str,
|
|
1066
|
+
) -> ChainListResponse:
|
|
840
1067
|
started = time.perf_counter()
|
|
841
1068
|
try:
|
|
842
|
-
response = await self._delete_chain_impl(name, scope)
|
|
1069
|
+
response = await self._delete_chain_impl(name, scope, trace_id)
|
|
843
1070
|
except BaseException:
|
|
844
1071
|
self.stats_store.record_operation(
|
|
845
1072
|
"delete_chain",
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
+
),
|
|
849
1082
|
)
|
|
850
1083
|
raise
|
|
851
1084
|
self.stats_store.record_operation(
|
|
852
1085
|
"delete_chain",
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1086
|
+
OperationObservation(
|
|
1087
|
+
duration_ms=_elapsed_ms(started),
|
|
1088
|
+
success=True,
|
|
1089
|
+
output_bytes=len(to_json(response.model_dump(mode="json"))),
|
|
1090
|
+
),
|
|
856
1091
|
)
|
|
857
1092
|
return response
|
|
858
1093
|
|
|
859
|
-
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:
|
|
860
1100
|
effective = self.chain_store.get(name)
|
|
861
1101
|
called_by = self._called_by().get(name, []) if effective.scope == scope else []
|
|
862
1102
|
if called_by:
|
|
@@ -865,7 +1105,7 @@ class GatewayRuntime:
|
|
|
865
1105
|
)
|
|
866
1106
|
self.chain_store.delete(scope, name)
|
|
867
1107
|
await self._rebuild_catalog()
|
|
868
|
-
return self._list_chains()
|
|
1108
|
+
return self._list_chains(trace_id)
|
|
869
1109
|
|
|
870
1110
|
async def _dispatch(
|
|
871
1111
|
self,
|
|
@@ -891,7 +1131,22 @@ class GatewayRuntime:
|
|
|
891
1131
|
),
|
|
892
1132
|
)
|
|
893
1133
|
normalized = context.catalog.normalize_result(public_name, result)
|
|
894
|
-
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
|
+
)
|
|
895
1150
|
self.stats_store.record_upstream(
|
|
896
1151
|
spec.server,
|
|
897
1152
|
spec.backend_name,
|
|
@@ -924,27 +1179,6 @@ class GatewayRuntime:
|
|
|
924
1179
|
await self._rebuild_catalog()
|
|
925
1180
|
return self.status()
|
|
926
1181
|
|
|
927
|
-
async def apply_manager_changes(
|
|
928
|
-
self,
|
|
929
|
-
changes: list[ChainEnabledChange],
|
|
930
|
-
) -> ManagerApplyResponse:
|
|
931
|
-
previous_settings = self.settings
|
|
932
|
-
try:
|
|
933
|
-
with self.chain_store.enabled_transaction(changes):
|
|
934
|
-
self._load_settings()
|
|
935
|
-
await self._rebuild_catalog()
|
|
936
|
-
except BaseException:
|
|
937
|
-
self.settings = previous_settings
|
|
938
|
-
for handle in self.handles.values():
|
|
939
|
-
handle.cache.max_age_seconds = previous_settings.cache_ttl_seconds
|
|
940
|
-
self.executor.settings = previous_settings.execution_settings()
|
|
941
|
-
await self._rebuild_catalog()
|
|
942
|
-
raise
|
|
943
|
-
return ManagerApplyResponse(
|
|
944
|
-
status=self.status(),
|
|
945
|
-
chains=self._chain_views(),
|
|
946
|
-
)
|
|
947
|
-
|
|
948
1182
|
def _load_settings(self) -> None:
|
|
949
1183
|
self.settings = load_settings(self.settings_path)
|
|
950
1184
|
for handle in self.handles.values():
|
|
@@ -1206,6 +1440,104 @@ def _elapsed_ms(started: float) -> float:
|
|
|
1206
1440
|
return (time.perf_counter() - started) * 1_000
|
|
1207
1441
|
|
|
1208
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
|
+
|
|
1209
1541
|
def _discovery_error_message(error: Exception) -> str:
|
|
1210
1542
|
return str(error).strip() or type(error).__name__
|
|
1211
1543
|
|
|
@@ -1286,6 +1618,8 @@ mcp = FastMCP(
|
|
|
1286
1618
|
|
|
1287
1619
|
@mcp.tool
|
|
1288
1620
|
async def search(
|
|
1621
|
+
trace_id: str,
|
|
1622
|
+
*,
|
|
1289
1623
|
query: str | None = None,
|
|
1290
1624
|
limit: int = 5,
|
|
1291
1625
|
server: str | None = None,
|
|
@@ -1294,13 +1628,21 @@ async def search(
|
|
|
1294
1628
|
cursor: int = 0,
|
|
1295
1629
|
) -> SearchResponse:
|
|
1296
1630
|
"""Search or page through configured upstream MCP tools and saved chains."""
|
|
1297
|
-
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
|
+
)
|
|
1298
1640
|
|
|
1299
1641
|
|
|
1300
1642
|
@mcp.tool
|
|
1301
|
-
async def inspect(calls: list[str]) -> InspectResponse:
|
|
1643
|
+
async def inspect(trace_id: str, calls: list[str]) -> InspectResponse:
|
|
1302
1644
|
"""Return exact typed SDK stubs for selected call identifiers."""
|
|
1303
|
-
return await _require_runtime().inspect(calls)
|
|
1645
|
+
return await _require_runtime().inspect(calls, trace_id)
|
|
1304
1646
|
|
|
1305
1647
|
|
|
1306
1648
|
@mcp.tool
|
|
@@ -1316,21 +1658,30 @@ async def reload_settings() -> StatusResponse:
|
|
|
1316
1658
|
|
|
1317
1659
|
|
|
1318
1660
|
@mcp.tool
|
|
1319
|
-
async def
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1661
|
+
async def set_chain_enabled(
|
|
1662
|
+
trace_id: str,
|
|
1663
|
+
name: str,
|
|
1664
|
+
scope: ChainScope,
|
|
1665
|
+
enabled: bool,
|
|
1666
|
+
) -> ChainStatusView:
|
|
1667
|
+
"""Enable or disable one saved chain in its storage scope."""
|
|
1668
|
+
return await _require_runtime().chains.set_enabled(name, scope, enabled, trace_id)
|
|
1324
1669
|
|
|
1325
1670
|
|
|
1326
1671
|
@mcp.tool
|
|
1327
|
-
async def execute(
|
|
1672
|
+
async def execute(
|
|
1673
|
+
trace_id: str,
|
|
1674
|
+
code: str,
|
|
1675
|
+
input_ref: str | None = None,
|
|
1676
|
+
) -> ExecutionResponse:
|
|
1328
1677
|
"""Type-check and run one sandboxed Python MCP SDK chain."""
|
|
1329
|
-
return await _require_runtime().execute(code)
|
|
1678
|
+
return await _require_runtime().execute(code, trace_id, input_ref)
|
|
1330
1679
|
|
|
1331
1680
|
|
|
1332
1681
|
@mcp.tool
|
|
1333
1682
|
async def save_chain(
|
|
1683
|
+
trace_id: str,
|
|
1684
|
+
*,
|
|
1334
1685
|
name: str,
|
|
1335
1686
|
description: str,
|
|
1336
1687
|
code: str,
|
|
@@ -1346,38 +1697,51 @@ async def save_chain(
|
|
|
1346
1697
|
code=code,
|
|
1347
1698
|
input_schema=json_types.JSON_OBJECT_ADAPTER.validate_python(input_schema),
|
|
1348
1699
|
output_schema=json_types.JSON_OBJECT_ADAPTER.validate_python(output_schema),
|
|
1700
|
+
trace_id=trace_id,
|
|
1349
1701
|
)
|
|
1350
1702
|
|
|
1351
1703
|
|
|
1352
1704
|
@mcp.tool
|
|
1353
|
-
def list_chains() -> ChainListResponse:
|
|
1705
|
+
def list_chains(trace_id: str) -> ChainListResponse:
|
|
1354
1706
|
"""List saved chains and their dependency state."""
|
|
1355
|
-
return _require_runtime().chains.list()
|
|
1707
|
+
return _require_runtime().chains.list(trace_id)
|
|
1356
1708
|
|
|
1357
1709
|
|
|
1358
1710
|
@mcp.tool
|
|
1359
|
-
async def execute_chain(
|
|
1711
|
+
async def execute_chain(
|
|
1712
|
+
trace_id: str,
|
|
1713
|
+
name: str,
|
|
1714
|
+
arguments: JsonObject,
|
|
1715
|
+
) -> ExecutionResponse:
|
|
1360
1716
|
"""Execute one saved chain through its typed input contract."""
|
|
1361
1717
|
validated_arguments = json_types.JSON_OBJECT_ADAPTER.validate_python(arguments)
|
|
1362
|
-
return await _require_runtime().chains.execute(name, validated_arguments)
|
|
1718
|
+
return await _require_runtime().chains.execute(name, validated_arguments, trace_id)
|
|
1363
1719
|
|
|
1364
1720
|
|
|
1365
1721
|
@mcp.tool
|
|
1366
|
-
async def revalidate_chain(
|
|
1722
|
+
async def revalidate_chain(
|
|
1723
|
+
trace_id: str,
|
|
1724
|
+
name: str,
|
|
1725
|
+
scope: ChainScope,
|
|
1726
|
+
) -> ChainStatusView:
|
|
1367
1727
|
"""Revalidate one scoped saved chain against the current callable catalog."""
|
|
1368
|
-
return await _require_runtime().chains.revalidate(name, scope)
|
|
1728
|
+
return await _require_runtime().chains.revalidate(name, scope, trace_id)
|
|
1369
1729
|
|
|
1370
1730
|
|
|
1371
1731
|
@mcp.tool
|
|
1372
|
-
async def delete_chain(
|
|
1732
|
+
async def delete_chain(
|
|
1733
|
+
trace_id: str,
|
|
1734
|
+
name: str,
|
|
1735
|
+
scope: ChainScope,
|
|
1736
|
+
) -> ChainListResponse:
|
|
1373
1737
|
"""Delete an unused saved chain from its storage scope."""
|
|
1374
|
-
return await _require_runtime().chains.delete(name, scope)
|
|
1738
|
+
return await _require_runtime().chains.delete(name, scope, trace_id)
|
|
1375
1739
|
|
|
1376
1740
|
|
|
1377
1741
|
@mcp.tool
|
|
1378
|
-
def stats() -> JsonObject:
|
|
1742
|
+
async def stats() -> JsonObject:
|
|
1379
1743
|
"""Return bounded local CodeMCP telemetry rollups."""
|
|
1380
|
-
return _require_runtime().stats_store.snapshot()
|
|
1744
|
+
return await _require_runtime().stats_store.snapshot()
|
|
1381
1745
|
|
|
1382
1746
|
|
|
1383
1747
|
@mcp.tool
|