pyagent-studio 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ """PyAgent Studio — terminal-based interactive workbench for agent systems."""
2
+
3
+ from pyagent_studio.services.blueprint_service import BlueprintService
4
+ from pyagent_studio.services.governance_service import GovernanceService
5
+ from pyagent_studio.services.simulation_service import SimulationService
6
+ from pyagent_studio.services.trace_service import TraceService
7
+
8
+ __all__ = [
9
+ "BlueprintService",
10
+ "GovernanceService",
11
+ "SimulationService",
12
+ "TraceService",
13
+ ]
14
+ __version__ = "0.1.0"
pyagent_studio/app.py ADDED
@@ -0,0 +1,84 @@
1
+ """StudioApp: main Textual application with screen routing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import App, ComposeResult
6
+ from textual.widgets import Footer, Header, TabbedContent, TabPane
7
+
8
+ from pyagent_blueprint.schema import BlueprintSpec
9
+ from pyagent_studio.services.blueprint_service import BlueprintService
10
+
11
+
12
+ class StudioApp(App):
13
+ """PyAgent Studio — terminal-based interactive workbench.
14
+
15
+ Launch via: ``pyagent-studio [blueprint.yaml]``
16
+ """
17
+
18
+ TITLE = "PyAgent Studio"
19
+ CSS = """
20
+ TabbedContent {
21
+ height: 1fr;
22
+ }
23
+ """
24
+ BINDINGS = [
25
+ ("q", "quit", "Quit"),
26
+ ("ctrl+s", "save", "Save"),
27
+ ("ctrl+v", "validate", "Validate"),
28
+ ("ctrl+r", "render", "Render Graph"),
29
+ ("ctrl+t", "simulate", "Simulate"),
30
+ ("ctrl+d", "diff", "Diff"),
31
+ ]
32
+
33
+ def __init__(self, blueprint_path: str | None = None, **kwargs) -> None:
34
+ super().__init__(**kwargs)
35
+ self._blueprint_path = blueprint_path
36
+ self._service = BlueprintService()
37
+ self._spec: BlueprintSpec | None = None
38
+
39
+ def compose(self) -> ComposeResult:
40
+ yield Header()
41
+ with TabbedContent():
42
+ with TabPane("Dashboard", id="tab-dashboard"):
43
+ from textual.widgets import Static
44
+ yield Static("[bold]Dashboard[/bold]\n\nLoad a blueprint to get started.")
45
+ with TabPane("Editor", id="tab-editor"):
46
+ from textual.widgets import Static
47
+ yield Static("Editor — use Ctrl+V to validate")
48
+ with TabPane("Graph", id="tab-graph"):
49
+ from textual.widgets import Static
50
+ yield Static("Graph — load a blueprint first")
51
+ with TabPane("Simulation", id="tab-sim"):
52
+ from textual.widgets import Static
53
+ yield Static("Simulation — load a blueprint first")
54
+ with TabPane("Traces", id="tab-traces"):
55
+ from textual.widgets import Static
56
+ yield Static("Traces — no trace file loaded")
57
+ with TabPane("Cost", id="tab-cost"):
58
+ from textual.widgets import Static
59
+ yield Static("Cost — no data available")
60
+ with TabPane("Governance", id="tab-gov"):
61
+ from textual.widgets import Static
62
+ yield Static("Governance — load a blueprint first")
63
+ yield Footer()
64
+
65
+ def on_mount(self) -> None:
66
+ if self._blueprint_path:
67
+ try:
68
+ self._spec = self._service.load(self._blueprint_path)
69
+ self.notify(f"Loaded: {self._spec.metadata.name}")
70
+ except Exception as exc:
71
+ self.notify(f"Load error: {exc}", severity="error")
72
+
73
+ def action_quit(self) -> None:
74
+ self.exit()
75
+
76
+ def action_validate(self) -> None:
77
+ if self._spec is None:
78
+ self.notify("No blueprint loaded", severity="warning")
79
+ return
80
+ issues = self._service.validate()
81
+ if issues:
82
+ self.notify(f"{len(issues)} validation issue(s) found", severity="warning")
83
+ else:
84
+ self.notify("Blueprint is valid ✓")
pyagent_studio/cli.py ADDED
@@ -0,0 +1,30 @@
1
+ """CLI entry point: pyagent-studio [blueprint.yaml]."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+
8
+ @click.command()
9
+ @click.argument("blueprint", required=False, default=None, type=click.Path())
10
+ def main(blueprint: str | None) -> None:
11
+ """Launch PyAgent Studio — interactive agent system workbench.
12
+
13
+ Optionally pass a blueprint YAML file to load on startup.
14
+ """
15
+ try:
16
+ from pyagent_studio.app import StudioApp
17
+ except ImportError:
18
+ click.echo(
19
+ "Error: textual is not installed. "
20
+ "Install with: pip install pyagent-studio[tui]",
21
+ err=True,
22
+ )
23
+ raise SystemExit(1)
24
+
25
+ app = StudioApp(blueprint_path=blueprint)
26
+ app.run()
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
File without changes
@@ -0,0 +1,22 @@
1
+ """Studio screens — Textual Screen subclasses for each TUI view."""
2
+
3
+ try:
4
+ from pyagent_studio.screens.dashboard import DashboardScreen
5
+ from pyagent_studio.screens.editor import EditorScreen
6
+ from pyagent_studio.screens.graph import GraphScreen
7
+ from pyagent_studio.screens.simulation import SimulationScreen
8
+ from pyagent_studio.screens.traces import TracesScreen
9
+ from pyagent_studio.screens.cost import CostScreen
10
+ from pyagent_studio.screens.governance import GovernanceScreen
11
+
12
+ __all__ = [
13
+ "CostScreen",
14
+ "DashboardScreen",
15
+ "EditorScreen",
16
+ "GovernanceScreen",
17
+ "GraphScreen",
18
+ "SimulationScreen",
19
+ "TracesScreen",
20
+ ]
21
+ except ImportError:
22
+ __all__ = []
@@ -0,0 +1,33 @@
1
+ """CostScreen: cost breakdown tables by pattern/agent/model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.screen import Screen
7
+ from textual.widgets import Footer, Header, Static
8
+
9
+ from pyagent_studio.widgets.cost_chart import CostChart
10
+
11
+
12
+ class CostScreen(Screen):
13
+ """Display cost breakdown for agent system runs."""
14
+
15
+ BINDINGS = [("q", "quit", "Quit")]
16
+
17
+ def __init__(self, costs: dict[str, float] | None = None, **kwargs) -> None:
18
+ super().__init__(**kwargs)
19
+ self._costs = costs
20
+
21
+ def compose(self) -> ComposeResult:
22
+ yield Header()
23
+ yield Static("[bold]Cost Analysis[/bold]", id="title")
24
+ yield CostChart(id="cost-chart")
25
+ yield Footer()
26
+
27
+ def on_mount(self) -> None:
28
+ if self._costs:
29
+ chart = self.query_one("#cost-chart", CostChart)
30
+ chart.load_data(self._costs)
31
+
32
+ def action_quit(self) -> None:
33
+ self.app.exit()
@@ -0,0 +1,55 @@
1
+ """DashboardScreen: list blueprints, health summary, quick actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.screen import Screen
7
+ from textual.widgets import DataTable, Footer, Header, Static
8
+
9
+ from pyagent_studio.services.blueprint_service import BlueprintService
10
+
11
+
12
+ class DashboardScreen(Screen):
13
+ """Main dashboard showing discovered blueprints and their status."""
14
+
15
+ BINDINGS = [
16
+ ("n", "new_blueprint", "New"),
17
+ ("enter", "open_blueprint", "Open"),
18
+ ("q", "quit", "Quit"),
19
+ ]
20
+
21
+ def __init__(self, service: BlueprintService | None = None, **kwargs) -> None:
22
+ super().__init__(**kwargs)
23
+ self._service = service or BlueprintService()
24
+
25
+ def compose(self) -> ComposeResult:
26
+ yield Header()
27
+ yield Static("[bold]PyAgent Studio — Dashboard[/bold]", id="title")
28
+ yield DataTable(id="blueprint-list")
29
+ yield Footer()
30
+
31
+ def on_mount(self) -> None:
32
+ table = self.query_one("#blueprint-list", DataTable)
33
+ table.add_columns("File", "Name", "Agents", "Workflows", "Status")
34
+ self._refresh_list(table)
35
+
36
+ def _refresh_list(self, table: DataTable) -> None:
37
+ """Discover and list blueprints."""
38
+ table.clear()
39
+ for path in self._service.discover_blueprints():
40
+ try:
41
+ spec = self._service.load(path)
42
+ issues = self._service.validate()
43
+ status = "✓ Valid" if not issues else f"✗ {len(issues)} issue(s)"
44
+ table.add_row(
45
+ str(path.name),
46
+ spec.metadata.name,
47
+ str(len(spec.agents)),
48
+ str(len(spec.workflows)),
49
+ status,
50
+ )
51
+ except Exception:
52
+ table.add_row(str(path.name), "?", "?", "?", "✗ Load error")
53
+
54
+ def action_quit(self) -> None:
55
+ self.app.exit()
@@ -0,0 +1,64 @@
1
+ """EditorScreen: YAML editing with live validation panel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Horizontal
7
+ from textual.screen import Screen
8
+ from textual.widgets import Footer, Header, TextArea
9
+
10
+ from pyagent_studio.widgets.validation_log import ValidationLog
11
+
12
+
13
+ class EditorScreen(Screen):
14
+ """YAML editor with live validation sidebar."""
15
+
16
+ BINDINGS = [
17
+ ("ctrl+s", "save", "Save"),
18
+ ("ctrl+v", "validate", "Validate"),
19
+ ("q", "quit", "Quit"),
20
+ ]
21
+
22
+ DEFAULT_CSS = """
23
+ EditorScreen Horizontal {
24
+ height: 1fr;
25
+ }
26
+ EditorScreen TextArea {
27
+ width: 2fr;
28
+ }
29
+ EditorScreen ValidationLog {
30
+ width: 1fr;
31
+ border-left: solid green;
32
+ }
33
+ """
34
+
35
+ def __init__(self, content: str = "", **kwargs) -> None:
36
+ super().__init__(**kwargs)
37
+ self._content = content
38
+
39
+ def compose(self) -> ComposeResult:
40
+ yield Header()
41
+ with Horizontal():
42
+ yield TextArea(self._content, language="yaml", id="editor")
43
+ yield ValidationLog(id="validation-log")
44
+ yield Footer()
45
+
46
+ def action_validate(self) -> None:
47
+ """Validate current editor content."""
48
+ from pyagent_blueprint import BlueprintValidator, load_blueprint_from_str
49
+ from pyagent_blueprint.loader import BlueprintLoadError
50
+
51
+ editor = self.query_one("#editor", TextArea)
52
+ log = self.query_one("#validation-log", ValidationLog)
53
+
54
+ try:
55
+ spec = load_blueprint_from_str(editor.text)
56
+ validator = BlueprintValidator()
57
+ issues = validator.validate(spec)
58
+ log.load_issues(issues)
59
+ except (BlueprintLoadError, Exception) as exc:
60
+ log.clear()
61
+ log.write(f"[red]Parse error: {exc}[/red]")
62
+
63
+ def action_quit(self) -> None:
64
+ self.app.exit()
@@ -0,0 +1,77 @@
1
+ """GovernanceScreen: validation issues, policy compliance, diff view."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Vertical
7
+ from textual.screen import Screen
8
+ from textual.widgets import Footer, Header, Static
9
+
10
+ from pyagent_blueprint.schema import BlueprintSpec
11
+ from pyagent_studio.services.governance_service import GovernanceService
12
+ from pyagent_studio.widgets.diff_view import DiffView
13
+ from pyagent_studio.widgets.validation_log import ValidationLog
14
+
15
+
16
+ class GovernanceScreen(Screen):
17
+ """Display validation issues, compliance score, and spec diff."""
18
+
19
+ BINDINGS = [("q", "quit", "Quit")]
20
+
21
+ DEFAULT_CSS = """
22
+ GovernanceScreen Vertical {
23
+ height: 1fr;
24
+ }
25
+ GovernanceScreen ValidationLog {
26
+ height: 1fr;
27
+ border: solid green;
28
+ }
29
+ GovernanceScreen DiffView {
30
+ height: 1fr;
31
+ border: solid blue;
32
+ }
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ spec: BlueprintSpec | None = None,
38
+ old_spec: BlueprintSpec | None = None,
39
+ **kwargs,
40
+ ) -> None:
41
+ super().__init__(**kwargs)
42
+ self._spec = spec
43
+ self._old_spec = old_spec
44
+ self._service = GovernanceService()
45
+
46
+ def compose(self) -> ComposeResult:
47
+ yield Header()
48
+ yield Static("[bold]Governance & Compliance[/bold]", id="title")
49
+ with Vertical():
50
+ yield Static("", id="score")
51
+ yield ValidationLog(id="gov-validation")
52
+ yield DiffView(id="gov-diff")
53
+ yield Footer()
54
+
55
+ def on_mount(self) -> None:
56
+ if self._spec:
57
+ self._run_compliance()
58
+ if self._spec and self._old_spec:
59
+ self._run_diff()
60
+
61
+ def _run_compliance(self) -> None:
62
+ report = self._service.check_compliance(self._spec)
63
+ score_widget = self.query_one("#score", Static)
64
+ score_widget.update(
65
+ f"[bold]Compliance Score: {report.score:.0%}[/bold] "
66
+ f"({report.passed}/{report.total_checks} checks passing)"
67
+ )
68
+ log = self.query_one("#gov-validation", ValidationLog)
69
+ log.load_issues(report.issues)
70
+
71
+ def _run_diff(self) -> None:
72
+ changes = self._service.diff(self._old_spec, self._spec)
73
+ diff_view = self.query_one("#gov-diff", DiffView)
74
+ diff_view.load_changes(changes)
75
+
76
+ def action_quit(self) -> None:
77
+ self.app.exit()
@@ -0,0 +1,42 @@
1
+ """GraphScreen: ASCII/Rich renderable DAG of workflow topology."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.screen import Screen
7
+ from textual.widgets import Footer, Header, Static
8
+
9
+ from pyagent_blueprint.renderer import BlueprintRenderer
10
+ from pyagent_blueprint.schema import BlueprintSpec
11
+
12
+
13
+ class GraphScreen(Screen):
14
+ """Display the workflow DAG as a Mermaid-like ASCII diagram."""
15
+
16
+ BINDINGS = [("q", "quit", "Quit")]
17
+
18
+ def __init__(self, spec: BlueprintSpec | None = None, **kwargs) -> None:
19
+ super().__init__(**kwargs)
20
+ self._spec = spec
21
+
22
+ def compose(self) -> ComposeResult:
23
+ yield Header()
24
+ yield Static("[bold]Workflow Graph[/bold]", id="title")
25
+ yield Static("No blueprint loaded", id="graph-content")
26
+ yield Footer()
27
+
28
+ def on_mount(self) -> None:
29
+ if self._spec is not None:
30
+ self.load_spec(self._spec)
31
+
32
+ def load_spec(self, spec: BlueprintSpec) -> None:
33
+ """Render the blueprint graph."""
34
+ self._spec = spec
35
+ renderer = BlueprintRenderer()
36
+ mermaid = renderer.to_mermaid(spec)
37
+
38
+ content = self.query_one("#graph-content", Static)
39
+ content.update(f"```\n{mermaid}\n```")
40
+
41
+ def action_quit(self) -> None:
42
+ self.app.exit()
@@ -0,0 +1,64 @@
1
+ """SimulationScreen: run blueprint with MockLLM, stream results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ from textual.app import ComposeResult
8
+ from textual.screen import Screen
9
+ from textual.widgets import Footer, Header, Input, RichLog, Static
10
+
11
+ from pyagent_blueprint.schema import BlueprintSpec
12
+ from pyagent_studio.services.simulation_service import SimulationService
13
+
14
+
15
+ class SimulationScreen(Screen):
16
+ """Run simulations against a compiled blueprint and display results."""
17
+
18
+ BINDINGS = [
19
+ ("ctrl+t", "run_simulation", "Run"),
20
+ ("q", "quit", "Quit"),
21
+ ]
22
+
23
+ DEFAULT_CSS = """
24
+ SimulationScreen RichLog {
25
+ height: 1fr;
26
+ border: solid green;
27
+ }
28
+ """
29
+
30
+ def __init__(self, spec: BlueprintSpec | None = None, **kwargs) -> None:
31
+ super().__init__(**kwargs)
32
+ self._spec = spec
33
+ self._service = SimulationService()
34
+
35
+ def compose(self) -> ComposeResult:
36
+ yield Header()
37
+ yield Static("[bold]Simulation[/bold]", id="title")
38
+ yield Input(placeholder="Enter task to simulate...", id="task-input")
39
+ yield RichLog(id="sim-output")
40
+ yield Footer()
41
+
42
+ async def on_input_submitted(self, event: Input.Submitted) -> None:
43
+ """Run simulation when input is submitted."""
44
+ if self._spec is None:
45
+ log = self.query_one("#sim-output", RichLog)
46
+ log.write("[red]No blueprint loaded[/red]")
47
+ return
48
+
49
+ await self._run(event.value)
50
+
51
+ async def _run(self, task: str) -> None:
52
+ log = self.query_one("#sim-output", RichLog)
53
+ log.write(f"\n[bold]Running:[/bold] {task}")
54
+
55
+ for wf_name in self._spec.workflows:
56
+ result = await self._service.run(self._spec, wf_name, task)
57
+ if result.success:
58
+ log.write(f"[green]✓ {wf_name}[/green] ({result.elapsed_ms:.0f}ms)")
59
+ log.write(f" Output: {result.output[:200]}")
60
+ else:
61
+ log.write(f"[red]✗ {wf_name}[/red]: {result.error}")
62
+
63
+ def action_quit(self) -> None:
64
+ self.app.exit()
@@ -0,0 +1,43 @@
1
+ """TracesScreen: browse recorded traces, inspect spans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.screen import Screen
7
+ from textual.widgets import Footer, Header, Static
8
+
9
+ from pyagent_studio.services.trace_service import TraceService
10
+ from pyagent_studio.widgets.trace_table import TraceTable
11
+
12
+
13
+ class TracesScreen(Screen):
14
+ """Browse recorded trace spans from pyagent-trace Recorder."""
15
+
16
+ BINDINGS = [("q", "quit", "Quit")]
17
+
18
+ def __init__(self, trace_path: str | None = None, **kwargs) -> None:
19
+ super().__init__(**kwargs)
20
+ self._trace_path = trace_path
21
+ self._service = TraceService()
22
+
23
+ def compose(self) -> ComposeResult:
24
+ yield Header()
25
+ yield Static("[bold]Traces[/bold]", id="title")
26
+ yield TraceTable(id="trace-table")
27
+ yield Footer()
28
+
29
+ def on_mount(self) -> None:
30
+ if self._trace_path:
31
+ self.load_traces(self._trace_path)
32
+
33
+ def load_traces(self, path: str) -> None:
34
+ """Load and display traces from a JSONL file."""
35
+ try:
36
+ spans = self._service.load(path)
37
+ table = self.query_one("#trace-table", TraceTable)
38
+ table.load_spans(spans)
39
+ except FileNotFoundError:
40
+ pass
41
+
42
+ def action_quit(self) -> None:
43
+ self.app.exit()
@@ -0,0 +1,13 @@
1
+ """Studio services — headless logic for blueprint, simulation, traces, governance."""
2
+
3
+ from pyagent_studio.services.blueprint_service import BlueprintService
4
+ from pyagent_studio.services.governance_service import GovernanceService
5
+ from pyagent_studio.services.simulation_service import SimulationService
6
+ from pyagent_studio.services.trace_service import TraceService
7
+
8
+ __all__ = [
9
+ "BlueprintService",
10
+ "GovernanceService",
11
+ "SimulationService",
12
+ "TraceService",
13
+ ]
@@ -0,0 +1,115 @@
1
+ """BlueprintService: load, validate, compile blueprints for the studio."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from pyagent_blueprint import (
9
+ BlueprintCompiler,
10
+ BlueprintValidator,
11
+ RuntimeGraph,
12
+ load_blueprint,
13
+ )
14
+ from pyagent_blueprint.loader import BlueprintLoadError
15
+ from pyagent_blueprint.schema import BlueprintSpec
16
+ from pyagent_blueprint.validator import ValidationIssue
17
+
18
+
19
+ class BlueprintService:
20
+ """Headless service for loading, validating, and compiling blueprints.
21
+
22
+ Wraps ``pyagent-blueprint`` for use by TUI screens and tests.
23
+ """
24
+
25
+ def __init__(self) -> None:
26
+ self._compiler = BlueprintCompiler()
27
+ self._validator = BlueprintValidator()
28
+ self._spec: BlueprintSpec | None = None
29
+ self._graph: RuntimeGraph | None = None
30
+ self._path: Path | None = None
31
+
32
+ def load(self, path: str | Path) -> BlueprintSpec:
33
+ """Load a blueprint from file.
34
+
35
+ Args:
36
+ path: Path to YAML/JSON blueprint.
37
+
38
+ Returns:
39
+ Validated ``BlueprintSpec``.
40
+
41
+ Raises:
42
+ BlueprintLoadError: On load/validation failure.
43
+ """
44
+ self._path = Path(path)
45
+ self._spec = load_blueprint(self._path)
46
+ self._graph = None
47
+ return self._spec
48
+
49
+ def validate(self) -> list[ValidationIssue]:
50
+ """Run static validation on the loaded spec.
51
+
52
+ Returns:
53
+ List of validation issues.
54
+
55
+ Raises:
56
+ RuntimeError: If no spec loaded.
57
+ """
58
+ if self._spec is None:
59
+ raise RuntimeError("No blueprint loaded. Call load() first.")
60
+ return self._validator.validate(self._spec)
61
+
62
+ def compile(self) -> RuntimeGraph:
63
+ """Compile the loaded spec into a RuntimeGraph.
64
+
65
+ Returns:
66
+ Executable ``RuntimeGraph``.
67
+
68
+ Raises:
69
+ RuntimeError: If no spec loaded.
70
+ """
71
+ if self._spec is None:
72
+ raise RuntimeError("No blueprint loaded. Call load() first.")
73
+ self._graph = self._compiler.compile(self._spec)
74
+ return self._graph
75
+
76
+ def discover_blueprints(self, directory: str | Path = ".") -> list[Path]:
77
+ """Find all YAML/JSON blueprint files in a directory.
78
+
79
+ Args:
80
+ directory: Root directory to search.
81
+
82
+ Returns:
83
+ List of file paths.
84
+ """
85
+ root = Path(directory)
86
+ files: list[Path] = []
87
+ for ext in ("*.yaml", "*.yml", "*.json"):
88
+ files.extend(root.glob(f"**/{ext}"))
89
+ return sorted(files)
90
+
91
+ @property
92
+ def spec(self) -> BlueprintSpec | None:
93
+ return self._spec
94
+
95
+ @property
96
+ def graph(self) -> RuntimeGraph | None:
97
+ return self._graph
98
+
99
+ @property
100
+ def path(self) -> Path | None:
101
+ return self._path
102
+
103
+ def summary(self) -> dict[str, Any]:
104
+ """Quick summary of loaded blueprint."""
105
+ if self._spec is None:
106
+ return {"loaded": False}
107
+ return {
108
+ "loaded": True,
109
+ "name": self._spec.metadata.name,
110
+ "version": self._spec.metadata.version,
111
+ "agents": len(self._spec.agents),
112
+ "workflows": len(self._spec.workflows),
113
+ "providers": len(self._spec.providers),
114
+ "contracts": len(self._spec.contracts),
115
+ }