strands-compose 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.
- strands_compose/__init__.py +64 -0
- strands_compose/cli.py +427 -0
- strands_compose/config/__init__.py +53 -0
- strands_compose/config/interpolation.py +196 -0
- strands_compose/config/loaders/__init__.py +12 -0
- strands_compose/config/loaders/helpers.py +406 -0
- strands_compose/config/loaders/loaders.py +270 -0
- strands_compose/config/loaders/validators.py +124 -0
- strands_compose/config/resolvers/__init__.py +28 -0
- strands_compose/config/resolvers/agents.py +203 -0
- strands_compose/config/resolvers/config.py +166 -0
- strands_compose/config/resolvers/conversation_manager.py +54 -0
- strands_compose/config/resolvers/hooks.py +68 -0
- strands_compose/config/resolvers/mcp.py +106 -0
- strands_compose/config/resolvers/models.py +47 -0
- strands_compose/config/resolvers/orchestrations/__init__.py +80 -0
- strands_compose/config/resolvers/orchestrations/builders.py +354 -0
- strands_compose/config/resolvers/orchestrations/planner.py +102 -0
- strands_compose/config/resolvers/session_manager.py +149 -0
- strands_compose/config/schema.py +337 -0
- strands_compose/converters/__init__.py +13 -0
- strands_compose/converters/base.py +54 -0
- strands_compose/converters/openai.py +221 -0
- strands_compose/converters/raw.py +31 -0
- strands_compose/exceptions.py +40 -0
- strands_compose/hooks/__init__.py +17 -0
- strands_compose/hooks/event_publisher.py +378 -0
- strands_compose/hooks/max_calls_guard.py +90 -0
- strands_compose/hooks/stop_guard.py +113 -0
- strands_compose/hooks/tool_name_sanitizer.py +184 -0
- strands_compose/mcp/README.md +105 -0
- strands_compose/mcp/__init__.py +27 -0
- strands_compose/mcp/client.py +170 -0
- strands_compose/mcp/lifecycle.py +233 -0
- strands_compose/mcp/server.py +327 -0
- strands_compose/mcp/transports.py +188 -0
- strands_compose/models.py +69 -0
- strands_compose/py.typed +0 -0
- strands_compose/renderers/__init__.py +9 -0
- strands_compose/renderers/ansi.py +237 -0
- strands_compose/renderers/base.py +36 -0
- strands_compose/startup/__init__.py +24 -0
- strands_compose/startup/report.py +174 -0
- strands_compose/startup/validator.py +167 -0
- strands_compose/tools/__init__.py +33 -0
- strands_compose/tools/extractors.py +182 -0
- strands_compose/tools/loaders.py +238 -0
- strands_compose/tools/wrappers.py +100 -0
- strands_compose/types.py +110 -0
- strands_compose/utils.py +210 -0
- strands_compose/wire.py +196 -0
- strands_compose-0.1.0.dist-info/METADATA +445 -0
- strands_compose-0.1.0.dist-info/RECORD +56 -0
- strands_compose-0.1.0.dist-info/WHEEL +4 -0
- strands_compose-0.1.0.dist-info/entry_points.txt +2 -0
- strands_compose-0.1.0.dist-info/licenses/LICENSE +174 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""strands-compose — Zero-code YAML-driven agent orchestration over strands-agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .config import (
|
|
6
|
+
AppConfig,
|
|
7
|
+
ConfigInput,
|
|
8
|
+
ResolvedConfig,
|
|
9
|
+
ResolvedInfra,
|
|
10
|
+
load,
|
|
11
|
+
load_config,
|
|
12
|
+
load_session,
|
|
13
|
+
resolve_infra,
|
|
14
|
+
)
|
|
15
|
+
from .config.resolvers.orchestrations import OrchestrationBuilder
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
CircularDependencyError,
|
|
18
|
+
ConfigurationError,
|
|
19
|
+
ImportResolutionError,
|
|
20
|
+
SchemaValidationError,
|
|
21
|
+
UnresolvedReferenceError,
|
|
22
|
+
)
|
|
23
|
+
from .hooks import EventPublisher, MaxToolCallsGuard, StopGuard, ToolNameSanitizer
|
|
24
|
+
from .mcp import MCPLifecycle, create_mcp_client, create_mcp_server
|
|
25
|
+
from .renderers import AnsiRenderer
|
|
26
|
+
from .tools import (
|
|
27
|
+
node_as_async_tool,
|
|
28
|
+
node_as_tool,
|
|
29
|
+
)
|
|
30
|
+
from .types import EventType, StreamEvent
|
|
31
|
+
from .utils import cli_errors
|
|
32
|
+
from .wire import EventQueue, make_event_queue
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AnsiRenderer",
|
|
36
|
+
"AppConfig",
|
|
37
|
+
"CircularDependencyError",
|
|
38
|
+
"ConfigInput",
|
|
39
|
+
"ConfigurationError",
|
|
40
|
+
"EventPublisher",
|
|
41
|
+
"EventQueue",
|
|
42
|
+
"EventType",
|
|
43
|
+
"ImportResolutionError",
|
|
44
|
+
"MCPLifecycle",
|
|
45
|
+
"MaxToolCallsGuard",
|
|
46
|
+
"OrchestrationBuilder",
|
|
47
|
+
"ResolvedConfig",
|
|
48
|
+
"ResolvedInfra",
|
|
49
|
+
"SchemaValidationError",
|
|
50
|
+
"StopGuard",
|
|
51
|
+
"StreamEvent",
|
|
52
|
+
"ToolNameSanitizer",
|
|
53
|
+
"UnresolvedReferenceError",
|
|
54
|
+
"cli_errors",
|
|
55
|
+
"create_mcp_client",
|
|
56
|
+
"create_mcp_server",
|
|
57
|
+
"load",
|
|
58
|
+
"load_config",
|
|
59
|
+
"load_session",
|
|
60
|
+
"make_event_queue",
|
|
61
|
+
"node_as_async_tool",
|
|
62
|
+
"node_as_tool",
|
|
63
|
+
"resolve_infra",
|
|
64
|
+
]
|
strands_compose/cli.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""Command-line interface for strands-compose.
|
|
2
|
+
|
|
3
|
+
Exposes two sub-commands:
|
|
4
|
+
|
|
5
|
+
``check``
|
|
6
|
+
Parse and validate a YAML config via :func:`load_config`.
|
|
7
|
+
Pure, fast, zero side-effects — safe to run in CI and pre-deploy hooks.
|
|
8
|
+
|
|
9
|
+
``load``
|
|
10
|
+
Full pipeline via :func:`load` followed by an async MCP health check
|
|
11
|
+
via :func:`validate_mcp`. Starts MCP server processes; always cleans
|
|
12
|
+
them up before exiting.
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
strands-compose check config.yaml
|
|
17
|
+
strands-compose check base.yaml agents.yaml # multi-file merge
|
|
18
|
+
strands-compose load config.yaml [--json]
|
|
19
|
+
strands-compose load config.yaml [--quiet]
|
|
20
|
+
|
|
21
|
+
Exit codes: ``0`` on success, ``1`` on any error or critical health failure.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import asyncio
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
import textwrap
|
|
31
|
+
from importlib.metadata import version as pkg_version
|
|
32
|
+
from typing import TYPE_CHECKING
|
|
33
|
+
|
|
34
|
+
from .config import AppConfig, ConfigInput, load, load_config
|
|
35
|
+
from .startup.report import CheckResult, StartupReport
|
|
36
|
+
from .startup.validator import validate_mcp
|
|
37
|
+
from .utils import cli_errors
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from .config.resolvers import ResolvedConfig
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# ANSI helpers
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
_GREEN = "\033[32m"
|
|
47
|
+
_RED = "\033[31m"
|
|
48
|
+
_YELLOW = "\033[33m"
|
|
49
|
+
_DIM = "\033[2m"
|
|
50
|
+
_BOLD = "\033[1m"
|
|
51
|
+
_RESET = "\033[0m"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _colour(text: str, code: str) -> str:
|
|
55
|
+
"""Wrap *text* in an ANSI colour code when stdout is a TTY.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
text: The string to colour.
|
|
59
|
+
code: An ANSI escape code string (e.g. ``_GREEN``).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Coloured string if stdout is a TTY, plain string otherwise.
|
|
63
|
+
"""
|
|
64
|
+
if sys.stdout.isatty():
|
|
65
|
+
return f"{code}{text}{_RESET}"
|
|
66
|
+
return text
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_version() -> str:
|
|
70
|
+
"""Return the installed package version.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Version string from package metadata, or ``"unknown"`` as fallback.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
return pkg_version("strands-compose")
|
|
77
|
+
except Exception:
|
|
78
|
+
return "unknown"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# check sub-command
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _count_hooks(app_config: AppConfig) -> int:
|
|
87
|
+
"""Count total hook entries across all agents and orchestrations.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
app_config: The validated application config.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Total number of hook entries.
|
|
94
|
+
"""
|
|
95
|
+
total = 0
|
|
96
|
+
for agent_def in app_config.agents.values():
|
|
97
|
+
total += len(agent_def.hooks)
|
|
98
|
+
for orch_def in app_config.orchestrations.values():
|
|
99
|
+
total += len(orch_def.hooks)
|
|
100
|
+
return total
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _render_check_success_ansi(app_config: AppConfig) -> None:
|
|
104
|
+
"""Print a human-readable success summary for the ``check`` sub-command.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
app_config: The validated :class:`AppConfig` returned by
|
|
108
|
+
:func:`load_config`.
|
|
109
|
+
"""
|
|
110
|
+
agent_names = list(app_config.agents)
|
|
111
|
+
orch_names = list(app_config.orchestrations)
|
|
112
|
+
mcp_server_names = list(app_config.mcp_servers)
|
|
113
|
+
mcp_client_names = list(app_config.mcp_clients)
|
|
114
|
+
|
|
115
|
+
agent_str = f"{len(agent_names)} agent{'s' if len(agent_names) != 1 else ''}"
|
|
116
|
+
if agent_names:
|
|
117
|
+
agent_str += f" ({', '.join(agent_names)})"
|
|
118
|
+
|
|
119
|
+
# Collect rows as (label, value) pairs, then align on the colon.
|
|
120
|
+
rows: list[tuple[str, str]] = [
|
|
121
|
+
("entry", str(app_config.entry)),
|
|
122
|
+
("agents", agent_str),
|
|
123
|
+
]
|
|
124
|
+
if app_config.models:
|
|
125
|
+
rows.append(("models", ", ".join(app_config.models)))
|
|
126
|
+
if mcp_server_names:
|
|
127
|
+
rows.append(("mcp servers", ", ".join(mcp_server_names)))
|
|
128
|
+
if mcp_client_names:
|
|
129
|
+
rows.append(("mcp clients", ", ".join(mcp_client_names)))
|
|
130
|
+
if orch_names:
|
|
131
|
+
rows.append(("orchestrations", ", ".join(orch_names)))
|
|
132
|
+
if app_config.session_manager:
|
|
133
|
+
rows.append(("session", str(app_config.session_manager.type)))
|
|
134
|
+
|
|
135
|
+
hook_count = _count_hooks(app_config)
|
|
136
|
+
if hook_count:
|
|
137
|
+
rows.append(("hooks", f"{hook_count} total"))
|
|
138
|
+
|
|
139
|
+
width = max(len(label) for label, _ in rows)
|
|
140
|
+
parts = [_colour("✓ Config valid", _GREEN + _BOLD)]
|
|
141
|
+
for label, value in rows:
|
|
142
|
+
parts.append(f" {label.ljust(width)} : {value}")
|
|
143
|
+
|
|
144
|
+
print("\n".join(parts)) # noqa: T201
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _render_check_success_json(app_config: AppConfig) -> None:
|
|
148
|
+
"""Print a JSON success payload for the ``check`` sub-command.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
app_config: The validated :class:`AppConfig`.
|
|
152
|
+
"""
|
|
153
|
+
payload = {
|
|
154
|
+
"ok": True,
|
|
155
|
+
"stage": "check",
|
|
156
|
+
"version": _get_version(),
|
|
157
|
+
"entry": app_config.entry,
|
|
158
|
+
"agents": list(app_config.agents),
|
|
159
|
+
"models": list(app_config.models),
|
|
160
|
+
"mcp_clients": list(app_config.mcp_clients),
|
|
161
|
+
"mcp_servers": list(app_config.mcp_servers),
|
|
162
|
+
"orchestrations": list(app_config.orchestrations),
|
|
163
|
+
"session_manager": app_config.session_manager.type if app_config.session_manager else None,
|
|
164
|
+
"hooks": _count_hooks(app_config),
|
|
165
|
+
}
|
|
166
|
+
print(json.dumps(payload)) # noqa: T201
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _cmd_check(configs: list[ConfigInput], *, json_output: bool, quiet: bool) -> None:
|
|
170
|
+
"""Run the ``check`` sub-command.
|
|
171
|
+
|
|
172
|
+
Calls :func:`load_config` and prints a success summary or exits with
|
|
173
|
+
code 1 on any validation error (via :func:`cli_errors`).
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
configs: Paths to one or more YAML configuration files.
|
|
177
|
+
json_output: When ``True``, emit JSON instead of ANSI output.
|
|
178
|
+
quiet: When ``True``, suppress output on success (exit code only).
|
|
179
|
+
"""
|
|
180
|
+
with cli_errors():
|
|
181
|
+
config_input = configs[0] if len(configs) == 1 else configs
|
|
182
|
+
app_config = load_config(config_input)
|
|
183
|
+
|
|
184
|
+
if quiet:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if json_output:
|
|
188
|
+
_render_check_success_json(app_config)
|
|
189
|
+
else:
|
|
190
|
+
_render_check_success_ansi(app_config)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# load sub-command
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _render_check_result_ansi(check: CheckResult) -> str:
|
|
199
|
+
"""Format one :class:`CheckResult` as a coloured ANSI line.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
check: The check result to format.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
A single (possibly multi-line) string ready for printing.
|
|
206
|
+
"""
|
|
207
|
+
if check.ok:
|
|
208
|
+
icon = _colour("✓", _GREEN)
|
|
209
|
+
elif check.severity == "warning":
|
|
210
|
+
icon = _colour("⚠", _YELLOW)
|
|
211
|
+
else:
|
|
212
|
+
icon = _colour("✗", _RED)
|
|
213
|
+
|
|
214
|
+
line = f"[{check.category:8s}] {icon} {check.subject}: {check.message}"
|
|
215
|
+
if not check.ok and check.hint:
|
|
216
|
+
indent = " " * 14
|
|
217
|
+
line += f"\n{indent}{_colour('hint:', _DIM)} {check.hint}"
|
|
218
|
+
return line
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _render_report_ansi(report: StartupReport) -> None:
|
|
222
|
+
"""Print a human-readable MCP health report to stdout.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
report: The :class:`StartupReport` from :func:`validate_mcp`.
|
|
226
|
+
"""
|
|
227
|
+
for check in report.checks:
|
|
228
|
+
print(_render_check_result_ansi(check)) # noqa: T201
|
|
229
|
+
|
|
230
|
+
n_ok = len(report.passed_checks)
|
|
231
|
+
n_warn = len(report.warnings)
|
|
232
|
+
n_crit = len(report.critical_checks)
|
|
233
|
+
total = len(report.checks)
|
|
234
|
+
|
|
235
|
+
if total == 0:
|
|
236
|
+
print(_colour("✓ Load OK", _GREEN + _BOLD) + " (no MCP servers/clients configured)") # noqa: T201
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
summary = f"{n_ok}/{total} passed"
|
|
240
|
+
if n_warn:
|
|
241
|
+
summary += f", {n_warn} warning(s)"
|
|
242
|
+
if n_crit:
|
|
243
|
+
summary += f", {n_crit} critical"
|
|
244
|
+
|
|
245
|
+
if report.ok:
|
|
246
|
+
print(_colour(f"✓ Load OK — {summary}", _GREEN + _BOLD)) # noqa: T201
|
|
247
|
+
else:
|
|
248
|
+
print(_colour(f"✗ Load FAILED — {summary}", _RED + _BOLD)) # noqa: T201
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _render_report_json(report: StartupReport) -> None:
|
|
252
|
+
"""Print a JSON health report to stdout.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
report: The :class:`StartupReport` from :func:`validate_mcp`.
|
|
256
|
+
"""
|
|
257
|
+
payload = {
|
|
258
|
+
"ok": report.ok,
|
|
259
|
+
"stage": "load",
|
|
260
|
+
"version": _get_version(),
|
|
261
|
+
"checks": [
|
|
262
|
+
{
|
|
263
|
+
"ok": c.ok,
|
|
264
|
+
"category": c.category,
|
|
265
|
+
"subject": c.subject,
|
|
266
|
+
"message": c.message,
|
|
267
|
+
"severity": c.severity,
|
|
268
|
+
"hint": c.hint,
|
|
269
|
+
}
|
|
270
|
+
for c in report.checks
|
|
271
|
+
],
|
|
272
|
+
}
|
|
273
|
+
print(json.dumps(payload)) # noqa: T201
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def _cmd_load_async(configs: list[ConfigInput], *, json_output: bool, quiet: bool) -> None:
|
|
277
|
+
"""Async body of the ``load`` sub-command.
|
|
278
|
+
|
|
279
|
+
Calls :func:`load`, runs :func:`validate_mcp`, prints the health
|
|
280
|
+
report, and always stops the MCP lifecycle before returning.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
configs: Paths to one or more YAML configuration files.
|
|
284
|
+
json_output: When ``True``, emit JSON instead of ANSI output.
|
|
285
|
+
quiet: When ``True``, suppress output on success (exit code only).
|
|
286
|
+
|
|
287
|
+
Raises:
|
|
288
|
+
SystemExit: With code 1 when any critical MCP health check fails.
|
|
289
|
+
"""
|
|
290
|
+
resolved: ResolvedConfig | None = None
|
|
291
|
+
try:
|
|
292
|
+
with cli_errors():
|
|
293
|
+
config_input = configs[0] if len(configs) == 1 else configs
|
|
294
|
+
resolved = load(config_input)
|
|
295
|
+
|
|
296
|
+
report = await validate_mcp(resolved)
|
|
297
|
+
|
|
298
|
+
if not quiet or not report.ok:
|
|
299
|
+
if json_output:
|
|
300
|
+
_render_report_json(report)
|
|
301
|
+
else:
|
|
302
|
+
_render_report_ansi(report)
|
|
303
|
+
|
|
304
|
+
if not report.ok:
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
finally:
|
|
307
|
+
if resolved is not None:
|
|
308
|
+
resolved.mcp_lifecycle.stop()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _cmd_load(configs: list[ConfigInput], *, json_output: bool, quiet: bool) -> None:
|
|
312
|
+
"""Run the ``load`` sub-command.
|
|
313
|
+
|
|
314
|
+
Delegates to :func:`_cmd_load_async` via :func:`asyncio.run`.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
configs: Paths to one or more YAML configuration files.
|
|
318
|
+
json_output: When ``True``, emit JSON instead of ANSI output.
|
|
319
|
+
quiet: When ``True``, suppress output on success (exit code only).
|
|
320
|
+
"""
|
|
321
|
+
asyncio.run(_cmd_load_async(configs, json_output=json_output, quiet=quiet))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ---------------------------------------------------------------------------
|
|
325
|
+
# Argument parser
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _add_common_flags(parser: argparse.ArgumentParser) -> None:
|
|
330
|
+
"""Add ``--json`` and ``--quiet`` flags shared by both sub-commands.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
parser: The sub-command parser to extend.
|
|
334
|
+
"""
|
|
335
|
+
parser.add_argument(
|
|
336
|
+
"--json", dest="json_output", action="store_true", help="Emit JSON output instead of ANSI"
|
|
337
|
+
)
|
|
338
|
+
parser.add_argument(
|
|
339
|
+
"-q",
|
|
340
|
+
"--quiet",
|
|
341
|
+
action="store_true",
|
|
342
|
+
help="Suppress output on success (exit code only, useful for CI)",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
347
|
+
"""Build and return the top-level argument parser.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Configured :class:`argparse.ArgumentParser`.
|
|
351
|
+
"""
|
|
352
|
+
parser = argparse.ArgumentParser(
|
|
353
|
+
prog="strands-compose",
|
|
354
|
+
description=textwrap.dedent(
|
|
355
|
+
"""\
|
|
356
|
+
strands-compose — YAML-driven multi-agent orchestration
|
|
357
|
+
|
|
358
|
+
Sub-commands:
|
|
359
|
+
check Validate config (no side-effects, safe for CI)
|
|
360
|
+
load Full load + MCP health check
|
|
361
|
+
"""
|
|
362
|
+
),
|
|
363
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
364
|
+
)
|
|
365
|
+
parser.add_argument(
|
|
366
|
+
"-V",
|
|
367
|
+
"--version",
|
|
368
|
+
action="version",
|
|
369
|
+
version=f"%(prog)s {_get_version()}",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
subparsers = parser.add_subparsers(dest="command", metavar="<command>")
|
|
373
|
+
subparsers.required = True
|
|
374
|
+
|
|
375
|
+
# -- check --
|
|
376
|
+
check_parser = subparsers.add_parser(
|
|
377
|
+
"check",
|
|
378
|
+
help="Parse and validate a YAML config (no side-effects)",
|
|
379
|
+
description="Load and validate the config via load_config(). "
|
|
380
|
+
"Checks YAML syntax, schema, variable interpolation, and "
|
|
381
|
+
"cross-references. Exits 0 on success, 1 on any error.",
|
|
382
|
+
)
|
|
383
|
+
check_parser.add_argument(
|
|
384
|
+
"config",
|
|
385
|
+
metavar="CONFIG",
|
|
386
|
+
nargs="+",
|
|
387
|
+
help="Path(s) to YAML config file(s). Multiple files are merged left-to-right.",
|
|
388
|
+
)
|
|
389
|
+
_add_common_flags(check_parser)
|
|
390
|
+
|
|
391
|
+
# -- load --
|
|
392
|
+
load_parser = subparsers.add_parser(
|
|
393
|
+
"load",
|
|
394
|
+
help="Full load pipeline + MCP health check",
|
|
395
|
+
description="Run the full load() pipeline (starts MCP servers, builds agents) "
|
|
396
|
+
"then probe MCP connectivity. Always stops MCP servers on exit. "
|
|
397
|
+
"Exits 0 on success, 1 on any error or critical health failure.",
|
|
398
|
+
)
|
|
399
|
+
load_parser.add_argument(
|
|
400
|
+
"config",
|
|
401
|
+
metavar="CONFIG",
|
|
402
|
+
nargs="+",
|
|
403
|
+
help="Path(s) to YAML config file(s). Multiple files are merged left-to-right.",
|
|
404
|
+
)
|
|
405
|
+
_add_common_flags(load_parser)
|
|
406
|
+
|
|
407
|
+
return parser
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# Entry point
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def main() -> None:
|
|
416
|
+
"""CLI entry point for the ``strands-compose`` command.
|
|
417
|
+
|
|
418
|
+
Dispatches to :func:`_cmd_check` or :func:`_cmd_load` based on the
|
|
419
|
+
sub-command supplied on the command line.
|
|
420
|
+
"""
|
|
421
|
+
parser = _build_parser()
|
|
422
|
+
args = parser.parse_args()
|
|
423
|
+
|
|
424
|
+
if args.command == "check":
|
|
425
|
+
_cmd_check(args.config, json_output=args.json_output, quiet=args.quiet)
|
|
426
|
+
else:
|
|
427
|
+
_cmd_load(args.config, json_output=args.json_output, quiet=args.quiet)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""YAML configuration loading, validation, and resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .interpolation import interpolate, strip_anchors
|
|
6
|
+
from .loaders import ConfigInput, load, load_config, load_session
|
|
7
|
+
from .resolvers import ResolvedConfig, ResolvedInfra, resolve_infra
|
|
8
|
+
from .schema import (
|
|
9
|
+
COLLECTION_KEYS,
|
|
10
|
+
JOINT_NAMESPACES,
|
|
11
|
+
AgentDef,
|
|
12
|
+
AppConfig,
|
|
13
|
+
ConversationManagerDef,
|
|
14
|
+
DelegateConnectionDef,
|
|
15
|
+
DelegateOrchestrationDef,
|
|
16
|
+
GraphEdgeDef,
|
|
17
|
+
GraphOrchestrationDef,
|
|
18
|
+
HookDef,
|
|
19
|
+
MCPClientDef,
|
|
20
|
+
MCPServerDef,
|
|
21
|
+
ModelDef,
|
|
22
|
+
OrchestrationDef,
|
|
23
|
+
SessionManagerDef,
|
|
24
|
+
SwarmOrchestrationDef,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AgentDef",
|
|
29
|
+
"AppConfig",
|
|
30
|
+
"COLLECTION_KEYS",
|
|
31
|
+
"ConversationManagerDef",
|
|
32
|
+
"JOINT_NAMESPACES",
|
|
33
|
+
"ConfigInput",
|
|
34
|
+
"DelegateConnectionDef",
|
|
35
|
+
"DelegateOrchestrationDef",
|
|
36
|
+
"GraphEdgeDef",
|
|
37
|
+
"GraphOrchestrationDef",
|
|
38
|
+
"HookDef",
|
|
39
|
+
"MCPClientDef",
|
|
40
|
+
"MCPServerDef",
|
|
41
|
+
"ModelDef",
|
|
42
|
+
"OrchestrationDef",
|
|
43
|
+
"SessionManagerDef",
|
|
44
|
+
"SwarmOrchestrationDef",
|
|
45
|
+
"ResolvedConfig",
|
|
46
|
+
"ResolvedInfra",
|
|
47
|
+
"interpolate",
|
|
48
|
+
"load",
|
|
49
|
+
"load_config",
|
|
50
|
+
"load_session",
|
|
51
|
+
"resolve_infra",
|
|
52
|
+
"strip_anchors",
|
|
53
|
+
]
|