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.
Files changed (50) hide show
  1. swarmkit_runtime/__init__.py +6 -0
  2. swarmkit_runtime/_workspace_runtime.py +308 -0
  3. swarmkit_runtime/archetypes/__init__.py +163 -0
  4. swarmkit_runtime/audit/__init__.py +13 -0
  5. swarmkit_runtime/authoring/__init__.py +10 -0
  6. swarmkit_runtime/authoring/_agent.py +289 -0
  7. swarmkit_runtime/authoring/_prompts.py +486 -0
  8. swarmkit_runtime/authoring/_tools.py +160 -0
  9. swarmkit_runtime/cli/__init__.py +662 -0
  10. swarmkit_runtime/cli/_knowledge.py +283 -0
  11. swarmkit_runtime/cli/_render.py +243 -0
  12. swarmkit_runtime/errors/__init__.py +104 -0
  13. swarmkit_runtime/gaps/__init__.py +107 -0
  14. swarmkit_runtime/governance/__init__.py +115 -0
  15. swarmkit_runtime/governance/_mock.py +98 -0
  16. swarmkit_runtime/governance/agt_provider.py +194 -0
  17. swarmkit_runtime/knowledge/__init__.py +1 -0
  18. swarmkit_runtime/knowledge/__main__.py +5 -0
  19. swarmkit_runtime/knowledge/_server.py +437 -0
  20. swarmkit_runtime/langgraph_compiler/__init__.py +16 -0
  21. swarmkit_runtime/langgraph_compiler/_compiler.py +679 -0
  22. swarmkit_runtime/langgraph_compiler/_panel.py +131 -0
  23. swarmkit_runtime/langgraph_compiler/_skill_executor.py +185 -0
  24. swarmkit_runtime/langgraph_compiler/_state.py +40 -0
  25. swarmkit_runtime/mcp/__init__.py +8 -0
  26. swarmkit_runtime/mcp/_client.py +259 -0
  27. swarmkit_runtime/model_providers/__init__.py +45 -0
  28. swarmkit_runtime/model_providers/_anthropic.py +148 -0
  29. swarmkit_runtime/model_providers/_google.py +187 -0
  30. swarmkit_runtime/model_providers/_mock.py +71 -0
  31. swarmkit_runtime/model_providers/_ollama.py +133 -0
  32. swarmkit_runtime/model_providers/_openai.py +155 -0
  33. swarmkit_runtime/model_providers/_openai_compat.py +64 -0
  34. swarmkit_runtime/model_providers/_registry.py +62 -0
  35. swarmkit_runtime/model_providers/_types.py +76 -0
  36. swarmkit_runtime/resolver/__init__.py +420 -0
  37. swarmkit_runtime/resolver/_resolved.py +85 -0
  38. swarmkit_runtime/resolver/_topology.py +506 -0
  39. swarmkit_runtime/resolver/_triggers.py +100 -0
  40. swarmkit_runtime/review/__init__.py +118 -0
  41. swarmkit_runtime/review/_hitl.py +60 -0
  42. swarmkit_runtime/server.py +134 -0
  43. swarmkit_runtime/skills/__init__.py +242 -0
  44. swarmkit_runtime/skills/_output_validator.py +161 -0
  45. swarmkit_runtime/topology/__init__.py +9 -0
  46. swarmkit_runtime/workspace/__init__.py +231 -0
  47. swarmkit_runtime-1.0.0.dist-info/METADATA +87 -0
  48. swarmkit_runtime-1.0.0.dist-info/RECORD +50 -0
  49. swarmkit_runtime-1.0.0.dist-info/WHEEL +4 -0
  50. swarmkit_runtime-1.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,6 @@
1
+ """SwarmKit runtime — topology interpreter, LangGraph compiler, governance wiring.
2
+
3
+ See `design/SwarmKit-Design-v0.6.md` §9 and §14 for architectural context.
4
+ """
5
+
6
+ __version__ = "0.0.1"
@@ -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
+ """
@@ -0,0 +1,10 @@
1
+ """Conversational authoring — interactive artifact generation (M3.5).
2
+
3
+ See ``design/details/conversational-authoring.md``.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ._agent import run_authoring_session
9
+
10
+ __all__ = ["run_authoring_session"]