swarmkit-runtime 1.0.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.
- swarmkit_runtime/__init__.py +6 -0
- swarmkit_runtime/_workspace_runtime.py +308 -0
- swarmkit_runtime/archetypes/__init__.py +163 -0
- swarmkit_runtime/audit/__init__.py +13 -0
- swarmkit_runtime/authoring/__init__.py +10 -0
- swarmkit_runtime/authoring/_agent.py +289 -0
- swarmkit_runtime/authoring/_prompts.py +486 -0
- swarmkit_runtime/authoring/_tools.py +160 -0
- swarmkit_runtime/cli/__init__.py +662 -0
- swarmkit_runtime/cli/_knowledge.py +283 -0
- swarmkit_runtime/cli/_render.py +243 -0
- swarmkit_runtime/errors/__init__.py +104 -0
- swarmkit_runtime/gaps/__init__.py +107 -0
- swarmkit_runtime/governance/__init__.py +115 -0
- swarmkit_runtime/governance/_mock.py +98 -0
- swarmkit_runtime/governance/agt_provider.py +194 -0
- swarmkit_runtime/knowledge/__init__.py +1 -0
- swarmkit_runtime/knowledge/__main__.py +5 -0
- swarmkit_runtime/knowledge/_server.py +437 -0
- swarmkit_runtime/langgraph_compiler/__init__.py +16 -0
- swarmkit_runtime/langgraph_compiler/_compiler.py +679 -0
- swarmkit_runtime/langgraph_compiler/_panel.py +131 -0
- swarmkit_runtime/langgraph_compiler/_skill_executor.py +185 -0
- swarmkit_runtime/langgraph_compiler/_state.py +40 -0
- swarmkit_runtime/mcp/__init__.py +8 -0
- swarmkit_runtime/mcp/_client.py +259 -0
- swarmkit_runtime/model_providers/__init__.py +45 -0
- swarmkit_runtime/model_providers/_anthropic.py +148 -0
- swarmkit_runtime/model_providers/_google.py +187 -0
- swarmkit_runtime/model_providers/_mock.py +71 -0
- swarmkit_runtime/model_providers/_ollama.py +133 -0
- swarmkit_runtime/model_providers/_openai.py +155 -0
- swarmkit_runtime/model_providers/_openai_compat.py +64 -0
- swarmkit_runtime/model_providers/_registry.py +62 -0
- swarmkit_runtime/model_providers/_types.py +76 -0
- swarmkit_runtime/resolver/__init__.py +420 -0
- swarmkit_runtime/resolver/_resolved.py +85 -0
- swarmkit_runtime/resolver/_topology.py +506 -0
- swarmkit_runtime/resolver/_triggers.py +100 -0
- swarmkit_runtime/review/__init__.py +118 -0
- swarmkit_runtime/review/_hitl.py +60 -0
- swarmkit_runtime/server.py +134 -0
- swarmkit_runtime/skills/__init__.py +242 -0
- swarmkit_runtime/skills/_output_validator.py +161 -0
- swarmkit_runtime/topology/__init__.py +9 -0
- swarmkit_runtime/workspace/__init__.py +231 -0
- swarmkit_runtime-1.0.0.dist-info/METADATA +87 -0
- swarmkit_runtime-1.0.0.dist-info/RECORD +50 -0
- swarmkit_runtime-1.0.0.dist-info/WHEEL +4 -0
- swarmkit_runtime-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""WorkspaceRuntime — the backend that CLI, HTTP server, and web UI call into.
|
|
2
|
+
|
|
3
|
+
Owns the full execution lifecycle: resolve workspace → build providers →
|
|
4
|
+
build governance → wire MCP → compile topology → invoke graph → close.
|
|
5
|
+
The CLI is a thin interface over this; ``swarmkit serve`` (M9) and the
|
|
6
|
+
v1.1 web UI will be additional interfaces over the same class.
|
|
7
|
+
|
|
8
|
+
See ``design/details/workspace-runtime.md`` (to be written) and the
|
|
9
|
+
architectural decision in ``memory/feedback_cli_architecture.md``.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
21
|
+
|
|
22
|
+
from swarmkit_runtime.governance import GovernanceProvider
|
|
23
|
+
from swarmkit_runtime.governance._mock import MockGovernanceProvider
|
|
24
|
+
from swarmkit_runtime.langgraph_compiler import compile_topology
|
|
25
|
+
from swarmkit_runtime.mcp import MCPClientManager, MCPServerConfig, parse_mcp_servers
|
|
26
|
+
from swarmkit_runtime.model_providers import (
|
|
27
|
+
AnthropicModelProvider,
|
|
28
|
+
GoogleModelProvider,
|
|
29
|
+
GroqModelProvider,
|
|
30
|
+
MockModelProvider,
|
|
31
|
+
OllamaModelProvider,
|
|
32
|
+
OpenAIModelProvider,
|
|
33
|
+
OpenRouterModelProvider,
|
|
34
|
+
ProviderRegistry,
|
|
35
|
+
TogetherModelProvider,
|
|
36
|
+
)
|
|
37
|
+
from swarmkit_runtime.model_providers._registry import ModelProviderProtocol
|
|
38
|
+
from swarmkit_runtime.resolver import ResolvedWorkspace, resolve_workspace
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class RunResult:
|
|
43
|
+
"""Output of a topology execution."""
|
|
44
|
+
|
|
45
|
+
output: str
|
|
46
|
+
agent_results: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MissingMCPServerError(Exception):
|
|
50
|
+
"""A skill targets an MCP server that the workspace doesn't declare."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, missing: list[tuple[str, str]]) -> None:
|
|
53
|
+
self.missing = missing
|
|
54
|
+
lines = [
|
|
55
|
+
f"skill '{sid}' targets MCP server '{srv}' but the workspace declares no such server"
|
|
56
|
+
for sid, srv in missing
|
|
57
|
+
]
|
|
58
|
+
super().__init__("\n".join(lines))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class WorkspaceRuntime:
|
|
62
|
+
"""The backend that both CLI and HTTP server call into.
|
|
63
|
+
|
|
64
|
+
Holds a resolved workspace plus all the wired runtime components
|
|
65
|
+
(model providers, governance, MCP manager). Constructed via the
|
|
66
|
+
``from_workspace_path`` classmethod.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
workspace: ResolvedWorkspace,
|
|
73
|
+
workspace_root: Path,
|
|
74
|
+
provider_registry: ProviderRegistry,
|
|
75
|
+
governance: GovernanceProvider,
|
|
76
|
+
mcp_manager: MCPClientManager | None,
|
|
77
|
+
) -> None:
|
|
78
|
+
self._workspace = workspace
|
|
79
|
+
self._workspace_root = workspace_root
|
|
80
|
+
self._provider_registry = provider_registry
|
|
81
|
+
self._governance = governance
|
|
82
|
+
self._mcp_manager = mcp_manager
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_workspace_path(cls, path: Path) -> WorkspaceRuntime:
|
|
86
|
+
"""Build a fully-wired runtime from a workspace directory.
|
|
87
|
+
|
|
88
|
+
Resolves the workspace, registers model providers, selects the
|
|
89
|
+
governance provider, parses MCP server config, and validates
|
|
90
|
+
that every mcp_tool skill targets a configured server.
|
|
91
|
+
|
|
92
|
+
Raises ``ResolutionErrors`` if the workspace is invalid, or
|
|
93
|
+
``MissingMCPServerError`` if skills reference unconfigured
|
|
94
|
+
MCP servers.
|
|
95
|
+
"""
|
|
96
|
+
ws_root = path.resolve()
|
|
97
|
+
workspace = resolve_workspace(ws_root)
|
|
98
|
+
|
|
99
|
+
registry = ProviderRegistry()
|
|
100
|
+
register_available_providers(registry)
|
|
101
|
+
|
|
102
|
+
governance = build_governance(workspace, ws_root)
|
|
103
|
+
|
|
104
|
+
mcp_configs = parse_mcp_servers(getattr(workspace.raw, "mcp_servers", None))
|
|
105
|
+
mcp_manager = MCPClientManager(mcp_configs, workspace_root=ws_root) if mcp_configs else None
|
|
106
|
+
|
|
107
|
+
missing = find_missing_mcp_servers(workspace, mcp_configs)
|
|
108
|
+
if missing:
|
|
109
|
+
raise MissingMCPServerError(missing)
|
|
110
|
+
|
|
111
|
+
return cls(
|
|
112
|
+
workspace=workspace,
|
|
113
|
+
workspace_root=ws_root,
|
|
114
|
+
provider_registry=registry,
|
|
115
|
+
governance=governance,
|
|
116
|
+
mcp_manager=mcp_manager,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def compile(self, topology_name: str) -> CompiledStateGraph[Any]:
|
|
120
|
+
"""Compile a named topology into a LangGraph graph.
|
|
121
|
+
|
|
122
|
+
Raises ``KeyError`` if the topology doesn't exist.
|
|
123
|
+
"""
|
|
124
|
+
if topology_name not in self._workspace.topologies:
|
|
125
|
+
available = sorted(self._workspace.topologies.keys())
|
|
126
|
+
raise KeyError(
|
|
127
|
+
f"Topology '{topology_name}' not found. "
|
|
128
|
+
f"Available: {', '.join(available) or '(none)'}."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
topology = self._workspace.topologies[topology_name]
|
|
132
|
+
return compile_topology(
|
|
133
|
+
topology,
|
|
134
|
+
provider_registry=self._provider_registry,
|
|
135
|
+
governance=self._governance,
|
|
136
|
+
mcp_manager=self._mcp_manager,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def run(
|
|
140
|
+
self,
|
|
141
|
+
topology_name: str,
|
|
142
|
+
user_input: str,
|
|
143
|
+
*,
|
|
144
|
+
max_steps: int = 10,
|
|
145
|
+
) -> RunResult:
|
|
146
|
+
"""Execute a topology end-to-end and return the result.
|
|
147
|
+
|
|
148
|
+
Handles MCP session lifecycle (start_all / close_all) within
|
|
149
|
+
the same async task so the SDK's anyio task groups unwind cleanly.
|
|
150
|
+
"""
|
|
151
|
+
graph = self.compile(topology_name)
|
|
152
|
+
|
|
153
|
+
if self._mcp_manager is not None:
|
|
154
|
+
await self._mcp_manager.start_all()
|
|
155
|
+
try:
|
|
156
|
+
result = await graph.ainvoke(
|
|
157
|
+
{
|
|
158
|
+
"input": user_input,
|
|
159
|
+
"messages": [],
|
|
160
|
+
"agent_results": {},
|
|
161
|
+
"current_agent": "",
|
|
162
|
+
"output": "",
|
|
163
|
+
},
|
|
164
|
+
config={"recursion_limit": max_steps},
|
|
165
|
+
)
|
|
166
|
+
finally:
|
|
167
|
+
if self._mcp_manager is not None:
|
|
168
|
+
await self._mcp_manager.close_all()
|
|
169
|
+
|
|
170
|
+
return RunResult(
|
|
171
|
+
output=result.get("output", ""),
|
|
172
|
+
agent_results={
|
|
173
|
+
k: str(v) for k, v in result.get("agent_results", {}).items() if isinstance(v, str)
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
async def close(self) -> None:
|
|
178
|
+
"""Release all held resources."""
|
|
179
|
+
if self._mcp_manager is not None:
|
|
180
|
+
await self._mcp_manager.close_all()
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def workspace(self) -> ResolvedWorkspace:
|
|
184
|
+
return self._workspace
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def workspace_root(self) -> Path:
|
|
188
|
+
return self._workspace_root
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def governance(self) -> GovernanceProvider:
|
|
192
|
+
return self._governance
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def mcp_manager(self) -> MCPClientManager | None:
|
|
196
|
+
return self._mcp_manager
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def provider_registry(self) -> ProviderRegistry:
|
|
200
|
+
return self._provider_registry
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---- helpers (public — used by CLI and tests) ----------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def register_available_providers(registry: ProviderRegistry) -> None:
|
|
207
|
+
"""Register all model providers whose credentials are in the environment."""
|
|
208
|
+
registry.register(MockModelProvider())
|
|
209
|
+
|
|
210
|
+
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
211
|
+
registry.register(AnthropicModelProvider())
|
|
212
|
+
if os.environ.get("GOOGLE_API_KEY"):
|
|
213
|
+
registry.register(GoogleModelProvider())
|
|
214
|
+
if os.environ.get("OPENAI_API_KEY"):
|
|
215
|
+
registry.register(OpenAIModelProvider())
|
|
216
|
+
if os.environ.get("OPENROUTER_API_KEY"):
|
|
217
|
+
registry.register(OpenRouterModelProvider())
|
|
218
|
+
if os.environ.get("GROQ_API_KEY"):
|
|
219
|
+
registry.register(GroqModelProvider())
|
|
220
|
+
if os.environ.get("TOGETHER_API_KEY"):
|
|
221
|
+
registry.register(TogetherModelProvider())
|
|
222
|
+
|
|
223
|
+
registry.register(OllamaModelProvider())
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def build_governance(workspace: ResolvedWorkspace, ws_root: Path) -> GovernanceProvider:
|
|
227
|
+
"""Select the GovernanceProvider based on workspace.yaml's governance block."""
|
|
228
|
+
gov = getattr(workspace.raw, "governance", None)
|
|
229
|
+
if gov is None:
|
|
230
|
+
return MockGovernanceProvider(allow_all=True)
|
|
231
|
+
|
|
232
|
+
provider_value = gov.provider.value if hasattr(gov.provider, "value") else str(gov.provider)
|
|
233
|
+
|
|
234
|
+
if provider_value == "agt":
|
|
235
|
+
from swarmkit_runtime.governance.agt_provider import AGTGovernanceProvider # noqa: PLC0415
|
|
236
|
+
|
|
237
|
+
config = gov.config or {}
|
|
238
|
+
policies_dir = ws_root / config.get("policies_dir", "policies")
|
|
239
|
+
audit_db = ws_root / ".swarmkit" / "audit.db"
|
|
240
|
+
audit_db.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
return AGTGovernanceProvider.from_config(
|
|
242
|
+
policy_dir=policies_dir,
|
|
243
|
+
audit_db=audit_db,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if provider_value == "custom":
|
|
247
|
+
print(
|
|
248
|
+
"warning: governance.provider=custom is not yet supported; "
|
|
249
|
+
"falling back to mock. See design §8.5 for the plugin path.",
|
|
250
|
+
file=sys.stderr,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return MockGovernanceProvider(allow_all=True)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def resolve_authoring_provider(
|
|
257
|
+
registry: ProviderRegistry | None = None,
|
|
258
|
+
) -> tuple[ModelProviderProtocol, str]:
|
|
259
|
+
"""Resolve which model provider + model name to use for authoring.
|
|
260
|
+
|
|
261
|
+
Checks SWARMKIT_AUTHOR_MODEL (format: provider/model), then falls
|
|
262
|
+
back to SWARMKIT_PROVIDER + SWARMKIT_MODEL, then first available
|
|
263
|
+
real provider.
|
|
264
|
+
"""
|
|
265
|
+
author_model = os.environ.get("SWARMKIT_AUTHOR_MODEL", "")
|
|
266
|
+
if "/" in author_model:
|
|
267
|
+
provider_id, model_name = author_model.split("/", 1)
|
|
268
|
+
else:
|
|
269
|
+
provider_id = os.environ.get("SWARMKIT_PROVIDER", "")
|
|
270
|
+
model_name = os.environ.get("SWARMKIT_MODEL", "")
|
|
271
|
+
|
|
272
|
+
if registry is None:
|
|
273
|
+
registry = ProviderRegistry()
|
|
274
|
+
register_available_providers(registry)
|
|
275
|
+
|
|
276
|
+
if provider_id:
|
|
277
|
+
provider = registry.get(provider_id)
|
|
278
|
+
if provider is not None:
|
|
279
|
+
return provider, model_name or "claude-sonnet-4-6"
|
|
280
|
+
|
|
281
|
+
for pid in registry.provider_ids:
|
|
282
|
+
if pid == "mock":
|
|
283
|
+
continue
|
|
284
|
+
provider = registry.get(pid)
|
|
285
|
+
if provider is not None:
|
|
286
|
+
return provider, model_name or "claude-sonnet-4-6"
|
|
287
|
+
|
|
288
|
+
raise RuntimeError(
|
|
289
|
+
"No model provider available. Set SWARMKIT_PROVIDER "
|
|
290
|
+
"and the corresponding API key (e.g. GROQ_API_KEY)."
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def find_missing_mcp_servers(
|
|
295
|
+
workspace: ResolvedWorkspace,
|
|
296
|
+
mcp_configs: dict[str, MCPServerConfig],
|
|
297
|
+
) -> list[tuple[str, str]]:
|
|
298
|
+
"""Return ``(skill_id, server_id)`` pairs whose mcp_tool target is unconfigured."""
|
|
299
|
+
missing: list[tuple[str, str]] = []
|
|
300
|
+
for skill_id, skill in workspace.skills.items():
|
|
301
|
+
impl = skill.raw.implementation
|
|
302
|
+
impl_type = impl.get("type") if isinstance(impl, dict) else getattr(impl, "type", None)
|
|
303
|
+
if impl_type != "mcp_tool":
|
|
304
|
+
continue
|
|
305
|
+
server_id = impl.get("server") if isinstance(impl, dict) else getattr(impl, "server", "")
|
|
306
|
+
if server_id and server_id not in mcp_configs:
|
|
307
|
+
missing.append((skill_id, server_id))
|
|
308
|
+
return missing
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Archetype registry + skill-reference verification (M1.4 / phase 3b).
|
|
2
|
+
|
|
3
|
+
Given a list of archetype ``DiscoveredArtifact`` and the already-built
|
|
4
|
+
skill registry (from :mod:`swarmkit_runtime.skills`), produce a registry
|
|
5
|
+
of :class:`ResolvedArchetype` keyed by archetype ID.
|
|
6
|
+
|
|
7
|
+
Concrete skill references in an archetype's ``defaults.skills`` list are
|
|
8
|
+
verified against the skill registry. Abstract-skill placeholders
|
|
9
|
+
(``{ abstract: { category, capability? } }`` per §6.6) are left
|
|
10
|
+
unchecked here — they bind to a concrete skill at topology-load time in
|
|
11
|
+
phase 3c (M1.5).
|
|
12
|
+
|
|
13
|
+
Design reference: ``design/details/topology-loader.md`` phase 3b.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Iterable, Mapping
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
23
|
+
from swarmkit_schema.models import SwarmKitArchetype
|
|
24
|
+
|
|
25
|
+
from swarmkit_runtime.errors import ResolutionError
|
|
26
|
+
from swarmkit_runtime.skills import ResolvedSkill
|
|
27
|
+
from swarmkit_runtime.workspace import DiscoveredArtifact
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ResolvedArchetype:
|
|
32
|
+
"""An archetype whose concrete skill references have been verified
|
|
33
|
+
against the workspace skill registry.
|
|
34
|
+
|
|
35
|
+
Abstract-skill placeholders in ``raw.defaults.skills`` are retained
|
|
36
|
+
as-is — the topology resolver (M1.5) binds them per-agent.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
id: str
|
|
40
|
+
raw: SwarmKitArchetype
|
|
41
|
+
source_path: Path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_archetype_registry(
|
|
45
|
+
artifacts: Iterable[DiscoveredArtifact],
|
|
46
|
+
skill_registry: Mapping[str, ResolvedSkill],
|
|
47
|
+
) -> tuple[Mapping[str, ResolvedArchetype], list[ResolutionError]]:
|
|
48
|
+
"""Build a registry of :class:`ResolvedArchetype` keyed by ID.
|
|
49
|
+
|
|
50
|
+
Returns ``(registry, errors)``. Every concrete skill reference in an
|
|
51
|
+
archetype's defaults must exist in ``skill_registry``; unknown refs
|
|
52
|
+
surface as ``archetype.unknown-skill`` entries. Duplicate archetype
|
|
53
|
+
IDs surface as ``archetype.duplicate-id``.
|
|
54
|
+
"""
|
|
55
|
+
errors: list[ResolutionError] = []
|
|
56
|
+
registry: dict[str, ResolvedArchetype] = {}
|
|
57
|
+
|
|
58
|
+
for artifact in artifacts:
|
|
59
|
+
if artifact.kind != "archetype":
|
|
60
|
+
continue
|
|
61
|
+
try:
|
|
62
|
+
model = SwarmKitArchetype.model_validate(dict(artifact.raw))
|
|
63
|
+
except PydanticValidationError as exc:
|
|
64
|
+
errors.append(
|
|
65
|
+
ResolutionError(
|
|
66
|
+
code="archetype.model-construction",
|
|
67
|
+
message=(
|
|
68
|
+
f"archetype at {artifact.path} could not be "
|
|
69
|
+
"constructed as a pydantic SwarmKitArchetype model."
|
|
70
|
+
),
|
|
71
|
+
artifact_path=artifact.path,
|
|
72
|
+
suggestion=f"pydantic raised: {exc.errors()[0]['msg']}",
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
continue
|
|
76
|
+
arch_id = _identifier_str(model.metadata.id)
|
|
77
|
+
if arch_id in registry:
|
|
78
|
+
prior_path = registry[arch_id].source_path
|
|
79
|
+
errors.append(
|
|
80
|
+
ResolutionError(
|
|
81
|
+
code="archetype.duplicate-id",
|
|
82
|
+
message=(
|
|
83
|
+
f"Archetype id {arch_id!r} is declared twice: "
|
|
84
|
+
f"first at {prior_path}, again at {artifact.path}."
|
|
85
|
+
),
|
|
86
|
+
artifact_path=artifact.path,
|
|
87
|
+
yaml_pointer="/metadata/id",
|
|
88
|
+
suggestion=(
|
|
89
|
+
"Rename one of the archetypes so every archetype ID "
|
|
90
|
+
"is unique within the workspace."
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Verify every concrete skill reference exists. Abstract
|
|
97
|
+
# placeholders skipped here; their binding happens in phase 3c.
|
|
98
|
+
skill_errors = _check_skill_refs(arch_id, artifact.path, model, skill_registry)
|
|
99
|
+
errors.extend(skill_errors)
|
|
100
|
+
# Even with skill errors, keep the archetype in the registry so
|
|
101
|
+
# later phases can produce more useful cross-references.
|
|
102
|
+
registry[arch_id] = ResolvedArchetype(id=arch_id, raw=model, source_path=artifact.path)
|
|
103
|
+
|
|
104
|
+
return registry, errors
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _check_skill_refs(
|
|
108
|
+
arch_id: str,
|
|
109
|
+
source: Path,
|
|
110
|
+
model: SwarmKitArchetype,
|
|
111
|
+
skill_registry: Mapping[str, ResolvedSkill],
|
|
112
|
+
) -> list[ResolutionError]:
|
|
113
|
+
errors: list[ResolutionError] = []
|
|
114
|
+
defaults = model.defaults
|
|
115
|
+
skills = getattr(defaults, "skills", None)
|
|
116
|
+
if not skills:
|
|
117
|
+
return errors
|
|
118
|
+
for index, entry in enumerate(skills):
|
|
119
|
+
# Entry is either a bare identifier string or an object with
|
|
120
|
+
# ``abstract: { category, capability? }``. Only the concrete
|
|
121
|
+
# case is checked here.
|
|
122
|
+
if _is_abstract_skill(entry):
|
|
123
|
+
continue
|
|
124
|
+
skill_id = _identifier_str(entry)
|
|
125
|
+
if skill_id not in skill_registry:
|
|
126
|
+
errors.append(
|
|
127
|
+
ResolutionError(
|
|
128
|
+
code="archetype.unknown-skill",
|
|
129
|
+
message=(
|
|
130
|
+
f"Archetype {arch_id!r} references skill "
|
|
131
|
+
f"{skill_id!r} which is not defined in this workspace."
|
|
132
|
+
),
|
|
133
|
+
artifact_path=source,
|
|
134
|
+
yaml_pointer=f"/defaults/skills/{index}",
|
|
135
|
+
suggestion=(
|
|
136
|
+
f"Define a skill with id={skill_id!r}, or change "
|
|
137
|
+
"the reference to an existing one. You can also "
|
|
138
|
+
"use an abstract placeholder "
|
|
139
|
+
"({ abstract: { category, capability? } }) if the "
|
|
140
|
+
"archetype should be concrete-skill-agnostic."
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
return errors
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_abstract_skill(entry: object) -> bool:
|
|
148
|
+
# The skill entry union is string | { abstract: {...} }. pydantic
|
|
149
|
+
# materialises the string branch as a plain string and the object
|
|
150
|
+
# branch as an Abstract-containing object. Duck-type check:
|
|
151
|
+
# anything with an `abstract` attribute that isn't None is abstract.
|
|
152
|
+
return getattr(entry, "abstract", None) is not None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _identifier_str(value: object) -> str:
|
|
156
|
+
root = getattr(value, "root", value)
|
|
157
|
+
return str(root)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
"ResolvedArchetype",
|
|
162
|
+
"build_archetype_registry",
|
|
163
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Audit and skill-gap logging (design §5.4, §14.5, §16.4).
|
|
2
|
+
|
|
3
|
+
Audit events flow into AGT's Agent SRE telemetry pipeline via the
|
|
4
|
+
`GovernanceProvider.record_event` interface. This module adds SwarmKit-specific
|
|
5
|
+
event types on top:
|
|
6
|
+
|
|
7
|
+
- skill gap events (design §12.1)
|
|
8
|
+
- review queue state changes
|
|
9
|
+
- gap surfacing decisions
|
|
10
|
+
- authoring swarm conversations
|
|
11
|
+
|
|
12
|
+
All writes are append-only. No code path in this module exposes update/delete.
|
|
13
|
+
"""
|