multiagentos 0.4.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 (122) hide show
  1. agents/__init__.py +1 -0
  2. agents/catalog.py +52 -0
  3. agents/registry.py +11 -0
  4. core/__init__.py +38 -0
  5. core/artifacts.py +47 -0
  6. core/chat_agent_bridge.py +280 -0
  7. core/chat_agent_registry.py +61 -0
  8. core/chat_agent_router.py +85 -0
  9. core/chat_session.py +62 -0
  10. core/contracts/__init__.py +22 -0
  11. core/contracts/agent.py +21 -0
  12. core/contracts/ai.py +25 -0
  13. core/contracts/capability.py +44 -0
  14. core/contracts/chat_agent.py +34 -0
  15. core/contracts/checkpoint.py +78 -0
  16. core/contracts/discovery.py +30 -0
  17. core/contracts/execution.py +28 -0
  18. core/contracts/github.py +74 -0
  19. core/contracts/handoff.py +56 -0
  20. core/contracts/health.py +206 -0
  21. core/contracts/human_review.py +20 -0
  22. core/contracts/ide.py +113 -0
  23. core/contracts/mcp.py +76 -0
  24. core/contracts/model_runtime.py +25 -0
  25. core/contracts/planning.py +25 -0
  26. core/contracts/profile.py +66 -0
  27. core/contracts/quota.py +62 -0
  28. core/contracts/repository.py +34 -0
  29. core/contracts/resume.py +55 -0
  30. core/contracts/runtime.py +19 -0
  31. core/contracts/vyrelon_runtime.py +118 -0
  32. core/contracts/work_unit.py +48 -0
  33. core/delegation.py +33 -0
  34. core/handoff.py +68 -0
  35. core/lifecycle.py +85 -0
  36. core/multi_agent_workflow.py +574 -0
  37. core/orchestrator.py +51 -0
  38. core/planning.py +22 -0
  39. core/registry.py +44 -0
  40. core/routing.py +195 -0
  41. core/state.py +155 -0
  42. installer/__init__.py +1 -0
  43. installer/init.py +71 -0
  44. integrations/__init__.py +0 -0
  45. integrations/github/__init__.py +3 -0
  46. integrations/github/gateway.py +164 -0
  47. integrations/openai/__init__.py +1 -0
  48. integrations/openai/chat_agent.py +147 -0
  49. multiagentos/__init__.py +1 -0
  50. multiagentos/cli.py +759 -0
  51. multiagentos-0.4.0.dist-info/METADATA +7 -0
  52. multiagentos-0.4.0.dist-info/RECORD +122 -0
  53. multiagentos-0.4.0.dist-info/WHEEL +5 -0
  54. multiagentos-0.4.0.dist-info/entry_points.txt +2 -0
  55. multiagentos-0.4.0.dist-info/top_level.txt +7 -0
  56. profiles/__init__.py +1 -0
  57. profiles/common.py +11 -0
  58. profiles/detector.py +67 -0
  59. profiles/registry.py +46 -0
  60. profiles/resolver.py +63 -0
  61. runtime/__init__.py +5 -0
  62. runtime/agent/__init__.py +1 -0
  63. runtime/agent/ide.py +74 -0
  64. runtime/agent/model.py +197 -0
  65. runtime/agent/model_backed.py +49 -0
  66. runtime/agent/process.py +21 -0
  67. runtime/builtin_tools.py +57 -0
  68. runtime/capability.py +143 -0
  69. runtime/chat_adapter_registry.py +43 -0
  70. runtime/chat_config.py +57 -0
  71. runtime/chat_execution.py +52 -0
  72. runtime/config/__init__.py +1 -0
  73. runtime/config/loader.py +67 -0
  74. runtime/discovery.py +334 -0
  75. runtime/execution_config.py +71 -0
  76. runtime/execution_registry.py +50 -0
  77. runtime/git.py +84 -0
  78. runtime/github.py +56 -0
  79. runtime/github_probe.py +23 -0
  80. runtime/health.py +14 -0
  81. runtime/ide/__init__.py +6 -0
  82. runtime/ide/bridge.py +198 -0
  83. runtime/ide/registry.py +24 -0
  84. runtime/ide/runtime.py +35 -0
  85. runtime/local/__init__.py +18 -0
  86. runtime/local/filesystem.py +58 -0
  87. runtime/local/patch.py +79 -0
  88. runtime/local/path_security.py +31 -0
  89. runtime/local/permissions.py +20 -0
  90. runtime/local/process.py +56 -0
  91. runtime/local/shell.py +97 -0
  92. runtime/mcp/__init__.py +7 -0
  93. runtime/mcp/client.py +120 -0
  94. runtime/mcp/config.py +36 -0
  95. runtime/mcp/policy.py +57 -0
  96. runtime/mcp/proxy.py +30 -0
  97. runtime/mcp/server.py +137 -0
  98. runtime/mcp/session.py +19 -0
  99. runtime/model/__init__.py +5 -0
  100. runtime/model/ai_runtime.py +165 -0
  101. runtime/model/cli.py +49 -0
  102. runtime/model/config.py +98 -0
  103. runtime/model/credentials.py +42 -0
  104. runtime/model/factory.py +82 -0
  105. runtime/model/http.py +78 -0
  106. runtime/model/invoker.py +16 -0
  107. runtime/model/materializer.py +101 -0
  108. runtime/model/native.py +244 -0
  109. runtime/model/providers.py +55 -0
  110. runtime/model/registry.py +26 -0
  111. runtime/model/runtime_loader.py +51 -0
  112. runtime/model_control.py +136 -0
  113. runtime/multi_agent.py +142 -0
  114. runtime/policy.py +28 -0
  115. runtime/process.py +41 -0
  116. runtime/quota.py +259 -0
  117. runtime/repository.py +103 -0
  118. runtime/repository_tools.py +62 -0
  119. runtime/status.py +87 -0
  120. runtime/tool_calling.py +134 -0
  121. runtime/validation.py +121 -0
  122. runtime/vyrelon.py +1557 -0
