maof 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 (121) hide show
  1. maof/__init__.py +73 -0
  2. maof/agents/__init__.py +0 -0
  3. maof/agents/base.py +74 -0
  4. maof/agents/client.py +199 -0
  5. maof/agents/mcp_adapter.py +138 -0
  6. maof/agents/registry_runtime.py +92 -0
  7. maof/approval/__init__.py +0 -0
  8. maof/approval/service.py +265 -0
  9. maof/approval/tokens.py +28 -0
  10. maof/authz.py +118 -0
  11. maof/cli.py +515 -0
  12. maof/config.py +186 -0
  13. maof/context/__init__.py +0 -0
  14. maof/context/budget.py +86 -0
  15. maof/context/builder.py +81 -0
  16. maof/context/compaction.py +98 -0
  17. maof/context/jit.py +56 -0
  18. maof/context/notes.py +79 -0
  19. maof/context/redactor.py +69 -0
  20. maof/context/sources/__init__.py +0 -0
  21. maof/context/sources/builtins.py +87 -0
  22. maof/cost/__init__.py +0 -0
  23. maof/cost/accounting.py +106 -0
  24. maof/errors.py +71 -0
  25. maof/eval/__init__.py +0 -0
  26. maof/eval/judge.py +80 -0
  27. maof/eval/rubrics.py +49 -0
  28. maof/eval/runner.py +89 -0
  29. maof/identity.py +192 -0
  30. maof/memory/__init__.py +0 -0
  31. maof/memory/base.py +48 -0
  32. maof/memory/pgvector.py +71 -0
  33. maof/memory/service.py +53 -0
  34. maof/models/__init__.py +0 -0
  35. maof/models/anthropic.py +59 -0
  36. maof/models/azure_openai.py +53 -0
  37. maof/models/base.py +205 -0
  38. maof/models/bedrock.py +61 -0
  39. maof/models/cohere.py +53 -0
  40. maof/models/gateway.py +78 -0
  41. maof/models/mistral.py +53 -0
  42. maof/models/ollama.py +89 -0
  43. maof/models/openai.py +91 -0
  44. maof/models/vertex_gemini.py +60 -0
  45. maof/observability/__init__.py +0 -0
  46. maof/observability/events.py +51 -0
  47. maof/observability/otel.py +92 -0
  48. maof/observability/sinks/__init__.py +0 -0
  49. maof/observability/sinks/postgres_sink.py +39 -0
  50. maof/observability/sinks/stdout_sink.py +29 -0
  51. maof/observability/sinks/webhook_sink.py +172 -0
  52. maof/observability/trajectory.py +58 -0
  53. maof/orchestrator/__init__.py +0 -0
  54. maof/orchestrator/context_delegation.py +84 -0
  55. maof/orchestrator/coordinator.py +252 -0
  56. maof/orchestrator/delegation.py +44 -0
  57. maof/orchestrator/l1.py +172 -0
  58. maof/orchestrator/lifecycle.py +444 -0
  59. maof/orchestrator/loop.py +123 -0
  60. maof/orchestrator/messages.py +130 -0
  61. maof/orchestrator/pipeline.py +71 -0
  62. maof/orchestrator/stages/__init__.py +18 -0
  63. maof/orchestrator/stages/action_plan.py +81 -0
  64. maof/orchestrator/stages/approval.py +53 -0
  65. maof/orchestrator/stages/chat.py +27 -0
  66. maof/orchestrator/stages/intent.py +31 -0
  67. maof/orchestrator/stages/publish.py +79 -0
  68. maof/persistence/__init__.py +0 -0
  69. maof/persistence/base.py +116 -0
  70. maof/persistence/migrations/0001_init.sql +229 -0
  71. maof/persistence/postgres.py +433 -0
  72. maof/policy/__init__.py +0 -0
  73. maof/policy/actions.py +86 -0
  74. maof/policy/dsl.py +97 -0
  75. maof/policy/engine.py +228 -0
  76. maof/policy/rulesets.py +35 -0
  77. maof/py.typed +0 -0
  78. maof/registry/__init__.py +0 -0
  79. maof/registry/a2a.py +113 -0
  80. maof/registry/admin_api.py +71 -0
  81. maof/registry/loader.py +106 -0
  82. maof/registry/models.py +71 -0
  83. maof/registry/search.py +88 -0
  84. maof/registry/signing.py +73 -0
  85. maof/registry/store.py +121 -0
  86. maof/runs/__init__.py +0 -0
  87. maof/runs/artifacts.py +92 -0
  88. maof/runs/checkpoint.py +102 -0
  89. maof/runs/idempotency.py +95 -0
  90. maof/runs/ops.py +198 -0
  91. maof/runs/retention.py +61 -0
  92. maof/runs/store.py +179 -0
  93. maof/schemas/__init__.py +0 -0
  94. maof/schemas/registry.py +64 -0
  95. maof/tenancy.py +30 -0
  96. maof/transport/__init__.py +0 -0
  97. maof/transport/base.py +43 -0
  98. maof/transport/consumers.py +134 -0
  99. maof/transport/factory.py +50 -0
  100. maof/transport/fake.py +114 -0
  101. maof/transport/kafka.py +134 -0
  102. maof/transport/rabbitmq.py +208 -0
  103. maof/transport/redis.py +126 -0
  104. maof/transport/retry.py +50 -0
  105. maof/transport/signing.py +327 -0
  106. maof/transport/sqs.py +136 -0
  107. maof/transport/wire.py +34 -0
  108. maof/types.py +518 -0
  109. maof/workers/__init__.py +0 -0
  110. maof/workers/pool.py +172 -0
  111. maof/workflows/__init__.py +1 -0
  112. maof/workflows/definition.py +117 -0
  113. maof/workflows/executor.py +184 -0
  114. maof/workflows/promote.py +124 -0
  115. maof/workflows/store.py +244 -0
  116. maof-1.0.0.dist-info/METADATA +214 -0
  117. maof-1.0.0.dist-info/RECORD +121 -0
  118. maof-1.0.0.dist-info/WHEEL +4 -0
  119. maof-1.0.0.dist-info/entry_points.txt +2 -0
  120. maof-1.0.0.dist-info/licenses/LICENSE +201 -0
  121. maof-1.0.0.dist-info/licenses/NOTICE +8 -0
