cisternal 0.1.0a1__tar.gz
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.
- cisternal-0.1.0a1/LICENSE +21 -0
- cisternal-0.1.0a1/PKG-INFO +109 -0
- cisternal-0.1.0a1/README.md +83 -0
- cisternal-0.1.0a1/pyproject.toml +76 -0
- cisternal-0.1.0a1/setup.cfg +4 -0
- cisternal-0.1.0a1/src/cisternal/__init__.py +150 -0
- cisternal-0.1.0a1/src/cisternal/adapters/__init__.py +1 -0
- cisternal-0.1.0a1/src/cisternal/adapters/base.py +221 -0
- cisternal-0.1.0a1/src/cisternal/adapters/cli.py +85 -0
- cisternal-0.1.0a1/src/cisternal/adapters/v2_decorator.py +101 -0
- cisternal-0.1.0a1/src/cisternal/adapters/v3_middleware.py +64 -0
- cisternal-0.1.0a1/src/cisternal/assets/__init__.py +51 -0
- cisternal-0.1.0a1/src/cisternal/assets/bridge.py +140 -0
- cisternal-0.1.0a1/src/cisternal/assets/bundle.py +176 -0
- cisternal-0.1.0a1/src/cisternal/assets/capability.py +146 -0
- cisternal-0.1.0a1/src/cisternal/assets/composite.py +79 -0
- cisternal-0.1.0a1/src/cisternal/assets/data/__init__.py +1 -0
- cisternal-0.1.0a1/src/cisternal/assets/data/vendor_tools.toml +34 -0
- cisternal-0.1.0a1/src/cisternal/assets/inspect_json.py +49 -0
- cisternal-0.1.0a1/src/cisternal/assets/load.py +21 -0
- cisternal-0.1.0a1/src/cisternal/assets/manifest.py +236 -0
- cisternal-0.1.0a1/src/cisternal/assets/manifest_commands.py +48 -0
- cisternal-0.1.0a1/src/cisternal/assets/manifest_extensions.py +50 -0
- cisternal-0.1.0a1/src/cisternal/assets/protocol.py +15 -0
- cisternal-0.1.0a1/src/cisternal/assets/source.py +135 -0
- cisternal-0.1.0a1/src/cisternal/assets/spec.py +32 -0
- cisternal-0.1.0a1/src/cisternal/assets/validate_golden.py +182 -0
- cisternal-0.1.0a1/src/cisternal/cli.py +559 -0
- cisternal-0.1.0a1/src/cisternal/export/__init__.py +40 -0
- cisternal-0.1.0a1/src/cisternal/export/_hash.py +58 -0
- cisternal-0.1.0a1/src/cisternal/export/_markdown.py +33 -0
- cisternal-0.1.0a1/src/cisternal/export/_rust_emit.py +116 -0
- cisternal-0.1.0a1/src/cisternal/export/antigravity.py +155 -0
- cisternal-0.1.0a1/src/cisternal/export/antigravity_rust.py +56 -0
- cisternal-0.1.0a1/src/cisternal/export/base.py +65 -0
- cisternal-0.1.0a1/src/cisternal/export/claude.py +181 -0
- cisternal-0.1.0a1/src/cisternal/export/claude_rust.py +52 -0
- cisternal-0.1.0a1/src/cisternal/export/copilot.py +105 -0
- cisternal-0.1.0a1/src/cisternal/export/copilot_rust.py +71 -0
- cisternal-0.1.0a1/src/cisternal/export/cursor.py +119 -0
- cisternal-0.1.0a1/src/cisternal/export/cursor_rust.py +70 -0
- cisternal-0.1.0a1/src/cisternal/export/hooks.py +116 -0
- cisternal-0.1.0a1/src/cisternal/export/registry.py +94 -0
- cisternal-0.1.0a1/src/cisternal/export/sink.py +117 -0
- cisternal-0.1.0a1/src/cisternal/export/write.py +59 -0
- cisternal-0.1.0a1/src/cisternal/probe/__init__.py +1 -0
- cisternal-0.1.0a1/src/cisternal/probe/capability_probe.py +50 -0
- cisternal-0.1.0a1/src/cisternal/probe/telemetry_doctor.py +333 -0
- cisternal-0.1.0a1/src/cisternal/probe/telemetry_env.py +23 -0
- cisternal-0.1.0a1/src/cisternal/py.typed +0 -0
- cisternal-0.1.0a1/src/cisternal/registration/__init__.py +46 -0
- cisternal-0.1.0a1/src/cisternal/registration/compose.py +89 -0
- cisternal-0.1.0a1/src/cisternal/registration/decorator.py +87 -0
- cisternal-0.1.0a1/src/cisternal/registration/errors.py +36 -0
- cisternal-0.1.0a1/src/cisternal/registration/registry.py +140 -0
- cisternal-0.1.0a1/src/cisternal/registration/shim.py +126 -0
- cisternal-0.1.0a1/src/cisternal/registration/wired.py +205 -0
- cisternal-0.1.0a1/src/cisternal/telemetry/__init__.py +39 -0
- cisternal-0.1.0a1/src/cisternal/telemetry/context.py +69 -0
- cisternal-0.1.0a1/src/cisternal/telemetry/exporter.py +155 -0
- cisternal-0.1.0a1/src/cisternal/telemetry/otlp_exporter.py +255 -0
- cisternal-0.1.0a1/src/cisternal/telemetry/pipeline.py +314 -0
- cisternal-0.1.0a1/src/cisternal/telemetry/record.py +41 -0
- cisternal-0.1.0a1/src/cisternal/telemetry/self_obs.py +245 -0
- cisternal-0.1.0a1/src/cisternal/telemetry/span.py +204 -0
- cisternal-0.1.0a1/src/cisternal.egg-info/PKG-INFO +109 -0
- cisternal-0.1.0a1/src/cisternal.egg-info/SOURCES.txt +133 -0
- cisternal-0.1.0a1/src/cisternal.egg-info/dependency_links.txt +1 -0
- cisternal-0.1.0a1/src/cisternal.egg-info/entry_points.txt +8 -0
- cisternal-0.1.0a1/src/cisternal.egg-info/requires.txt +9 -0
- cisternal-0.1.0a1/src/cisternal.egg-info/top_level.txt +1 -0
- cisternal-0.1.0a1/tests/test_antigravity_rust_parity.py +74 -0
- cisternal-0.1.0a1/tests/test_assets_bundle_m31.py +110 -0
- cisternal-0.1.0a1/tests/test_assets_capability.py +49 -0
- cisternal-0.1.0a1/tests/test_assets_composite.py +33 -0
- cisternal-0.1.0a1/tests/test_assets_manifest.py +172 -0
- cisternal-0.1.0a1/tests/test_assets_manifest_extensions.py +101 -0
- cisternal-0.1.0a1/tests/test_assets_manifest_vendor_commands.py +75 -0
- cisternal-0.1.0a1/tests/test_assets_public_api.py +172 -0
- cisternal-0.1.0a1/tests/test_assets_spec.py +294 -0
- cisternal-0.1.0a1/tests/test_bathos_telemetry_cutover.py +64 -0
- cisternal-0.1.0a1/tests/test_claude_rust_parity.py +100 -0
- cisternal-0.1.0a1/tests/test_cli.py +127 -0
- cisternal-0.1.0a1/tests/test_cli_assets.py +272 -0
- cisternal-0.1.0a1/tests/test_cli_assets_export_surface.py +86 -0
- cisternal-0.1.0a1/tests/test_cli_assets_inspect.py +90 -0
- cisternal-0.1.0a1/tests/test_cli_assets_validate.py +179 -0
- cisternal-0.1.0a1/tests/test_cli_assets_validate_conflict.py +48 -0
- cisternal-0.1.0a1/tests/test_cli_native_validate.py +136 -0
- cisternal-0.1.0a1/tests/test_cli_rust_parity.py +68 -0
- cisternal-0.1.0a1/tests/test_cli_telemetry_doctor.py +397 -0
- cisternal-0.1.0a1/tests/test_contemplex_telemetry_cutover.py +59 -0
- cisternal-0.1.0a1/tests/test_copilot_rust_parity.py +63 -0
- cisternal-0.1.0a1/tests/test_core.py +350 -0
- cisternal-0.1.0a1/tests/test_cursor_rust_parity.py +63 -0
- cisternal-0.1.0a1/tests/test_dogfood_praxia_fixture.py +59 -0
- cisternal-0.1.0a1/tests/test_export_antigravity.py +178 -0
- cisternal-0.1.0a1/tests/test_export_claude.py +348 -0
- cisternal-0.1.0a1/tests/test_export_claude_bodies.py +135 -0
- cisternal-0.1.0a1/tests/test_export_copilot.py +35 -0
- cisternal-0.1.0a1/tests/test_export_cursor.py +81 -0
- cisternal-0.1.0a1/tests/test_export_hooks_surface.py +92 -0
- cisternal-0.1.0a1/tests/test_export_registry.py +58 -0
- cisternal-0.1.0a1/tests/test_export_regression_m31b.py +71 -0
- cisternal-0.1.0a1/tests/test_export_regression_m31c.py +21 -0
- cisternal-0.1.0a1/tests/test_export_regression_m32.py +24 -0
- cisternal-0.1.0a1/tests/test_export_sink.py +56 -0
- cisternal-0.1.0a1/tests/test_export_write.py +208 -0
- cisternal-0.1.0a1/tests/test_golden_matrix.py +72 -0
- cisternal-0.1.0a1/tests/test_job_span.py +78 -0
- cisternal-0.1.0a1/tests/test_jsonl_exporter.py +116 -0
- cisternal-0.1.0a1/tests/test_mcp.py +533 -0
- cisternal-0.1.0a1/tests/test_myxcel_adapter.py +37 -0
- cisternal-0.1.0a1/tests/test_myxcel_telemetry_cutover.py +67 -0
- cisternal-0.1.0a1/tests/test_namefreeze.py +209 -0
- cisternal-0.1.0a1/tests/test_otlp_exporter.py +177 -0
- cisternal-0.1.0a1/tests/test_otlp_http.py +144 -0
- cisternal-0.1.0a1/tests/test_perf.py +136 -0
- cisternal-0.1.0a1/tests/test_registration_compose.py +505 -0
- cisternal-0.1.0a1/tests/test_registration_init.py +206 -0
- cisternal-0.1.0a1/tests/test_registration_registry.py +324 -0
- cisternal-0.1.0a1/tests/test_registration_snapshot_public.py +42 -0
- cisternal-0.1.0a1/tests/test_registration_telem.py +430 -0
- cisternal-0.1.0a1/tests/test_registration_wire.py +525 -0
- cisternal-0.1.0a1/tests/test_rust_parity.py +57 -0
- cisternal-0.1.0a1/tests/test_rust_parity_bridge.py +30 -0
- cisternal-0.1.0a1/tests/test_rust_parity_hook_filter.py +31 -0
- cisternal-0.1.0a1/tests/test_rust_parity_matrix.py +60 -0
- cisternal-0.1.0a1/tests/test_self_manifest.py +39 -0
- cisternal-0.1.0a1/tests/test_selfcheck.py +292 -0
- cisternal-0.1.0a1/tests/test_telemetry_import_guard.py +19 -0
- cisternal-0.1.0a1/tests/test_validate_golden_resolver.py +37 -0
- cisternal-0.1.0a1/tests/test_workflow_export_dogfood.py +57 -0
- cisternal-0.1.0a1/tests/test_xperiri_adapter.py +34 -0
- cisternal-0.1.0a1/tests/test_xperiri_telemetry_cutover.py +65 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marielle Russo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cisternal
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Shared telemetry substrate and agent-asset export toolkit for the Praxia tool family
|
|
5
|
+
Author: Marielle Russo
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/maraxen/cisternal
|
|
8
|
+
Project-URL: Repository, https://github.com/maraxen/cisternal
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: cyclopts>=4.18.0
|
|
18
|
+
Requires-Dist: fastmcp>=3.4.2
|
|
19
|
+
Requires-Dist: opentelemetry-api>=1.42.1
|
|
20
|
+
Provides-Extra: otlp
|
|
21
|
+
Requires-Dist: opentelemetry-sdk>=1.42.1; extra == "otlp"
|
|
22
|
+
Requires-Dist: opentelemetry-semantic-conventions>=0.63b1; extra == "otlp"
|
|
23
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.42.1; extra == "otlp"
|
|
24
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.42.1; extra == "otlp"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# cisternal
|
|
28
|
+
|
|
29
|
+
**Status: alpha.** APIs may change without notice before `1.0`.
|
|
30
|
+
|
|
31
|
+
Cisternal is a shared telemetry substrate and agent-asset export toolkit for the Praxia tool family. It has two parts:
|
|
32
|
+
|
|
33
|
+
- **Telemetry** — a lightweight, non-blocking event pipeline (JSONL export, OTLP export, MCP-tool registration wrapper) for instrumenting Python tools and MCP servers.
|
|
34
|
+
- **Agent-asset export** — a CLI that takes a registry of MCP tools/commands and emits native plugin/config bundles for downstream coding-agent surfaces: Claude Code, Cursor, GitHub Copilot, and Antigravity.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install cisternal
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For OTLP export support:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install "cisternal[otlp]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Telemetry quickstart
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import cisternal
|
|
52
|
+
|
|
53
|
+
cisternal.init() # log_dir defaults to ~/.cisternal/logs, or env-resolved
|
|
54
|
+
|
|
55
|
+
with cisternal.span("my.operation", request_id="abc123"):
|
|
56
|
+
do_work()
|
|
57
|
+
|
|
58
|
+
cisternal.emit_event("my.custom_event", tool="foo")
|
|
59
|
+
print(cisternal.status())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Check your effective telemetry configuration from the shell:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cisternal telemetry doctor
|
|
66
|
+
cisternal telemetry doctor --json --strict
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Registering MCP tools
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import cisternal
|
|
73
|
+
|
|
74
|
+
@cisternal.tool
|
|
75
|
+
def my_tool(x: int) -> int:
|
|
76
|
+
return x * 2
|
|
77
|
+
|
|
78
|
+
registry = cisternal.wire(server, app, adapter=my_adapter)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`cisternal.tool` is a pure-metadata decorator — it returns the original function unchanged. `cisternal.wire()` snapshots the registry at call time and registers each tool on a FastMCP server (and optionally a Cyclopts CLI app), returning a `WiredRegistry` for introspection.
|
|
82
|
+
|
|
83
|
+
## Agent-asset export
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Preview what would be written, without touching disk
|
|
87
|
+
cisternal assets export --dry-run
|
|
88
|
+
|
|
89
|
+
# Write bundles for specific surfaces
|
|
90
|
+
cisternal assets export --out ./dist/agent-assets
|
|
91
|
+
|
|
92
|
+
# Inspect or validate an existing bundle
|
|
93
|
+
cisternal assets inspect
|
|
94
|
+
cisternal assets validate
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Supported export targets: **Claude Code**, **Cursor**, **GitHub Copilot**, **Antigravity**.
|
|
98
|
+
|
|
99
|
+
The CLI is fastmcp-free by design — `cisternal.cli` imports and runs even in environments without `fastmcp` installed; asset-export logic never depends on the telemetry/registration surface.
|
|
100
|
+
|
|
101
|
+
## Design notes
|
|
102
|
+
|
|
103
|
+
- Telemetry emission never raises: if the pipeline isn't initialized, `emit_event`/`span` are no-ops.
|
|
104
|
+
- Registry state is process-scoped — call `cisternal.clear_registry()` between tests to avoid cross-test contamination.
|
|
105
|
+
- The M2 wire-time MCP callable is a pure passthrough: telemetry and shape adaptation are exclusively owned by the telemetry middleware, never by the registration wrapper itself.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# cisternal
|
|
2
|
+
|
|
3
|
+
**Status: alpha.** APIs may change without notice before `1.0`.
|
|
4
|
+
|
|
5
|
+
Cisternal is a shared telemetry substrate and agent-asset export toolkit for the Praxia tool family. It has two parts:
|
|
6
|
+
|
|
7
|
+
- **Telemetry** — a lightweight, non-blocking event pipeline (JSONL export, OTLP export, MCP-tool registration wrapper) for instrumenting Python tools and MCP servers.
|
|
8
|
+
- **Agent-asset export** — a CLI that takes a registry of MCP tools/commands and emits native plugin/config bundles for downstream coding-agent surfaces: Claude Code, Cursor, GitHub Copilot, and Antigravity.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install cisternal
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For OTLP export support:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install "cisternal[otlp]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Telemetry quickstart
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import cisternal
|
|
26
|
+
|
|
27
|
+
cisternal.init() # log_dir defaults to ~/.cisternal/logs, or env-resolved
|
|
28
|
+
|
|
29
|
+
with cisternal.span("my.operation", request_id="abc123"):
|
|
30
|
+
do_work()
|
|
31
|
+
|
|
32
|
+
cisternal.emit_event("my.custom_event", tool="foo")
|
|
33
|
+
print(cisternal.status())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Check your effective telemetry configuration from the shell:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cisternal telemetry doctor
|
|
40
|
+
cisternal telemetry doctor --json --strict
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Registering MCP tools
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import cisternal
|
|
47
|
+
|
|
48
|
+
@cisternal.tool
|
|
49
|
+
def my_tool(x: int) -> int:
|
|
50
|
+
return x * 2
|
|
51
|
+
|
|
52
|
+
registry = cisternal.wire(server, app, adapter=my_adapter)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`cisternal.tool` is a pure-metadata decorator — it returns the original function unchanged. `cisternal.wire()` snapshots the registry at call time and registers each tool on a FastMCP server (and optionally a Cyclopts CLI app), returning a `WiredRegistry` for introspection.
|
|
56
|
+
|
|
57
|
+
## Agent-asset export
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Preview what would be written, without touching disk
|
|
61
|
+
cisternal assets export --dry-run
|
|
62
|
+
|
|
63
|
+
# Write bundles for specific surfaces
|
|
64
|
+
cisternal assets export --out ./dist/agent-assets
|
|
65
|
+
|
|
66
|
+
# Inspect or validate an existing bundle
|
|
67
|
+
cisternal assets inspect
|
|
68
|
+
cisternal assets validate
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Supported export targets: **Claude Code**, **Cursor**, **GitHub Copilot**, **Antigravity**.
|
|
72
|
+
|
|
73
|
+
The CLI is fastmcp-free by design — `cisternal.cli` imports and runs even in environments without `fastmcp` installed; asset-export logic never depends on the telemetry/registration surface.
|
|
74
|
+
|
|
75
|
+
## Design notes
|
|
76
|
+
|
|
77
|
+
- Telemetry emission never raises: if the pipeline isn't initialized, `emit_event`/`span` are no-ops.
|
|
78
|
+
- Registry state is process-scoped — call `cisternal.clear_registry()` between tests to avoid cross-test contamination.
|
|
79
|
+
- The M2 wire-time MCP callable is a pure passthrough: telemetry and shape adaptation are exclusively owned by the telemetry middleware, never by the registration wrapper itself.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cisternal"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Shared telemetry substrate and agent-asset export toolkit for the Praxia tool family"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Marielle Russo" },
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
20
|
+
]
|
|
21
|
+
requires-python = ">=3.13"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"cyclopts>=4.18.0",
|
|
24
|
+
"fastmcp>=3.4.2",
|
|
25
|
+
"opentelemetry-api>=1.42.1",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/maraxen/cisternal"
|
|
30
|
+
Repository = "https://github.com/maraxen/cisternal"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
otlp = [
|
|
34
|
+
"opentelemetry-sdk>=1.42.1",
|
|
35
|
+
"opentelemetry-semantic-conventions>=0.63b1",
|
|
36
|
+
"opentelemetry-exporter-otlp-proto-grpc>=1.42.1",
|
|
37
|
+
"opentelemetry-exporter-otlp-proto-http>=1.42.1",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = [
|
|
42
|
+
"pytest>=9.0.3",
|
|
43
|
+
"pytest-asyncio>=0.24.0",
|
|
44
|
+
"pytest-cov>=7.1.0",
|
|
45
|
+
"ruff>=0.15.17",
|
|
46
|
+
"ty>=0.0.49",
|
|
47
|
+
"opentelemetry-sdk>=1.42.1",
|
|
48
|
+
"opentelemetry-semantic-conventions>=0.63b1",
|
|
49
|
+
"opentelemetry-exporter-otlp-proto-grpc>=1.42.1",
|
|
50
|
+
"opentelemetry-exporter-otlp-proto-http>=1.42.1",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
cisternal = "cisternal.cli:app"
|
|
55
|
+
|
|
56
|
+
[project.entry-points."cisternal.emitters"]
|
|
57
|
+
claude = "cisternal.export.registry:claude_factory"
|
|
58
|
+
cursor = "cisternal.export.registry:cursor_factory"
|
|
59
|
+
copilot = "cisternal.export.registry:copilot_factory"
|
|
60
|
+
antigravity = "cisternal.export.registry:antigravity_factory"
|
|
61
|
+
|
|
62
|
+
[tool.setuptools]
|
|
63
|
+
package-dir = { "" = "src" }
|
|
64
|
+
|
|
65
|
+
[tool.setuptools.packages.find]
|
|
66
|
+
where = ["src"]
|
|
67
|
+
|
|
68
|
+
[tool.setuptools.package-data]
|
|
69
|
+
"cisternal.assets" = ["data/*.toml"]
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
markers = [
|
|
74
|
+
"integration: requires external services (e.g. otel-collector on localhost)",
|
|
75
|
+
"golden_matrix: export trust digest matrix (M11)",
|
|
76
|
+
]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Cisternal: Shared telemetry substrate for praxia tool family.
|
|
2
|
+
|
|
3
|
+
Public API (spec §3.2 — M1 telemetry + M2 registration surface):
|
|
4
|
+
|
|
5
|
+
M1 — Telemetry:
|
|
6
|
+
init(log_dir, max_bytes, backup_count, exporters): Initialize pipeline
|
|
7
|
+
emit_event(name, **fields): Emit a telemetry event
|
|
8
|
+
span(name, **fields): Sync timing context manager
|
|
9
|
+
aspan(name, **fields): Async timing context manager
|
|
10
|
+
status(): Get pipeline health status
|
|
11
|
+
|
|
12
|
+
M2 — Registration surface (B+G2 hybrid design, challenger-hardened):
|
|
13
|
+
tool: Pure-metadata decorator for registering MCP tools (A1, A2).
|
|
14
|
+
@cisternal.tool returns the original fn unchanged (decorated_fn is fn).
|
|
15
|
+
wire(server, app, *, adapter, registry, expected, validate):
|
|
16
|
+
Snapshot a registry at call time and register each tool on a FastMCP
|
|
17
|
+
server (and optionally a Cyclopts App). Returns a WiredRegistry.
|
|
18
|
+
WiredRegistry: Introspection object returned by wire().
|
|
19
|
+
CisternalWireError: Raised by wire() when expected tools are missing.
|
|
20
|
+
clear_registry(name): Test teardown helper; clears a named registry (A7).
|
|
21
|
+
|
|
22
|
+
Design assumptions (spec §assumptions):
|
|
23
|
+
A1 — FastMCP v3 uses asyncio.iscoroutinefunction() to decide whether to await
|
|
24
|
+
the tool callable. The generated MCP callable is always async def.
|
|
25
|
+
A2 — FastMCP v3 reads inspect.signature(fn) for JSON schema generation.
|
|
26
|
+
Explicit __signature__ injection (H1) controls what FastMCP sees.
|
|
27
|
+
A3 — M1 CisternalMiddleware is installed on the FastMCP server before wire()
|
|
28
|
+
is called. Telemetry and shape adaptation are M1's exclusive responsibility.
|
|
29
|
+
A4 — Cyclopts 4.18.0+ calls asyncio.run() for async def command functions when
|
|
30
|
+
no event loop is running; inside a running loop, use app.run_async().
|
|
31
|
+
A5 — Global registry state is process-scoped; no cross-process sharing.
|
|
32
|
+
A6 — Python >= 3.11 (asyncio.get_running_loop() is the stable API).
|
|
33
|
+
A7 — Test environments call cisternal.clear_registry() in teardown to prevent
|
|
34
|
+
cross-test registry contamination.
|
|
35
|
+
|
|
36
|
+
HARD INVARIANT (C5/AC-M2-6):
|
|
37
|
+
The M2 wire-time MCP callable is a PURE PASSTHROUGH — it MUST NOT call any
|
|
38
|
+
adapter.emit_* or adapter.shape_* methods, or emit ANY telemetry. All
|
|
39
|
+
telemetry and shaping is exclusively owned by M1 CisternalMiddleware.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
from cisternal.telemetry import (
|
|
46
|
+
init_pipeline,
|
|
47
|
+
get_pipeline,
|
|
48
|
+
span,
|
|
49
|
+
aspan,
|
|
50
|
+
job_span,
|
|
51
|
+
status,
|
|
52
|
+
ExporterBase,
|
|
53
|
+
_build_record,
|
|
54
|
+
)
|
|
55
|
+
from cisternal.registration.decorator import tool
|
|
56
|
+
from cisternal.registration.errors import CisternalWireError
|
|
57
|
+
from cisternal.registration.registry import clear_registry
|
|
58
|
+
|
|
59
|
+
# M3 (assets export)
|
|
60
|
+
from cisternal.assets.spec import AssetSpec
|
|
61
|
+
from cisternal.assets.bundle import AssetBundle
|
|
62
|
+
from cisternal.assets.source import registry_assets
|
|
63
|
+
from cisternal.export.base import Emitter
|
|
64
|
+
from cisternal.export.claude import ClaudeEmitter
|
|
65
|
+
from cisternal.export.write import write_bundle
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def init(
|
|
69
|
+
log_dir: str | Path | None = None,
|
|
70
|
+
max_bytes: int = 10_485_760,
|
|
71
|
+
backup_count: int = 5,
|
|
72
|
+
exporters: list[ExporterBase] | None = None,
|
|
73
|
+
heartbeat_interval: float = 30.0,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Initialize the telemetry pipeline (idempotent).
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
log_dir: Directory for JSONL logs. If None, resolves via env vars or defaults to ~/.cisternal/logs.
|
|
79
|
+
max_bytes: Max file size before rotation (default 10 MB).
|
|
80
|
+
backup_count: Number of backup files to keep.
|
|
81
|
+
exporters: Custom exporters. If None, uses JsonlExporter with log_dir.
|
|
82
|
+
heartbeat_interval: Seconds between liveness heartbeat probes (default 30s).
|
|
83
|
+
"""
|
|
84
|
+
init_pipeline(
|
|
85
|
+
log_dir=log_dir,
|
|
86
|
+
max_bytes=max_bytes,
|
|
87
|
+
backup_count=backup_count,
|
|
88
|
+
exporters=exporters,
|
|
89
|
+
heartbeat_interval=heartbeat_interval,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def emit_event(name: str, **fields: Any) -> None:
|
|
94
|
+
"""Emit a telemetry event.
|
|
95
|
+
|
|
96
|
+
Snapshots contextvars on this thread, builds a Record, and enqueues
|
|
97
|
+
it for non-blocking export. Never raises.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
name: Event name (e.g. 'mcp.call_start').
|
|
101
|
+
**fields: Event fields (e.g. tool='foo', request_id='xyz').
|
|
102
|
+
"""
|
|
103
|
+
import time
|
|
104
|
+
|
|
105
|
+
pipeline = get_pipeline()
|
|
106
|
+
if pipeline is None:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
record = _build_record(name, ts=time.time(), **fields)
|
|
110
|
+
if record is not None:
|
|
111
|
+
pipeline.emit(record)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _lazy_import(name: str) -> object:
|
|
115
|
+
"""Lazy re-export for wire and WiredRegistry to defer fastmcp import."""
|
|
116
|
+
if name == "wire":
|
|
117
|
+
from cisternal.registration.wired import wire as _wire
|
|
118
|
+
return _wire
|
|
119
|
+
if name == "WiredRegistry":
|
|
120
|
+
from cisternal.registration.wired import WiredRegistry as _WiredRegistry
|
|
121
|
+
return _WiredRegistry
|
|
122
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def __getattr__(name: str) -> object:
|
|
126
|
+
return _lazy_import(name)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = [
|
|
130
|
+
# M1 — Telemetry
|
|
131
|
+
"init",
|
|
132
|
+
"emit_event",
|
|
133
|
+
"span",
|
|
134
|
+
"aspan",
|
|
135
|
+
"job_span",
|
|
136
|
+
"status",
|
|
137
|
+
# M2 — Registration surface
|
|
138
|
+
"tool",
|
|
139
|
+
"wire",
|
|
140
|
+
"WiredRegistry",
|
|
141
|
+
"CisternalWireError",
|
|
142
|
+
"clear_registry",
|
|
143
|
+
# M3 (assets export)
|
|
144
|
+
"AssetSpec",
|
|
145
|
+
"AssetBundle",
|
|
146
|
+
"registry_assets",
|
|
147
|
+
"Emitter",
|
|
148
|
+
"ClaudeEmitter",
|
|
149
|
+
"write_bundle",
|
|
150
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Consumer adapters: FastMCP v3 middleware, v2 decorator, CLI."""
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""AdapterBase: Consumer-agnostic adapter protocol (spec §3.6, §5.4).
|
|
2
|
+
|
|
3
|
+
Each adapter owns the event-name set (ALLOWED_NAMES) and the response shape
|
|
4
|
+
for its consumer (shape_ok/shape_error). The MCP wrappers (CisternalMiddleware,
|
|
5
|
+
traced_tool) instantiate an adapter, delegate telemetry emission + shaping to it,
|
|
6
|
+
and never re-raise exceptions (CH-5).
|
|
7
|
+
|
|
8
|
+
(CH-9) Runtime name guard: emit_start/end/error check that the emitted name is
|
|
9
|
+
in self.ALLOWED_NAMES; on mismatch, calls _swallow_name_error (stderr warn + continue).
|
|
10
|
+
Tests monkeypatch _swallow_name_error to raise AssertionError instead.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from cisternal import emit_event
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AdapterBase(ABC):
|
|
22
|
+
"""Abstract base for MCP tool adapters (v3 middleware, v2 decorator, etc.).
|
|
23
|
+
|
|
24
|
+
Each subclass defines:
|
|
25
|
+
- ALLOWED_NAMES: frozenset[str] of event names this adapter may emit.
|
|
26
|
+
- shape_ok(tool_name, result): Transform success response.
|
|
27
|
+
- shape_error(tool_name, exc): Transform error response.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
ALLOWED_NAMES: frozenset[str]
|
|
31
|
+
|
|
32
|
+
def emit_start(self, tool_name: str, arg_keys: list[str], request_id: str) -> None:
|
|
33
|
+
"""Emit mcp.call_start event.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
tool_name: Name of the tool being called.
|
|
37
|
+
arg_keys: Sorted list of argument keys (spec §3.7, §3.8).
|
|
38
|
+
request_id: Unique request ID for this invocation.
|
|
39
|
+
"""
|
|
40
|
+
name = "mcp.call_start"
|
|
41
|
+
if name not in self.ALLOWED_NAMES:
|
|
42
|
+
self._swallow_name_error(name)
|
|
43
|
+
emit_event(name, tool=tool_name, arg_keys=arg_keys, request_id=request_id)
|
|
44
|
+
|
|
45
|
+
def emit_end(self, tool_name: str, request_id: str, duration_ms: float) -> None:
|
|
46
|
+
"""Emit mcp.call_end event.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
tool_name: Name of the tool that executed.
|
|
50
|
+
request_id: The request ID from emit_start.
|
|
51
|
+
duration_ms: Duration of execution in milliseconds.
|
|
52
|
+
"""
|
|
53
|
+
name = "mcp.call_end"
|
|
54
|
+
if name not in self.ALLOWED_NAMES:
|
|
55
|
+
self._swallow_name_error(name)
|
|
56
|
+
emit_event(name, tool=tool_name, request_id=request_id, duration_ms=duration_ms)
|
|
57
|
+
|
|
58
|
+
def emit_error(self, tool_name: str, request_id: str, exc: BaseException) -> None:
|
|
59
|
+
"""Emit mcp.tool_error event.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
tool_name: Name of the tool that raised.
|
|
63
|
+
request_id: The request ID from emit_start.
|
|
64
|
+
exc: The exception that was raised.
|
|
65
|
+
"""
|
|
66
|
+
name = "mcp.tool_error"
|
|
67
|
+
if name not in self.ALLOWED_NAMES:
|
|
68
|
+
self._swallow_name_error(name)
|
|
69
|
+
emit_event(
|
|
70
|
+
name,
|
|
71
|
+
tool=tool_name,
|
|
72
|
+
request_id=request_id,
|
|
73
|
+
exc_type=type(exc).__name__,
|
|
74
|
+
exc_msg=str(exc),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def shape_ok(self, tool_name: str, result: Any) -> Any:
|
|
79
|
+
"""Shape a successful tool result for the consumer.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
tool_name: Name of the tool (for context).
|
|
83
|
+
result: The result returned by the tool.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Consumer-specific shaped response (dict, str, or other).
|
|
87
|
+
"""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
|
|
92
|
+
"""Shape an error response for the consumer (never re-raises).
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
tool_name: Name of the tool (for context).
|
|
96
|
+
exc: The exception that was caught.
|
|
97
|
+
**fields: Optional extra fields for the error envelope.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Consumer-specific error shape (dict, str, or other).
|
|
101
|
+
"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def _swallow_name_error(self, name: str) -> None:
|
|
105
|
+
"""Handle illegal event name: log to stderr and return (warn-and-continue).
|
|
106
|
+
|
|
107
|
+
Tests may monkeypatch this to raise AssertionError (AC-NAMEFREEZE-4).
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
name: The illegal event name.
|
|
111
|
+
"""
|
|
112
|
+
print(f"[cisternal] ILLEGAL event name: {name!r}", file=sys.stderr)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class BathosAdapter(AdapterBase):
|
|
116
|
+
"""Adapter for bathos v3 middleware.
|
|
117
|
+
|
|
118
|
+
Event names (spec §4.2): mcp.call_start, mcp.call_end, mcp.tool_error.
|
|
119
|
+
Response shape: dict envelope with ok/error_code/error/resolution_hint.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
ALLOWED_NAMES = frozenset({"mcp.call_start", "mcp.call_end", "mcp.tool_error"})
|
|
123
|
+
|
|
124
|
+
def shape_ok(self, tool_name: str, result: Any) -> Any:
|
|
125
|
+
"""Shape success: merge dict results, add envelope fields.
|
|
126
|
+
|
|
127
|
+
If result is a dict, spread it and add envelope.
|
|
128
|
+
Otherwise, return minimal envelope.
|
|
129
|
+
"""
|
|
130
|
+
if isinstance(result, dict):
|
|
131
|
+
return {
|
|
132
|
+
**result,
|
|
133
|
+
"ok": True,
|
|
134
|
+
"error_code": None,
|
|
135
|
+
"error": None,
|
|
136
|
+
"resolution_hint": None,
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
"ok": True,
|
|
140
|
+
"error_code": None,
|
|
141
|
+
"error": None,
|
|
142
|
+
"resolution_hint": None,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
|
|
146
|
+
"""Shape error: error envelope with error_code and message."""
|
|
147
|
+
return {
|
|
148
|
+
"ok": False,
|
|
149
|
+
"error_code": "INTERNAL",
|
|
150
|
+
"error": str(exc),
|
|
151
|
+
"resolution_hint": "",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ContemplexAdapter(AdapterBase):
|
|
156
|
+
"""Adapter for contemplex v2 decorator (sync).
|
|
157
|
+
|
|
158
|
+
Event names (spec §4.2): mcp.call_start, mcp.call_end, mcp.tool_error.
|
|
159
|
+
Response shape: passthrough for success; err_envelope for error.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
ALLOWED_NAMES = frozenset({"mcp.call_start", "mcp.call_end", "mcp.tool_error"})
|
|
163
|
+
|
|
164
|
+
def shape_ok(self, tool_name: str, result: Any) -> Any:
|
|
165
|
+
"""Shape success: passthrough result unchanged."""
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
|
|
169
|
+
"""Shape error: use contemplex err_envelope if available, else fallback.
|
|
170
|
+
|
|
171
|
+
Tries to import contemplex.errors; if unavailable, returns basic dict.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
from contemplex.errors import ErrorCode, err_envelope
|
|
175
|
+
|
|
176
|
+
return err_envelope(ErrorCode.INTERNAL, f"{type(exc).__name__}: {exc}")
|
|
177
|
+
except ImportError:
|
|
178
|
+
# Fallback if contemplex is not available
|
|
179
|
+
return {"ok": False, "error_code": "INTERNAL", "error": str(exc)}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class XpeririAdapter(AdapterBase):
|
|
183
|
+
"""Adapter for xperiri v2 decorator (sync, JSON-string MCP returns).
|
|
184
|
+
|
|
185
|
+
Event names (spec §4.2): mcp.call_start, mcp.call_end, mcp.tool_error.
|
|
186
|
+
Response shape: JSON string for success and error (xperiri MCP tools return str).
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
ALLOWED_NAMES = frozenset({"mcp.call_start", "mcp.call_end", "mcp.tool_error"})
|
|
190
|
+
|
|
191
|
+
def shape_ok(self, tool_name: str, result: Any) -> Any:
|
|
192
|
+
"""Shape success: passthrough str; serialize dict/other to JSON."""
|
|
193
|
+
if isinstance(result, str):
|
|
194
|
+
return result
|
|
195
|
+
return json.dumps(result, sort_keys=True)
|
|
196
|
+
|
|
197
|
+
def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
|
|
198
|
+
"""Shape error: JSON string envelope matching xperiri error returns."""
|
|
199
|
+
payload: dict[str, Any] = {"ok": False, "error": str(exc)}
|
|
200
|
+
payload.update(fields)
|
|
201
|
+
return json.dumps(payload, sort_keys=True)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class MyxcelAdapter(AdapterBase):
|
|
205
|
+
"""Adapter for myxcel v2 decorator (async MCP tools, dict returns).
|
|
206
|
+
|
|
207
|
+
Event names (spec §4.2): mcp.call_start, mcp.call_end, mcp.tool_error.
|
|
208
|
+
Response shape: dict passthrough; errors use {error, message} per myxcel.mcp_server._tool_error.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
ALLOWED_NAMES = frozenset({"mcp.call_start", "mcp.call_end", "mcp.tool_error"})
|
|
212
|
+
|
|
213
|
+
def shape_ok(self, tool_name: str, result: Any) -> Any:
|
|
214
|
+
"""Shape success: passthrough dict/list results (myxcel in-band errors included)."""
|
|
215
|
+
if isinstance(result, (dict, list)):
|
|
216
|
+
return result
|
|
217
|
+
return {"result": result}
|
|
218
|
+
|
|
219
|
+
def shape_error(self, tool_name: str, exc: BaseException, **fields: Any) -> Any:
|
|
220
|
+
"""Shape error: myxcel _tool_error envelope."""
|
|
221
|
+
return {"error": type(exc).__name__, "message": str(exc)}
|