@@ -0,0 +1,49 @@
1
+ """Agent executor backed by the provider-neutral model runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from core.contracts.agent import AgentContract
8
+ from core.contracts.ai import ModelSpec
9
+ from core.contracts.model_runtime import ModelRequest, ModelResponse
10
+ from core.contracts.work_unit import WorkUnit
11
+ from core.registry import AIRegistry
12
+ from runtime.model.invoker import ModelInvoker
13
+
14
+
15
+ class ModelBackedAgentExecutor:
16
+ """Turn an assigned model into an executable VYRELON Agent."""
17
+
18
+ def __init__(self, ai_registry: AIRegistry, model_invoker: ModelInvoker) -> None:
19
+ self.ai_registry = ai_registry
20
+ self.model_invoker = model_invoker
21
+
22
+ def execute(
23
+ self, *, agent: AgentContract, model_id: str, work_unit: WorkUnit
24
+ ) -> ModelResponse:
25
+ model = self.ai_registry.model(model_id)
26
+ request = ModelRequest(
27
+ system=self._system_prompt(agent),
28
+ prompt=self._work_prompt(work_unit),
29
+ metadata={
30
+ "agent_id": agent.id,
31
+ "role": agent.role,
32
+ "work_unit_id": work_unit.id,
33
+ },
34
+ )
35
+ return self.model_invoker.generate(model, request)
36
+
37
+ @staticmethod
38
+ def _system_prompt(agent: AgentContract) -> str:
39
+ capabilities = ", ".join(sorted(agent.capabilities)) or "general development"
40
+ return (
41
+ f"You are the VYRELON {agent.role} agent. "
42
+ f"Your capabilities are: {capabilities}. "
43
+ "Work only within the objective and permissions provided by the orchestrator."
44
+ )
45
+
46
+ @staticmethod
47
+ def _work_prompt(work_unit: WorkUnit) -> str:
48
+ inputs = json.dumps(work_unit.inputs, ensure_ascii=False, sort_keys=True, default=str)
49
+ return f"Objective: {work_unit.objective}\nInputs: {inputs}"
@@ -0,0 +1,21 @@
1
+ """Process-backed AgentExecutor for the VYRELON lifecycle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from core.contracts.agent import AgentContract
6
+ from core.contracts.work_unit import WorkUnit
7
+ from runtime.process import ProcessResult, ProcessRuntime
8
+
9
+
10
+ class ProcessAgentExecutor:
11
+ """Execute an agent's work through the local process runtime."""
12
+
13
+ def __init__(self, command: list[str]) -> None:
14
+ self.command = list(command)
15
+
16
+ def execute(
17
+ self, *, agent: AgentContract, model_id: str, work_unit: WorkUnit
18
+ ) -> ProcessResult:
19
+ work_unit.metadata["executor_agent"] = agent.id
20
+ work_unit.metadata["model_id"] = model_id
21
+ return ProcessRuntime().run(self.command, cwd=work_unit.metadata.get("cwd"))
@@ -0,0 +1,57 @@
1
+ """Built-in VYRELON tool bindings."""
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from core.contracts.vyrelon_runtime import ToolRequest, ToolSideEffect, ToolSpec
6
+ from runtime.local.filesystem import FilesystemRuntime
7
+ from runtime.local.patch import PatchRuntime
8
+ from runtime.local.shell import PersistentShellRuntime
9
+ from runtime.tool_calling import ToolRuntime
10
+
11
+ @dataclass
12
+ class BuiltinToolBindings:
13
+ project_root: str
14
+ runtime: ToolRuntime
15
+ filesystem: FilesystemRuntime | None = None
16
+ patch: PatchRuntime | None = None
17
+ shell: PersistentShellRuntime | None = None
18
+
19
+ def __post_init__(self) -> None:
20
+ self.filesystem = self.filesystem or FilesystemRuntime(policy=self.runtime.policy)
21
+ self.patch = self.patch or PatchRuntime(policy=self.runtime.policy)
22
+ self.shell = self.shell or PersistentShellRuntime(self.project_root, policy=self.runtime.policy)
23
+ self._register()
24
+
25
+ def _register(self) -> None:
26
+ self.runtime.register(ToolSpec("filesystem.read","Read a text file inside the project root.",ToolSideEffect.READ,input_schema={"type":"object","required":["path"]}),self._read)
27
+ self.runtime.register(ToolSpec("filesystem.write","Write text to a file inside the project root.",ToolSideEffect.WRITE,frozenset({"filesystem.write"}),{"type":"object","required":["path","content"]}),self._write)
28
+ self.runtime.register(ToolSpec("patch.apply","Check and apply a unified Git patch to the project.",ToolSideEffect.WRITE,frozenset({"filesystem.write"}),{"type":"object","required":["patch"]}),self._patch)
29
+ self.runtime.register(ToolSpec("shell.run","Run a command in the project root.",ToolSideEffect.EXECUTE,frozenset({"process"}),{"type":"object","required":["command"]}),self._shell)
30
+
31
+ def _path(self, request: ToolRequest) -> str:
32
+ value=request.arguments.get("path")
33
+ if not isinstance(value,str) or not value: raise ValueError("path must be a non-empty string")
34
+ return str(Path(self.project_root)/value)
35
+
36
+ def _approved(self, request: ToolRequest) -> bool:
37
+ return request.metadata.get("approved") is True
38
+
39
+ def _read(self, request: ToolRequest) -> object:
40
+ return self.filesystem.read_text(self._path(request))
41
+
42
+ def _write(self, request: ToolRequest) -> object:
43
+ content=request.arguments.get("content")
44
+ if not isinstance(content,str): raise ValueError("content must be a string")
45
+ return str(self.filesystem.write_text(self._path(request),content,approved=self._approved(request)))
46
+
47
+ def _patch(self, request: ToolRequest) -> object:
48
+ patch=request.arguments.get("patch")
49
+ if not isinstance(patch,str) or not patch.strip(): raise ValueError("patch must be a non-empty string")
50
+ result=self.patch.apply(self.project_root,patch,approved=self._approved(request))
51
+ return {"returncode":result.returncode,"stdout":result.stdout,"stderr":result.stderr}
52
+
53
+ def _shell(self, request: ToolRequest) -> object:
54
+ command=request.arguments.get("command")
55
+ if not isinstance(command,str) or not command.strip(): raise ValueError("command must be a non-empty string")
56
+ result=self.shell.run(command)
57
+ return {"returncode":result.returncode,"stdout":result.stdout,"stderr":result.stderr}
runtime/capability.py ADDED
@@ -0,0 +1,143 @@
1
+ """Persistent model capability registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from core.contracts.ai import ModelSpec
9
+ from core.contracts.capability import (
10
+ CapabilityConfidence,
11
+ CapabilityMatch,
12
+ ModelCapabilityProfile,
13
+ )
14
+
15
+
16
+ class CapabilityStore:
17
+ """Project-scoped JSON persistence for normalized model capabilities."""
18
+
19
+ def __init__(self, root: Path) -> None:
20
+ self.root = Path(root)
21
+ self.root.mkdir(parents=True, exist_ok=True)
22
+
23
+ def path(self, model_id: str) -> Path:
24
+ safe = model_id.replace("/", "__").replace(":", "__")
25
+ return self.root / f"{safe}.json"
26
+
27
+ def exists(self, model_id: str) -> bool:
28
+ return self.path(model_id).exists()
29
+
30
+ def save(self, profile: ModelCapabilityProfile) -> None:
31
+ self.path(profile.model_id).write_text(
32
+ json.dumps(
33
+ {
34
+ "model_id": profile.model_id,
35
+ "provider_id": profile.provider_id,
36
+ "capabilities": sorted(profile.capabilities),
37
+ "confidence": profile.confidence.value,
38
+ "source": profile.source,
39
+ "metadata": profile.metadata,
40
+ },
41
+ indent=2,
42
+ default=str,
43
+ ),
44
+ encoding="utf-8",
45
+ )
46
+
47
+ def load(self, model_id: str) -> ModelCapabilityProfile:
48
+ data = json.loads(self.path(model_id).read_text(encoding="utf-8"))
49
+ return ModelCapabilityProfile(
50
+ model_id=str(data["model_id"]),
51
+ provider_id=str(data["provider_id"]),
52
+ capabilities=frozenset(str(item) for item in data.get("capabilities", [])),
53
+ confidence=CapabilityConfidence(str(data.get("confidence", "unknown"))),
54
+ source=str(data.get("source", "unknown")),
55
+ metadata=dict(data.get("metadata", {})),
56
+ )
57
+
58
+
59
+ class CapabilityRegistry:
60
+ """Resolve and persist model capability profiles."""
61
+
62
+ def __init__(self, store: CapabilityStore) -> None:
63
+ self.store = store
64
+
65
+ def profile(self, model: ModelSpec) -> ModelCapabilityProfile:
66
+ if self.store.exists(model.id):
67
+ stored = self.store.load(model.id)
68
+ if stored.provider_id == model.provider_id:
69
+ return stored
70
+ capabilities = frozenset(str(item) for item in model.capabilities)
71
+ profile = ModelCapabilityProfile(
72
+ model_id=model.id,
73
+ provider_id=model.provider_id,
74
+ capabilities=capabilities,
75
+ confidence=CapabilityConfidence.DECLARED,
76
+ source="model_spec",
77
+ )
78
+ self.store.save(profile)
79
+ return profile
80
+
81
+ def merge(self, profile: ModelCapabilityProfile) -> ModelCapabilityProfile:
82
+ """Merge a discovered or observed profile with the persisted profile."""
83
+ if self.store.exists(profile.model_id):
84
+ current = self.store.load(profile.model_id)
85
+ if current.provider_id != profile.provider_id:
86
+ raise ValueError(f"Capability provider mismatch for {profile.model_id}")
87
+ confidence = (
88
+ CapabilityConfidence.OBSERVED
89
+ if CapabilityConfidence.OBSERVED in {current.confidence, profile.confidence}
90
+ else CapabilityConfidence.DECLARED
91
+ if CapabilityConfidence.DECLARED in {current.confidence, profile.confidence}
92
+ else CapabilityConfidence.UNKNOWN
93
+ )
94
+ metadata = dict(current.metadata)
95
+ metadata.update(profile.metadata)
96
+ merged = ModelCapabilityProfile(
97
+ model_id=profile.model_id,
98
+ provider_id=profile.provider_id,
99
+ capabilities=frozenset(current.capabilities | profile.capabilities),
100
+ confidence=confidence,
101
+ source=f"{current.source}+{profile.source}",
102
+ metadata=metadata,
103
+ )
104
+ else:
105
+ merged = profile
106
+ self.store.save(merged)
107
+ return merged
108
+
109
+ def observe_response(self, model: ModelSpec, metadata: dict[str, object]) -> ModelCapabilityProfile:
110
+ """Record capabilities directly evidenced by a successful model response."""
111
+ observed = {"chat"}
112
+ if "tool_calls" in metadata or metadata.get("tools_used") is True:
113
+ observed.add("tools")
114
+ if metadata.get("structured_output") is True:
115
+ observed.add("structured_output")
116
+ return self.merge(ModelCapabilityProfile(
117
+ model_id=model.id,
118
+ provider_id=model.provider_id,
119
+ capabilities=frozenset(observed),
120
+ confidence=CapabilityConfidence.OBSERVED,
121
+ source="runtime_observation",
122
+ metadata={"observed_capabilities": sorted(observed)},
123
+ ))
124
+
125
+ def match(self, model: ModelSpec, requirements: frozenset[str]) -> CapabilityMatch:
126
+ profile = self.profile(model)
127
+ matched = profile.capabilities & requirements
128
+ missing = requirements - profile.capabilities
129
+ return CapabilityMatch(
130
+ model_id=model.id,
131
+ required=requirements,
132
+ matched=matched,
133
+ missing=missing,
134
+ score=profile.match_score(requirements),
135
+ confidence=profile.confidence,
136
+ )
137
+
138
+ def matches(
139
+ self,
140
+ models: list[ModelSpec],
141
+ requirements: frozenset[str],
142
+ ) -> dict[str, CapabilityMatch]:
143
+ return {model.id: self.match(model, requirements) for model in models}
@@ -0,0 +1,43 @@
1
+ """Resolve project Chat Agent contracts to provider adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from core.chat_agent_registry import ChatAgentRegistry
8
+ from core.contracts.chat_agent import ChatAgentProvider
9
+
10
+
11
+ class ChatAdapterRegistry:
12
+ def __init__(self) -> None:
13
+ self._factories: dict[ChatAgentProvider, Any] = {}
14
+
15
+ def register(self, provider: ChatAgentProvider, factory: Any) -> None:
16
+ self._factories[provider] = factory
17
+
18
+ def create(self, provider: ChatAgentProvider, *, model: str | None = None) -> Any:
19
+ try:
20
+ factory = self._factories[provider]
21
+ except KeyError as exc:
22
+ raise LookupError(f"no Chat Agent adapter registered for {provider.value}") from exc
23
+ return factory(model=model)
24
+
25
+
26
+ def default_chat_adapters() -> ChatAdapterRegistry:
27
+ registry = ChatAdapterRegistry()
28
+
29
+ def openai_factory(*, model=None):
30
+ from integrations.openai.chat_agent import OpenAIChatAgentAdapter
31
+ return OpenAIChatAgentAdapter(model=model)
32
+
33
+ registry.register(ChatAgentProvider.CHATGPT, openai_factory)
34
+ return registry
35
+
36
+
37
+ def resolve_project_chat_adapter(
38
+ project_agent,
39
+ model: str | None = None,
40
+ registry: ChatAdapterRegistry | None = None,
41
+ ):
42
+ adapters = registry or default_chat_adapters()
43
+ return adapters.create(project_agent.provider, model=model)
runtime/chat_config.py ADDED
@@ -0,0 +1,57 @@
1
+ """Project-scoped conversational Chat Agent configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ChatConfig:
12
+ version: int
13
+ agent_id: str
14
+ model: str | None = None
15
+
16
+ def validate(self) -> None:
17
+ if self.version != 1:
18
+ raise ValueError(f"unsupported chat config version: {self.version}")
19
+ if not self.agent_id.strip():
20
+ raise ValueError("chat agent id must not be empty")
21
+ if self.model is not None and not self.model.strip():
22
+ raise ValueError("chat model must not be empty when provided")
23
+
24
+ def to_dict(self) -> dict[str, object]:
25
+ self.validate()
26
+ return {"version": self.version, "agent_id": self.agent_id, "model": self.model}
27
+
28
+
29
+ DEFAULT_CHAT_CONFIG = ChatConfig(version=1, agent_id="chatgpt", model=None)
30
+
31
+
32
+ def load_chat_config(project_root: Path) -> ChatConfig:
33
+ path = project_root / ".multiagentos" / "chat.json"
34
+ if not path.is_file():
35
+ raise FileNotFoundError(f"VYRELON chat config not found: {path}")
36
+ data = json.loads(path.read_text(encoding="utf-8"))
37
+ if not isinstance(data, dict):
38
+ raise ValueError("chat config must be a JSON object")
39
+ config = ChatConfig(
40
+ version=int(data.get("version", 0)),
41
+ agent_id=str(data.get("agent_id", "")),
42
+ model=data.get("model"),
43
+ )
44
+ config.validate()
45
+ return config
46
+
47
+
48
+ def write_default_chat_config(project_root: Path) -> Path:
49
+ target = project_root / ".multiagentos"
50
+ target.mkdir(parents=True, exist_ok=True)
51
+ path = target / "chat.json"
52
+ if not path.exists():
53
+ path.write_text(
54
+ json.dumps(DEFAULT_CHAT_CONFIG.to_dict(), indent=2) + "\n",
55
+ encoding="utf-8",
56
+ )
57
+ return path
@@ -0,0 +1,52 @@
1
+ """End-to-end project Chat Agent execution facade."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from core.chat_agent_bridge import ChatAgentExecutionResult, ChatAgentRequest
8
+ from core.contracts.agent import AgentContract
9
+ from core.contracts.ai import ModelSpec
10
+ from core.contracts.execution import AgentExecutor, ResultReviewer, ResultVerifier
11
+ from runtime.vyrelon import VYRELONRuntime
12
+
13
+
14
+ def execute_project_chat_request(
15
+ project_root: Path,
16
+ request: ChatAgentRequest,
17
+ *,
18
+ agent: AgentContract,
19
+ models: list[ModelSpec],
20
+ executor: AgentExecutor,
21
+ verifier: ResultVerifier | None = None,
22
+ reviewer: ResultReviewer | None = None,
23
+ chat_agent_id: str | None = None,
24
+ preferred_model_ids: list[str] | None = None,
25
+ routing_strategy: str = "pool",
26
+ ) -> ChatAgentExecutionResult:
27
+ """Resolve the configured Chat Agent and execute through VYRELON.
28
+
29
+ The configured conversational provider supplies intent/plan; the supplied
30
+ VYRELON Agent/Model contracts own execution.
31
+ """
32
+ runtime = VYRELONRuntime()
33
+ configured_agent, configured_model = runtime.project_chat_agent(project_root)
34
+ adapter = runtime.project_chat_adapter(project_root)
35
+
36
+ selected_chat_agent_id = chat_agent_id or configured_agent.id
37
+ if configured_model and preferred_model_ids is None:
38
+ preferred_model_ids = [configured_model]
39
+
40
+ return runtime.execute_chat_request(
41
+ request=request,
42
+ adapter=adapter,
43
+ agent=agent,
44
+ models=models,
45
+ executor=executor,
46
+ chat_agent_id=selected_chat_agent_id,
47
+ preferred_model_ids=preferred_model_ids,
48
+ verifier=verifier,
49
+ reviewer=reviewer,
50
+ routing_strategy=routing_strategy,
51
+ project_root=project_root,
52
+ )
@@ -0,0 +1 @@
1
+ """VYRELON runtime configuration."""
@@ -0,0 +1,67 @@
1
+ """JSON configuration loader for AI providers and agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from core.contracts.agent import AgentContract
9
+ from core.contracts.ai import AIProvider, ModelSpec
10
+ from core.registry import AIRegistry, AgentRegistry
11
+ from core.routing import RoutingStrategy
12
+
13
+
14
+ class RuntimeConfig:
15
+ def __init__(
16
+ self,
17
+ agents: AgentRegistry,
18
+ ai: AIRegistry,
19
+ routing: dict[str, RoutingStrategy],
20
+ ) -> None:
21
+ self.agents = agents
22
+ self.ai = ai
23
+ self.routing = routing
24
+
25
+
26
+ class ConfigLoader:
27
+ def load(self, path: Path) -> RuntimeConfig:
28
+ data = json.loads(path.read_text(encoding="utf-8"))
29
+ ai_registry = AIRegistry()
30
+ for provider_data in data.get("ai", {}).get("providers", []):
31
+ models = tuple(
32
+ ModelSpec(
33
+ id=model["id"],
34
+ provider_id=provider_data["id"],
35
+ capabilities=frozenset(model.get("capabilities", [])),
36
+ metadata=dict(model.get("metadata", {})),
37
+ )
38
+ for model in provider_data.get("models", [])
39
+ )
40
+ ai_registry.register(
41
+ AIProvider(
42
+ id=provider_data["id"],
43
+ kind=provider_data.get("type", "generic"),
44
+ models=models,
45
+ metadata=dict(provider_data.get("metadata", {})),
46
+ )
47
+ )
48
+
49
+ agent_registry = AgentRegistry()
50
+ routing = {}
51
+ for agent_data in data.get("agents", []):
52
+ agent = AgentContract(
53
+ id=agent_data["id"],
54
+ role=agent_data.get("role", agent_data["id"]),
55
+ capabilities=frozenset(agent_data.get("capabilities", [])),
56
+ tools=frozenset(agent_data.get("tools", [])),
57
+ permissions=frozenset(agent_data.get("permissions", [])),
58
+ model_ids=tuple(agent_data.get("models", [])),
59
+ metadata=dict(agent_data.get("metadata", {})),
60
+ kind=agent_data.get("type", "custom"),
61
+ )
62
+ agent_registry.register(agent)
63
+ routing[agent.id] = RoutingStrategy(
64
+ agent_data.get("strategy", RoutingStrategy.POOL.value)
65
+ )
66
+
67
+ return RuntimeConfig(agent_registry, ai_registry, routing)