maof/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ """MAOF — Multi-Agent Orchestration Framework.
2
+
3
+ The reusable orchestration + governance layer of a hierarchical (L1 -> L2)
4
+ multi-agent system. Ships zero domain content: adopters inject their own L1
5
+ orchestrator, L2 agents, skills, task schemas, and policy rulesets.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from maof.config import Settings
11
+ from maof.errors import (
12
+ ApprovalRequired,
13
+ BudgetExceeded,
14
+ ConfigError,
15
+ IdempotencyError,
16
+ MAOFError,
17
+ PolicyDenied,
18
+ RegistryTrustError,
19
+ SchemaValidationError,
20
+ SignatureError,
21
+ TenancyError,
22
+ TransportError,
23
+ )
24
+ from maof.types import (
25
+ ContextEnvelope,
26
+ CoordinationMode,
27
+ DataPointer,
28
+ EffortBudget,
29
+ Envelope,
30
+ Intent,
31
+ OrchestrationMode,
32
+ Plan,
33
+ Stage,
34
+ StageContext,
35
+ Task,
36
+ TaskResult,
37
+ TenantContext,
38
+ ToolRef,
39
+ )
40
+
41
+ __version__ = "1.0.0"
42
+
43
+ __all__ = [
44
+ "__version__",
45
+ "Settings",
46
+ # errors
47
+ "MAOFError",
48
+ "ConfigError",
49
+ "SignatureError",
50
+ "SchemaValidationError",
51
+ "PolicyDenied",
52
+ "ApprovalRequired",
53
+ "IdempotencyError",
54
+ "TransportError",
55
+ "RegistryTrustError",
56
+ "BudgetExceeded",
57
+ "TenancyError",
58
+ # core types
59
+ "Stage",
60
+ "CoordinationMode",
61
+ "OrchestrationMode",
62
+ "TenantContext",
63
+ "Envelope",
64
+ "ContextEnvelope",
65
+ "Task",
66
+ "TaskResult",
67
+ "Intent",
68
+ "Plan",
69
+ "ToolRef",
70
+ "DataPointer",
71
+ "EffortBudget",
72
+ "StageContext",
73
+ ]
File without changes
maof/agents/base.py ADDED
@@ -0,0 +1,74 @@
1
+ """Agent contracts.
2
+
3
+ First-party agents implement these Protocols and register via decorators /
4
+ entry points; the MCP adapter wraps remote MCP servers to satisfy the
5
+ same L2Agent contract. ``BaseL2Agent`` defaults
6
+ ``context_delegation = []`` so a simple agent that side-loads nothing need not
7
+ declare anything.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
13
+
14
+ if TYPE_CHECKING:
15
+ from maof.registry.models import ContextDeclaration
16
+ from maof.types import L2Context, OrchestrationResult, TaskResult, TenantContext
17
+
18
+
19
+ @runtime_checkable
20
+ class Skill(Protocol):
21
+ name: str
22
+ version: str
23
+
24
+ async def run(self, payload: dict[str, Any], ctx: L2Context) -> dict[str, Any]: ...
25
+
26
+
27
+ @runtime_checkable
28
+ class L2Agent(Protocol):
29
+ name: str
30
+ accepted_task_types: list[str]
31
+ skills: list[Skill]
32
+ context_delegation: list[ContextDeclaration] # declares any side-loaded context
33
+
34
+ async def handle(self, task: dict[str, Any], ctx: L2Context) -> TaskResult: ...
35
+
36
+
37
+ @runtime_checkable
38
+ class L1Orchestrator(Protocol):
39
+ async def run(self, goal: str, tenant: TenantContext) -> OrchestrationResult: ...
40
+
41
+
42
+ class BaseSkill:
43
+ """Convenience base for a skill (an L2 agent's internal chain step)."""
44
+
45
+ name: str = "skill"
46
+ version: str = "v1"
47
+
48
+ async def run(self, payload: dict[str, Any], ctx: L2Context) -> dict[str, Any]:
49
+ raise NotImplementedError
50
+
51
+
52
+ class BaseL2Agent:
53
+ """Convenience base for an L2 agent. Defaults ``context_delegation = []`` so a
54
+ simple agent that side-loads nothing need not declare anything."""
55
+
56
+ name: str = "base_l2_agent"
57
+ accepted_task_types: list[str] = []
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ skills: list[Skill] | None = None,
63
+ context_delegation: list[ContextDeclaration] | None = None,
64
+ ) -> None:
65
+ self.skills: list[Skill] = list(skills) if skills else []
66
+ self.context_delegation: list[ContextDeclaration] = (
67
+ list(context_delegation) if context_delegation else []
68
+ )
69
+
70
+ async def handle(self, task: dict[str, Any], ctx: L2Context) -> TaskResult:
71
+ raise NotImplementedError
72
+
73
+
74
+ __all__ = ["Skill", "L2Agent", "L1Orchestrator", "BaseSkill", "BaseL2Agent"]
maof/agents/client.py ADDED
@@ -0,0 +1,199 @@
1
+ """Hosting machinery for source-of-truth agents.
2
+
3
+ - :class:`AgentClientFactory` — registry-resolved, RBAC-scoped MCP clients for
4
+ agent→agent consultation (``ctx.agents``); every consultation is audited.
5
+ - :func:`attach_registry_context_sources` — approved ``context_source`` entries
6
+ auto-attach to the ContextBuilder (``required`` fails closed; contributions
7
+ cache per ``ContextDeclaration.mutable``).
8
+ - :func:`attach_registry_resolvers` — manifest ``resolver_schemes`` register as
9
+ JIT reference loaders (adopter-defined schemes, e.g. ``catalog://``, ``datastore://``).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Callable
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from maof.errors import MAOFError, RegistryTrustError
18
+ from maof.observability.events import AuditEvent
19
+ from maof.registry.loader import _scopes_granted
20
+
21
+ if TYPE_CHECKING:
22
+ from maof.context.builder import ContextBuilder
23
+ from maof.context.jit import DefaultReferenceResolver
24
+ from maof.observability.events import EventSink
25
+ from maof.registry.loader import RegistryLoader
26
+ from maof.registry.models import AgentManifest
27
+ from maof.types import ContextEnvelope, StageContext, TenantContext
28
+
29
+ #: Builds a connected client (MCP stdio/HTTP) for an approved manifest. Injected so
30
+ #: deployments choose their MCP client library; tests inject fakes.
31
+ ClientBuilder = Callable[["AgentManifest"], Any]
32
+
33
+
34
+ class AgentClientFactory:
35
+ """Resolve an approved+signed registry agent into an RBAC-scoped client."""
36
+
37
+ def __init__(
38
+ self,
39
+ loader: RegistryLoader,
40
+ *,
41
+ tenant: TenantContext,
42
+ client_builder: ClientBuilder,
43
+ event_sink: EventSink | None = None,
44
+ ) -> None:
45
+ self._loader = loader
46
+ self._tenant = tenant
47
+ self._client_builder = client_builder
48
+ self._event_sink = event_sink
49
+ self._cache: dict[str, Any] = {}
50
+
51
+ async def client(self, agent_id: str) -> Any:
52
+ if agent_id in self._cache:
53
+ await self._emit(agent_id)
54
+ return self._cache[agent_id]
55
+ manifest = next((m for m in await self._loader.manifests() if m.id == agent_id), None)
56
+ if manifest is None:
57
+ raise RegistryTrustError(f"no approved registry agent {agent_id!r}")
58
+ if not _scopes_granted(manifest, self._tenant):
59
+ raise RegistryTrustError(
60
+ f"tenant {self._tenant.tenant_id!r} lacks scopes for {agent_id!r}"
61
+ )
62
+ client = self._client_builder(manifest)
63
+ self._cache[agent_id] = client
64
+ await self._emit(agent_id)
65
+ return client
66
+
67
+ async def _emit(self, agent_id: str) -> None:
68
+ if self._event_sink is None:
69
+ return
70
+ await self._event_sink.emit(
71
+ AuditEvent(
72
+ tenant_id=self._tenant.tenant_id,
73
+ intent_id=None,
74
+ event_type="agent_consulted",
75
+ envelope={"agent": agent_id},
76
+ details={},
77
+ )
78
+ )
79
+
80
+
81
+ class ContextSourceCache:
82
+ """Caches context-source contributions keyed (entry_id, tenant, version)."""
83
+
84
+ def __init__(self) -> None:
85
+ self._cache: dict[tuple[str, str, str], Any] = {}
86
+
87
+ def get(self, manifest: AgentManifest, tenant_id: str) -> Any | None:
88
+ return self._cache.get((manifest.id, tenant_id, manifest.version))
89
+
90
+ def set(self, manifest: AgentManifest, tenant_id: str, value: Any) -> None:
91
+ self._cache[(manifest.id, tenant_id, manifest.version)] = value
92
+
93
+
94
+ class RegistryContextSource:
95
+ """Wraps an approved ``context_source`` entry as a ContextBuilder source."""
96
+
97
+ def __init__(
98
+ self,
99
+ manifest: AgentManifest,
100
+ client: Any,
101
+ *,
102
+ cache: ContextSourceCache | None = None,
103
+ event_sink: EventSink | None = None,
104
+ ) -> None:
105
+ # The stable registry id is the lookup key — display names are not contracts.
106
+ self.name = manifest.id
107
+ self._manifest = manifest
108
+ self._client = client
109
+ self._cache = cache
110
+ self._event_sink = event_sink
111
+ # mutable declarations force re-fetch every build (ContextDeclaration.mutable)
112
+ self._cacheable = not any(d.mutable for d in manifest.side_loaded_context)
113
+
114
+ async def contribute(self, sc: StageContext, env: ContextEnvelope) -> ContextEnvelope:
115
+ data: Any | None = None
116
+ if self._cache is not None and self._cacheable:
117
+ data = self._cache.get(self._manifest, sc.tenant.tenant_id)
118
+ if data is None:
119
+ try:
120
+ data = await self._client.read_resource(self._manifest.id)
121
+ except Exception as exc:
122
+ if self._manifest.required:
123
+ raise MAOFError(
124
+ f"required context source {self._manifest.id!r} unavailable "
125
+ f"(fail closed): {exc}"
126
+ ) from exc
127
+ return env # optional source: skip, keep planning
128
+ if self._cache is not None and self._cacheable:
129
+ self._cache.set(self._manifest, sc.tenant.tenant_id, data)
130
+ env.semantic_model[self.name] = data
131
+ env.extras.setdefault("registry_context_sources", []).append(self._manifest.id)
132
+ if self._event_sink is not None:
133
+ await self._event_sink.emit(
134
+ AuditEvent(
135
+ tenant_id=sc.tenant.tenant_id,
136
+ intent_id=env.intent_id,
137
+ event_type="context_delegated",
138
+ envelope={"agent": self._manifest.id, "kind": "registry_context_source"},
139
+ details={"version": self._manifest.version},
140
+ )
141
+ )
142
+ return env
143
+
144
+
145
+ async def attach_registry_context_sources(
146
+ builder: ContextBuilder,
147
+ loader: RegistryLoader,
148
+ *,
149
+ tenant: TenantContext,
150
+ client_builder: ClientBuilder,
151
+ cache: ContextSourceCache | None = None,
152
+ event_sink: EventSink | None = None,
153
+ ) -> list[AgentManifest]:
154
+ """Attach every approved + RBAC-granted ``context_source`` to the builder."""
155
+ attached: list[AgentManifest] = []
156
+ for manifest in await loader.context_sources(tenant=tenant):
157
+ builder.add_source(
158
+ RegistryContextSource(
159
+ manifest, client_builder(manifest), cache=cache, event_sink=event_sink
160
+ )
161
+ )
162
+ attached.append(manifest)
163
+ return attached
164
+
165
+
166
+ async def attach_registry_resolvers(
167
+ resolver: DefaultReferenceResolver,
168
+ loader: RegistryLoader,
169
+ *,
170
+ client_builder: ClientBuilder,
171
+ tenant: TenantContext | None = None,
172
+ ) -> list[str]:
173
+ """Register every approved manifest's ``resolver_schemes`` as JIT loaders, so
174
+ ``<scheme>://<ref>`` references resolve through the owning agent."""
175
+ registered: list[str] = []
176
+ manifests = await loader.manifests()
177
+ for manifest in manifests:
178
+ if tenant is not None and not _scopes_granted(manifest, tenant):
179
+ continue
180
+ for scheme in manifest.resolver_schemes:
181
+ client = client_builder(manifest)
182
+
183
+ async def _load(rest: str, _client: Any = client) -> str:
184
+ data = await _client.read_resource(rest)
185
+ return data if isinstance(data, str) else repr(data)
186
+
187
+ resolver.register_loader(scheme, _load)
188
+ registered.append(scheme)
189
+ return registered
190
+
191
+
192
+ __all__ = [
193
+ "ClientBuilder",
194
+ "AgentClientFactory",
195
+ "ContextSourceCache",
196
+ "RegistryContextSource",
197
+ "attach_registry_context_sources",
198
+ "attach_registry_resolvers",
199
+ ]
@@ -0,0 +1,138 @@
1
+ """MCP adapter.
2
+
3
+ Wraps a remote MCP server so it satisfies the L2Agent contract (and, separately,
4
+ the ContextSource contract). The MCP client is injected so this module imports
5
+ without the ``mcp`` extra; adopters pass a connected client (stdio or HTTP) —
6
+ either anything satisfying the duck-typed surface (``call_tool``/``read_resource``)
7
+ or :class:`MCPSDKClient`, which adapts an official ``mcp`` SDK session to it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import contextlib
13
+ import json
14
+ from collections.abc import AsyncIterator
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from maof.errors import ConfigError, MAOFError
18
+ from maof.types import TaskResult
19
+
20
+ if TYPE_CHECKING:
21
+ from maof.registry.models import AgentManifest, ContextDeclaration
22
+ from maof.types import ContextEnvelope, L2Context, StageContext
23
+
24
+
25
+ class MCPAgentAdapter:
26
+ """Presents an MCP server as an L2 agent: each task maps to an MCP tool call."""
27
+
28
+ def __init__(self, manifest: AgentManifest, client: Any) -> None:
29
+ self.name = manifest.name
30
+ self.accepted_task_types = list(manifest.accepted_task_types)
31
+ self.skills: list[Any] = []
32
+ self.context_delegation: list[ContextDeclaration] = list(manifest.side_loaded_context)
33
+ self._client = client
34
+ self._manifest = manifest
35
+
36
+ async def handle(self, task: dict[str, Any], ctx: L2Context) -> TaskResult:
37
+ tool = task.get("tool") or task.get("task_type", "")
38
+ result = await self._client.call_tool(tool, task)
39
+ return TaskResult(
40
+ status="ok",
41
+ task_id=str(task.get("task_id", "")),
42
+ output={"mcp_tool": tool, "result": result},
43
+ )
44
+
45
+
46
+ class MCPContextSource:
47
+ """Uses an MCP server purely as a context provider (a read-only slice)."""
48
+
49
+ def __init__(
50
+ self, manifest: AgentManifest, client: Any, *, resource: str | None = None
51
+ ) -> None:
52
+ self.name = manifest.name
53
+ self._client = client
54
+ self._resource = resource or manifest.id
55
+
56
+ async def contribute(self, sc: StageContext, env: ContextEnvelope) -> ContextEnvelope:
57
+ data = await self._client.read_resource(self._resource)
58
+ env.semantic_model[self.name] = data
59
+ return env
60
+
61
+
62
+ def _parse_payload(text: str) -> Any:
63
+ """Tool/resource payloads are JSON when the server sends JSON, else raw text."""
64
+ try:
65
+ return json.loads(text)
66
+ except (json.JSONDecodeError, ValueError):
67
+ return text
68
+
69
+
70
+ class MCPSDKClient:
71
+ """Adapts an official ``mcp`` SDK ``ClientSession`` to the duck-typed surface
72
+ the adapters above expect (``call_tool(name, args)`` / ``read_resource(ref)``).
73
+
74
+ Importing this module never requires the ``mcp`` extra; connecting does (see
75
+ :func:`mcp_stdio_client`). Fake clients keep satisfying the same surface.
76
+ """
77
+
78
+ def __init__(self, session: Any, *, default_resource_scheme: str = "resource") -> None:
79
+ self._session = session
80
+ self._scheme = default_resource_scheme
81
+
82
+ async def call_tool(self, name: str, args: dict[str, Any]) -> Any:
83
+ result = await self._session.call_tool(name, arguments=args)
84
+ structured = getattr(result, "structuredContent", None)
85
+ if structured:
86
+ return structured
87
+ parts: list[str] = []
88
+ for block in getattr(result, "content", None) or []:
89
+ text = getattr(block, "text", None)
90
+ if text is not None:
91
+ parts.append(str(text))
92
+ text_out = "\n".join(parts)
93
+ if getattr(result, "isError", False):
94
+ raise MAOFError(f"MCP tool {name!r} returned an error: {text_out or 'no detail'}")
95
+ return _parse_payload(text_out)
96
+
97
+ async def read_resource(self, ref: str) -> Any:
98
+ uri = ref if "://" in ref else f"{self._scheme}://{ref}"
99
+ result = await self._session.read_resource(uri)
100
+ for content in getattr(result, "contents", None) or []:
101
+ text = getattr(content, "text", None)
102
+ if text is not None:
103
+ return _parse_payload(str(text))
104
+ blob = getattr(content, "blob", None)
105
+ if blob is not None:
106
+ return blob
107
+ return None
108
+
109
+
110
+ @contextlib.asynccontextmanager
111
+ async def mcp_stdio_client(
112
+ command: str,
113
+ args: list[str] | None = None,
114
+ *,
115
+ env: dict[str, str] | None = None,
116
+ default_resource_scheme: str = "resource",
117
+ ) -> AsyncIterator[MCPSDKClient]:
118
+ """Spawn an MCP server over stdio and yield a connected :class:`MCPSDKClient`.
119
+
120
+ The official SDK is imported lazily so the module works without the extra.
121
+ """
122
+ try:
123
+ from mcp import ClientSession, StdioServerParameters
124
+ from mcp.client.stdio import stdio_client
125
+ except ImportError as exc: # pragma: no cover - depends on installed extras
126
+ raise ConfigError(
127
+ "mcp_stdio_client requires the 'mcp' extra (pip install maof[mcp])"
128
+ ) from exc
129
+ params = StdioServerParameters(command=command, args=list(args or []), env=env)
130
+ async with (
131
+ stdio_client(params) as (read_stream, write_stream),
132
+ ClientSession(read_stream, write_stream) as session,
133
+ ):
134
+ await session.initialize()
135
+ yield MCPSDKClient(session, default_resource_scheme=default_resource_scheme)
136
+
137
+
138
+ __all__ = ["MCPAgentAdapter", "MCPContextSource", "MCPSDKClient", "mcp_stdio_client"]
@@ -0,0 +1,92 @@
1
+ """Runtime agent/skill registry.
2
+
3
+ In-process map of locally injected L2 agents and skills (compile/deploy-time
4
+ trust), populated via decorators and/or ``importlib.metadata`` entry points
5
+ (groups ``maof.l2_agents`` / ``maof.skills``). Distinct from the persisted,
6
+ admin-gated discovery registry in ``maof.registry``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ from importlib import metadata
13
+ from typing import TYPE_CHECKING, TypeVar
14
+
15
+ if TYPE_CHECKING:
16
+ from maof.agents.base import L2Agent, Skill
17
+
18
+ A = TypeVar("A", bound="L2Agent")
19
+ S = TypeVar("S", bound="Skill")
20
+
21
+
22
+ class AgentRegistry:
23
+ def __init__(self) -> None:
24
+ self._agents: dict[str, L2Agent] = {}
25
+ self._by_task_type: dict[str, L2Agent] = {}
26
+ self._skills: dict[str, Skill] = {}
27
+
28
+ def register_agent(self, agent: L2Agent) -> None:
29
+ self._agents[agent.name] = agent
30
+ for task_type in agent.accepted_task_types:
31
+ self._by_task_type[task_type] = agent
32
+
33
+ def register_skill(self, skill: Skill) -> None:
34
+ self._skills[skill.name] = skill
35
+
36
+ def agent(self, name: str) -> L2Agent | None:
37
+ return self._agents.get(name)
38
+
39
+ def agent_for_task_type(self, task_type: str) -> L2Agent | None:
40
+ return self._by_task_type.get(task_type)
41
+
42
+ def skill(self, name: str) -> Skill | None:
43
+ return self._skills.get(name)
44
+
45
+ def agents(self) -> list[L2Agent]:
46
+ return list(self._agents.values())
47
+
48
+ def load_entry_points(self) -> None:
49
+ """Discover adopter agents/skills declared via packaging metadata."""
50
+ for ep in metadata.entry_points(group="maof.l2_agents"):
51
+ factory = ep.load()
52
+ self.register_agent(factory() if callable(factory) else factory)
53
+ for ep in metadata.entry_points(group="maof.skills"):
54
+ factory = ep.load()
55
+ self.register_skill(factory() if callable(factory) else factory)
56
+
57
+
58
+ #: A process-global default registry that the decorators target.
59
+ default_registry = AgentRegistry()
60
+
61
+
62
+ def register_l2_agent(
63
+ registry: AgentRegistry | None = None,
64
+ ) -> Callable[[type[A]], type[A]]:
65
+ """Class decorator: instantiate and register an L2 agent on import."""
66
+ target = registry or default_registry
67
+
68
+ def decorator(cls: type[A]) -> type[A]:
69
+ target.register_agent(cls())
70
+ return cls
71
+
72
+ return decorator
73
+
74
+
75
+ def register_skill(
76
+ registry: AgentRegistry | None = None,
77
+ ) -> Callable[[type[S]], type[S]]:
78
+ target = registry or default_registry
79
+
80
+ def decorator(cls: type[S]) -> type[S]:
81
+ target.register_skill(cls())
82
+ return cls
83
+
84
+ return decorator
85
+
86
+
87
+ __all__ = [
88
+ "AgentRegistry",
89
+ "default_registry",
90
+ "register_l2_agent",
91
+ "register_skill",
92
+ ]
File without changes