commonadk 0.0.1__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.
commonadk/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """commonadk -- define an agent system once, run it on any agent SDK.
2
+
3
+ See plan.md at the repo root for the full hypothesis and architecture. This
4
+ package (M1) is the framework-neutral core: models, loader, validation, and
5
+ the mermaid renderer. It has no dependency on any agent SDK.
6
+ """
7
+
8
+ from .adapters import BaseAdapter, get_adapter
9
+ from .loader import load
10
+ from .mermaid import render_mermaid, write_interaction_layer
11
+ from .mixed import MixedSystem, RuntimeUnit, build_mixed
12
+ from .models import (
13
+ AgentConfig,
14
+ AgentSpec,
15
+ EnvRequirement,
16
+ InteractionEdge,
17
+ InteractionGraph,
18
+ Project,
19
+ ProjectConfig,
20
+ Requires,
21
+ ToolParameter,
22
+ ToolSpec,
23
+ )
24
+ from .validation import ValidationError
25
+
26
+ __all__ = [
27
+ "BaseAdapter",
28
+ "get_adapter",
29
+ "load",
30
+ "render_mermaid",
31
+ "write_interaction_layer",
32
+ "MixedSystem",
33
+ "RuntimeUnit",
34
+ "build_mixed",
35
+ "AgentConfig",
36
+ "AgentSpec",
37
+ "EnvRequirement",
38
+ "InteractionEdge",
39
+ "InteractionGraph",
40
+ "Project",
41
+ "ProjectConfig",
42
+ "Requires",
43
+ "ToolParameter",
44
+ "ToolSpec",
45
+ "ValidationError",
46
+ ]
@@ -0,0 +1,65 @@
1
+ """Adapter registry -- turns a framework-neutral `Project` into a live SDK agent.
2
+
3
+ Each target's actual SDK is imported lazily, only when that target is
4
+ requested via `get_adapter`. Importing `commonadk` (or calling
5
+ `commonadk.load()`) must keep working with no agent SDK installed at all --
6
+ this module and `base.py` have zero SDK imports at module scope to guarantee
7
+ that.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from importlib import import_module
13
+
14
+ from .base import BaseAdapter
15
+
16
+ # target -> (module to import, class name to instantiate, pip extra to suggest)
17
+ _REGISTRY: dict[str, tuple[str, str, str]] = {
18
+ "google-adk": ("commonadk.adapters.google_adk", "GoogleADKAdapter", "google"),
19
+ "openai": ("commonadk.adapters.openai_agents", "OpenAIAgentsAdapter", "openai"),
20
+ "claude": ("commonadk.adapters.claude_agent", "ClaudeAgentSDKAdapter", "claude"),
21
+ "crewai": ("commonadk.adapters.crewai_adapter", "CrewAIAdapter", "crewai"),
22
+ "autogen": ("commonadk.adapters.autogen_adapter", "AutoGenAdapter", "autogen"),
23
+ "langgraph": ("commonadk.adapters.langgraph_adapter", "LangGraphAdapter", "langgraph"),
24
+ }
25
+
26
+
27
+ def known_targets() -> list[str]:
28
+ """Sorted list of registered target names -- no SDK import required.
29
+
30
+ Used by `validation.py`'s `runtime:` check to validate a name against
31
+ the registry without importing any adapter module (and therefore
32
+ without importing any agent SDK) -- see mixed-target-design.md, "What
33
+ `runtime:` means now".
34
+ """
35
+ return sorted(_REGISTRY)
36
+
37
+
38
+ def get_adapter(target: str) -> BaseAdapter:
39
+ """Look up and instantiate the adapter for `target`.
40
+
41
+ Raises `ValueError` naming the known targets if `target` is unrecognized,
42
+ or `ImportError` with a `pip install "commonadk[<extra>]"` hint if the
43
+ target is recognized but its underlying SDK is not installed.
44
+ """
45
+ if target not in _REGISTRY:
46
+ raise ValueError(
47
+ f"Unknown build target {target!r}. Known targets: "
48
+ f"{sorted(_REGISTRY)}"
49
+ )
50
+
51
+ module_path, class_name, extra = _REGISTRY[target]
52
+ try:
53
+ module = import_module(module_path)
54
+ except ImportError as e:
55
+ raise ImportError(
56
+ f"target {target!r} requires its SDK to be installed. "
57
+ f'Install it with: pip install "commonadk[{extra}]" '
58
+ f"(underlying import error: {e})"
59
+ ) from e
60
+
61
+ adapter_cls = getattr(module, class_name)
62
+ return adapter_cls()
63
+
64
+
65
+ __all__ = ["BaseAdapter", "get_adapter", "known_targets"]
@@ -0,0 +1,398 @@
1
+ """AutoGen adapter: `AgentSpec` -> live `autogen_agentchat` objects.
2
+
3
+ This targets Microsoft's current AutoGen stack (`autogen-agentchat` /
4
+ `autogen-core` / `autogen-ext`, 0.4+), NOT the community `ag2` fork -- the
5
+ two forked from the same original project and now have unrelated APIs.
6
+ Verified against the installed packages: autogen-agentchat 0.7.5,
7
+ autogen-core 0.7.5, autogen-ext 0.7.5 (`autogen_agentchat.agents`,
8
+ `autogen_agentchat.teams`, `autogen_agentchat.base`, and
9
+ `autogen_ext.models.{openai,anthropic}`, introspected via `inspect.signature`
10
+ / `inspect.getsource` and exercised directly during M7 -- not taken from
11
+ memory of the API, which is unreliable here given the fork).
12
+
13
+ WHAT `build()` RETURNS -- read this first: like the Google ADK and OpenAI
14
+ Agents adapters, this SDK has real persistent agent objects
15
+ (`autogen_agentchat.agents.AssistantAgent`), each wired with a
16
+ `model_client`, its own `tools.py` functions, and a `handoffs` list of
17
+ target agent *names* (see "Edge mapping" below). But an `AssistantAgent`
18
+ with handoffs configured needs a *team* to actually route those handoffs at
19
+ run time -- AutoGen's own mechanism for this is `autogen_agentchat.teams.
20
+ Swarm`, "a group chat team that selects the next speaker based on handoff
21
+ message[s]" (its own docstring). Investigated, not assumed, whether a lone
22
+ `AssistantAgent` with handoffs is runnable on its own: it is NOT -- handoffs
23
+ only take effect inside a `Swarm` (or another team); a bare `AssistantAgent.
24
+ run()` just answers once and never consults `.handoffs` at all.
25
+
26
+ So this adapter builds every reachable agent once (see "Edge mapping"), then
27
+ picks the return shape based on whether the build root actually has
28
+ somewhere to hand off to:
29
+
30
+ - The build root has NO outgoing edges (a leaf, e.g. `writer` in the shipped
31
+ example): there is nothing to route to, so this adapter returns the bare
32
+ `AssistantAgent` -- the simplest, most directly runnable object for that
33
+ case. Usage: `result = await agent.run(task="...")`.
34
+ - The build root HAS at least one outgoing edge: this adapter returns a
35
+ ready-to-run `autogen_agentchat.teams.Swarm` whose `participants` are
36
+ every reachable agent, build root first (`BaseAdapter._reachable_agents`
37
+ already returns `agent_name` at index 0, which is also exactly the
38
+ property `Swarm` requires: verified via `Swarm.__init__` -- the first
39
+ participant becomes the initial speaker). Usage:
40
+ `result = await team.run(task="...")`.
41
+
42
+ Usage (mirroring cli.py's `_run_autogen`):
43
+
44
+ import asyncio
45
+ from commonadk import load
46
+
47
+ project = load("common/")
48
+ built = project.build("coordinator", target="autogen") # a Swarm here
49
+
50
+ async def main():
51
+ result = await built.run(task="Research EV adoption")
52
+ print(result.messages[-1].content)
53
+
54
+ asyncio.run(main())
55
+
56
+ `max_turns` on the returned `Swarm` -- a deliberate, documented default, not
57
+ an SDK requirement: investigated directly against `Swarm`'s own docs and
58
+ `BaseGroupChat.__init__` -- with neither a `termination_condition` nor a
59
+ `max_turns` set, a group chat "will run indefinitely": if the current
60
+ speaker doesn't send a handoff message, `SwarmGroupChatManager` just lets
61
+ the same speaker go again, and there is no other built-in stop condition
62
+ (unlike a bare `AssistantAgent.run()`, which always returns after exactly
63
+ one turn). Since `commonadk run` needs a single execution that reliably
64
+ terminates (matching every other adapter's `_run_*` in cli.py), and
65
+ `max_turns`/`termination_condition` are `Swarm` CONSTRUCTOR-only fields with
66
+ no equivalent per-call override on `run`/`run_stream` (verified via
67
+ `inspect.signature`), this adapter sets `max_turns=len(reachable)` on every
68
+ `Swarm` it returns: exactly enough speaker-turns for one full pass down a
69
+ linear delegate/handoff chain (root speaks and hands off, ..., the final
70
+ agent speaks its answer and the budget is exhausted right after). This is a
71
+ heuristic, not a guarantee, for branchier graphs (multi-parent, cycles) --
72
+ documented here as a known v1 limitation, not silently assumed correct. A
73
+ caller who wants a different turn budget or an interactive, longer-running
74
+ conversation should not rely on the `Swarm` this adapter returns for that;
75
+ building one directly from `AssistantAgent`s (this adapter's own approach,
76
+ above) with an explicit `termination_condition` is the escape hatch.
77
+
78
+ Edge mapping (v1 intersection decision, plan.md "Edge semantics v1", same
79
+ call as openai_agents.py): both `delegate` and `handoff` edges map to
80
+ AutoGen's one handoff mechanism -- `AssistantAgent(handoffs=[...])`. commonadk
81
+ does not yet distinguish them for this target either.
82
+
83
+ KEY PROPERTY, investigated not assumed -- handoff targets are plain NAME
84
+ STRINGS, not object references: `AssistantAgent.__init__` accepts
85
+ `handoffs: List[HandoffBase | str] | None`, and a bare `str` is wrapped as
86
+ `HandoffBase(target=that_string)` (verified via `inspect.getsource`) --
87
+ `Swarm` resolves those names against its own `participants` list by name at
88
+ run time, there is no parent-tracking or "already referenced" guard
89
+ anywhere in construction. This makes multi-parent graphs and cycles even
90
+ more trivially fine here than in openai_agents.py (which at least memoizes
91
+ live object references): this adapter builds one `AssistantAgent` per
92
+ logical agent name (memoized in a `dict[str, AssistantAgent]`, matching
93
+ `_reachable_agents`'s own dedup) and each agent's `handoffs` list is just
94
+ `[edge.to for edge in ... if edge.from_ == name]` -- plain strings, so a name
95
+ reachable by two paths or a path that cycles back to the build root needs no
96
+ special handling at all: it is simply the same dict entry, and a cycle back
97
+ to `agent_name` is just another string in some other agent's `handoffs`
98
+ list, not a construction hazard (`agent_name` itself is never excluded from
99
+ `memo`, unlike the Claude/CrewAI adapters' flat registries, since here
100
+ "being referenced by name" carries no risk of infinite recursion or
101
+ double-registration).
102
+
103
+ Tool wiring: `AssistantAgent(tools=[...])` accepts PLAIN CALLABLES directly
104
+ (verified via `inspect.getsource` of `AssistantAgent.__init__`) -- it wraps
105
+ each with `autogen_core.tools.FunctionTool(tool, description=tool.__doc__)`
106
+ itself, introspecting the function's signature and docstring exactly like
107
+ every `tools.py` function already provides (enforced upstream by
108
+ validation.py). So this adapter passes `[t.func for t in spec.tools]`
109
+ straight through with no wrapping of its own -- simpler than every other
110
+ adapter in this codebase.
111
+
112
+ Model routing -- investigated against the installed `autogen_ext.models`
113
+ package tree (`anthropic`, `azure`, `ollama`, `openai`, ... submodules;
114
+ `importlib.metadata.metadata("autogen-ext").get_all("Requires-Dist")` for
115
+ the full extras list). Three providers get a real, verified path; anything
116
+ else is a clear unsupported-provider error:
117
+
118
+ - `openai/<model>` -> `autogen_ext.models.openai.OpenAIChatCompletionClient
119
+ (model=<bare id>)` -- the native OpenAI client, per plan.md's explicit
120
+ instruction for this provider.
121
+ - `anthropic/<model>` -> `autogen_ext.models.anthropic.
122
+ AnthropicChatCompletionClient(model=<bare id>, model_info=...)` -- a real,
123
+ separately-shipped native client module (needs the `anthropic` package,
124
+ already a transitive dependency of this project's `claude`/`crewai`
125
+ extras and pinned directly in this adapter's own `autogen` extra).
126
+ CRITICAL LANDMINE, verified not assumed: this client's bundled model-name
127
+ table (`autogen_ext.models.anthropic._model_info._MODEL_INFO`) only knows
128
+ a handful of hardcoded, DATED model ids (e.g. `claude-opus-4-20250514`)
129
+ and falls back to fuzzy prefix-matching for anything else -- and that
130
+ fallback is buggy for exactly the kind of aliased model id this project's
131
+ own examples use: `"claude-sonnet-5".startswith("claude-2.0".split("-2")
132
+ [0])` == `"claude-sonnet-5".startswith("claude")` == True, so an unrelated
133
+ legacy entry (`claude-2.0`, `function_calling: False`) silently wins the
134
+ match -- verified directly: constructing the client on `"claude-sonnet-5"`
135
+ with no explicit `model_info` returns `function_calling: False`, and then
136
+ `AssistantAgent.__init__` raises "The model does not support function
137
+ calling" as soon as this adapter passes any tools/handoffs. This adapter
138
+ works around it by ALWAYS passing an explicit `model_info` for this
139
+ provider (`_ANTHROPIC_MODEL_INFO` below, `function_calling: True`),
140
+ bypassing the stale table entirely rather than trusting it for a model id
141
+ it clearly does not know about.
142
+ - `gemini/<model>` -> also `OpenAIChatCompletionClient(model=<bare id>,
143
+ model_info=...)`. There is no separate native Gemini client class shipped
144
+ anywhere in `autogen_ext.models` (the `autogen-ext[gemini]` extra only
145
+ pulls in `google-genai`, used by the unrelated `semantic-kernel` optional
146
+ integration, not by any model-client class) -- but `OpenAIChatCompletionClient.
147
+ __init__` itself special-cases Gemini: verified via `inspect.getsource`,
148
+ when the model name starts with `"gemini-"` it automatically points
149
+ `base_url` at Gemini's OpenAI-compatible endpoint
150
+ (`GEMINI_OPENAI_BASE_URL`) and reads `GEMINI_API_KEY` from the
151
+ environment if no `api_key` is given -- this genuinely IS "the shipped
152
+ Gemini path", it just lives inside the OpenAI client rather than a
153
+ dedicated module. Its bundled model-info table is INCOMPLETE, not buggy
154
+ (verified: `gemini-2.5-flash` is listed, but `gemini-2.5-pro` -- used
155
+ directly by the shipped example's `researcher` agent -- is not, and
156
+ raises `"model_info is required when model name is not a valid OpenAI
157
+ model"`), so this adapter always passes an explicit `model_info` here
158
+ too (`_GEMINI_MODEL_INFO` below), for the same reason as the Anthropic
159
+ path: don't trust an incomplete/stale table for a model id it might not
160
+ recognize. The base-url/api-key special-casing runs unconditionally
161
+ before that table is even consulted, so passing `model_info` explicitly
162
+ does not disable it -- verified by constructing the client both ways.
163
+ - Anything else (azure, bedrock, ollama, cohere, mistral, ...) raises a
164
+ clear `ValueError` naming the agent, its resolved model string, and the
165
+ fix options (an `openai/`, `anthropic/`, or `gemini/`-prefixed model; a
166
+ different alias; or a `targets.autogen.model` override).
167
+
168
+ Unlike the Claude Agent SDK adapter (M5), this target needs NO per-target
169
+ model overrides added to the shipped research-crew example to make it
170
+ buildable: the example's `fast` alias resolves to `gemini/gemini-2.5-flash`
171
+ and `smart` to `anthropic/claude-sonnet-5`, and researcher's own
172
+ `gemini/gemini-2.5-pro` is used directly -- all three are covered by the
173
+ native Gemini/Anthropic paths above with no escape hatch needed.
174
+
175
+ Per-target override (`targets.autogen.model` in `agent-config.yaml`):
176
+ always wins, and is passed through as the bare model id to
177
+ `OpenAIChatCompletionClient` -- the "default client" of this adapter (the
178
+ same one the `openai/...` provider branch above uses), with NO explicit
179
+ `model_info` (unlike the `anthropic/gemini` provider branches): an override
180
+ is assumed to already be a valid, SDK-native identifier the project author
181
+ vouches for, exactly like every other adapter's override handling ("already
182
+ SDK-native form, passed through as-is"). If that id isn't in
183
+ `OpenAIChatCompletionClient`'s own known-model table, the SDK's own clear
184
+ `model_info is required` error surfaces -- at which point the fix is the
185
+ same escape hatch every other adapter documents for its overrides: it needs
186
+ to be a model this client actually knows, or the caller composes their own
187
+ client outside `project.build(...)`.
188
+
189
+ model_params: `OpenAIChatCompletionClient` and `AnthropicChatCompletionClient`
190
+ DO NOT share one parameter set -- investigated at the level that actually
191
+ matters (the runtime whitelist each client filters constructor kwargs
192
+ through before building its `create_args`, `_create_args_from_config` in
193
+ each client's own module), not just each client's `CreateArguments`
194
+ `TypedDict` type hints (which turn out to be a red herring here: passing an
195
+ unsupported kwarg like `seed` to `AnthropicChatCompletionClient` does NOT
196
+ raise at construction time -- it is silently accepted and then silently
197
+ DROPPED by that filter, never reaching the Anthropic API, which is worse
198
+ than an error if this adapter mapped it blindly). Verified directly against
199
+ both real whitelists: `autogen_ext.models.openai._openai_client.
200
+ create_kwargs` contains `temperature`, `max_tokens`, `top_p`, `stop`,
201
+ `presence_penalty`, `frequency_penalty`, `seed` (no `top_k`);
202
+ `autogen_ext.models.anthropic._anthropic_client.anthropic_message_params`
203
+ contains `temperature`, `max_tokens`, `top_p`, `top_k`, `stop_sequences` (no
204
+ `presence_penalty`, `frequency_penalty`, `seed`, and the key is
205
+ `stop_sequences`, not `stop`). So this adapter maps two SEPARATE dicts,
206
+ `_OPENAI_MODEL_PARAM_MAP` (used for the `openai`/`gemini` provider branches
207
+ and the per-target override, all three of which build an
208
+ `OpenAIChatCompletionClient`) and `_ANTHROPIC_MODEL_PARAM_MAP` (the
209
+ `anthropic` provider branch only) -- unlike every flat-single-map adapter in
210
+ this codebase. Any key absent from whichever map applies is
211
+ warned-and-ignored, per the same policy every other adapter applies to keys
212
+ it doesn't map.
213
+
214
+ Offline construction -- a real difference from every other adapter here,
215
+ investigated not assumed: `OpenAIChatCompletionClient`/
216
+ `AnthropicChatCompletionClient.__init__` EAGERLY construct the underlying
217
+ `openai.AsyncOpenAI`/`anthropic.AsyncAnthropic` client right there in
218
+ `build()` -- and that raises immediately (`openai.OpenAIError: "Missing
219
+ credentials..."`) if no `api_key` is given AND the matching env var
220
+ (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY`) isn't set --
221
+ verified directly, with the relevant env vars cleared. This is unlike
222
+ openai-agents' `Agent` (no client touched until `Runner.run` actually
223
+ executes) and unlike this project's `requires.env` mechanism (which is for
224
+ an agent's own tool-level env vars, e.g. `TAVILY_API_KEY` -- model-provider
225
+ auth has never been part of that contract for any target). Net effect:
226
+ `build()` for this target fails loudly on a missing provider API key all by
227
+ itself, with the SDK's own error text, un-wrapped -- this adapter does not
228
+ catch or re-word it, the same restraint every other adapter shows for
229
+ errors it doesn't specifically own. Tests in this codebase set fake
230
+ `OPENAI_API_KEY`/`ANTHROPIC_API_KEY`/`GEMINI_API_KEY` values up front for
231
+ exactly this reason (see test_adapter_autogen.py) -- construction never
232
+ makes a network call, but it does require *a* key-shaped string to exist
233
+ somewhere.
234
+ """
235
+
236
+ from __future__ import annotations
237
+
238
+ import warnings
239
+ from typing import TYPE_CHECKING, Any
240
+
241
+ from autogen_agentchat.agents import AssistantAgent
242
+ from autogen_agentchat.teams import Swarm
243
+ from autogen_core.models import ModelFamily, ModelInfo
244
+ from autogen_ext.models.anthropic import AnthropicChatCompletionClient
245
+ from autogen_ext.models.openai import OpenAIChatCompletionClient
246
+
247
+ if TYPE_CHECKING:
248
+ from ..models import AgentSpec, Project
249
+
250
+ from .base import BaseAdapter
251
+
252
+ # agent-config.yaml `model_params` key -> OpenAIChatCompletionClient
253
+ # constructor kwarg. Used for the `openai`/`gemini` provider branches and the
254
+ # per-target override (see module docstring, "model_params").
255
+ _OPENAI_MODEL_PARAM_MAP = {
256
+ "temperature": "temperature",
257
+ "max_tokens": "max_tokens",
258
+ "top_p": "top_p",
259
+ "stop": "stop",
260
+ "presence_penalty": "presence_penalty",
261
+ "frequency_penalty": "frequency_penalty",
262
+ "seed": "seed",
263
+ }
264
+
265
+ # agent-config.yaml `model_params` key -> AnthropicChatCompletionClient
266
+ # constructor kwarg. Used for the `anthropic` provider branch only -- this
267
+ # client's genuinely-accepted parameter set is smaller and differently named
268
+ # (`stop` -> `stop_sequences`) than the OpenAI-family client above (see
269
+ # module docstring, "model_params").
270
+ _ANTHROPIC_MODEL_PARAM_MAP = {
271
+ "temperature": "temperature",
272
+ "max_tokens": "max_tokens",
273
+ "top_p": "top_p",
274
+ "top_k": "top_k",
275
+ "stop": "stop_sequences",
276
+ }
277
+
278
+ # Explicit model_info for the `anthropic/...` provider branch -- bypasses
279
+ # autogen_ext's stale/buggy bundled Anthropic model-name table entirely (see
280
+ # module docstring, "Model routing"). `family: ModelFamily.UNKNOWN` is
281
+ # deliberate: this adapter doesn't know or assert which Claude generation a
282
+ # given alias/model id maps to, only that it is Anthropic-native and
283
+ # supports function calling (a requirement of every commonadk agent that
284
+ # has tools or outgoing edges).
285
+ _ANTHROPIC_MODEL_INFO: ModelInfo = {
286
+ "vision": False,
287
+ "function_calling": True,
288
+ "json_output": True,
289
+ "family": ModelFamily.UNKNOWN,
290
+ "structured_output": False,
291
+ "multiple_system_messages": False,
292
+ }
293
+
294
+ # Explicit model_info for the `gemini/...` provider branch -- bypasses
295
+ # autogen_ext's incomplete bundled Gemini model-name table (see module
296
+ # docstring, "Model routing"). Vision/structured_output reflect what every
297
+ # modern Gemini model actually supports; family is intentionally generic
298
+ # for the same reason as the Anthropic table above.
299
+ _GEMINI_MODEL_INFO: ModelInfo = {
300
+ "vision": True,
301
+ "function_calling": True,
302
+ "json_output": True,
303
+ "family": ModelFamily.UNKNOWN,
304
+ "structured_output": True,
305
+ "multiple_system_messages": False,
306
+ }
307
+
308
+
309
+ class AutoGenAdapter(BaseAdapter):
310
+ target = "autogen"
311
+
312
+ def build(self, project: "Project", agent_name: str) -> Any:
313
+ self._check_env(project, agent_name)
314
+
315
+ reachable = self._reachable_agents(project, agent_name) # agent_name first
316
+
317
+ agents: dict[str, AssistantAgent] = {}
318
+ for name in reachable:
319
+ spec = project.agents[name]
320
+ handoff_targets = [
321
+ edge.to for edge in project.graph.edges if edge.from_ == name
322
+ ]
323
+ agents[name] = AssistantAgent(
324
+ name=spec.name,
325
+ model_client=self._client_for(project, spec),
326
+ tools=[t.func for t in spec.tools],
327
+ handoffs=handoff_targets,
328
+ system_message=spec.instructions,
329
+ description=spec.config.description,
330
+ )
331
+
332
+ has_outgoing = any(edge.from_ == agent_name for edge in project.graph.edges)
333
+ if not has_outgoing:
334
+ # Nothing for the build root to hand off to -- handoffs only do
335
+ # anything inside a team, so the bare agent is the honest,
336
+ # directly runnable object here (see module docstring, "WHAT
337
+ # build() RETURNS").
338
+ return agents[agent_name]
339
+
340
+ participants = [agents[name] for name in reachable] # root first
341
+ return Swarm(participants, max_turns=len(reachable))
342
+
343
+ # -- model routing ------------------------------------------------------
344
+
345
+ def _client_for(self, project: "Project", spec: "AgentSpec") -> Any:
346
+ override = spec.config.targets.get("autogen", {})
347
+ if "model" in override:
348
+ # Per-target override: passed through as the bare model id to
349
+ # the default client, no explicit model_info -- see module
350
+ # docstring, "Per-target override". Always the OpenAI-family
351
+ # client, so the OpenAI param map applies.
352
+ kwargs = self._model_param_kwargs(spec, _OPENAI_MODEL_PARAM_MAP)
353
+ return OpenAIChatCompletionClient(model=override["model"], **kwargs)
354
+
355
+ resolved = project.resolve_model(spec.name) # LiteLLM-format string
356
+ provider, sep, rest = resolved.partition("/")
357
+ if sep and provider == "openai":
358
+ kwargs = self._model_param_kwargs(spec, _OPENAI_MODEL_PARAM_MAP)
359
+ return OpenAIChatCompletionClient(model=rest, **kwargs)
360
+ if sep and provider == "anthropic":
361
+ kwargs = self._model_param_kwargs(spec, _ANTHROPIC_MODEL_PARAM_MAP)
362
+ return AnthropicChatCompletionClient(
363
+ model=rest, model_info=_ANTHROPIC_MODEL_INFO, **kwargs
364
+ )
365
+ if sep and provider == "gemini":
366
+ kwargs = self._model_param_kwargs(spec, _OPENAI_MODEL_PARAM_MAP)
367
+ return OpenAIChatCompletionClient(
368
+ model=rest, model_info=_GEMINI_MODEL_INFO, **kwargs
369
+ )
370
+
371
+ raise ValueError(
372
+ f"commonadk: agent {spec.name!r} resolves to model {resolved!r}, "
373
+ f"but the AutoGen target ('autogen') only ships native model "
374
+ f"clients for 'openai/...', 'anthropic/...', and 'gemini/...' "
375
+ f"providers (see autogen_adapter.py's module docstring, 'Model "
376
+ f"routing'). Fix this by either: using one of those providers "
377
+ f"(e.g. 'openai/gpt-4o'), changing {spec.name}'s model alias in "
378
+ f"config.yaml to one that resolves to a supported provider, or "
379
+ f"adding a `targets.autogen.model` override to "
380
+ f"{spec.name}/agent-config.yaml with a bare model id understood "
381
+ f"by autogen_ext's OpenAIChatCompletionClient."
382
+ )
383
+
384
+ def _model_param_kwargs(
385
+ self, spec: "AgentSpec", param_map: dict[str, str]
386
+ ) -> dict[str, Any]:
387
+ kwargs: dict[str, Any] = {}
388
+ for key, value in spec.config.model_params.items():
389
+ mapped = param_map.get(key)
390
+ if mapped is None:
391
+ warnings.warn(
392
+ f"{spec.name}: model_params key '{key}' is not supported "
393
+ f"by the AutoGen adapter and will be ignored",
394
+ stacklevel=2,
395
+ )
396
+ continue
397
+ kwargs[mapped] = value
398
+ return kwargs
@@ -0,0 +1,75 @@
1
+ """Common interface every per-SDK adapter implements.
2
+
3
+ See plan.md ("Adapt") for the architecture this slots into: one adapter per
4
+ target SDK, each turning a framework-neutral `Project` + agent name into a
5
+ live, SDK-native agent object.
6
+
7
+ `_reachable_agents` and `_check_env` started life in M2's Google ADK adapter
8
+ and were hoisted here in M3 so both the Google ADK and OpenAI Agents adapters
9
+ share one implementation of "which agents does this build touch" and "fail
10
+ loudly, up front, if any of them is missing a required env var" -- the logic
11
+ is identical across targets, only the target name in the error message
12
+ differs (via `self.target`). This module stays free of any agent SDK
13
+ import -- `commonadk.load()` must keep working with no agent SDK installed
14
+ at all.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from abc import ABC, abstractmethod
20
+ from typing import TYPE_CHECKING, Any
21
+
22
+ if TYPE_CHECKING:
23
+ from ..models import Project
24
+
25
+
26
+ class BaseAdapter(ABC):
27
+ """One adapter per target SDK, registered in `commonadk.adapters.get_adapter`."""
28
+
29
+ target: str
30
+
31
+ @abstractmethod
32
+ def build(self, project: "Project", agent_name: str) -> Any:
33
+ """Build and return a live, SDK-native agent object for `agent_name`."""
34
+ raise NotImplementedError
35
+
36
+ # -- env preflight (shared across targets) ---------------------------
37
+
38
+ def _check_env(self, project: "Project", agent_name: str) -> None:
39
+ """Fail loudly, up front, if any reachable agent is missing a required env var.
40
+
41
+ Checks `agent_name` and every agent reachable from it via
42
+ `interactions.yaml` edges (not just its direct sub_agents/handoffs)
43
+ -- building the graph can transitively depend on any of them.
44
+ """
45
+ missing_lines: list[str] = []
46
+ for name in self._reachable_agents(project, agent_name):
47
+ agent = project.agents[name]
48
+ missing_names = set(project.check_env(name))
49
+ if not missing_names:
50
+ continue
51
+ for req in agent.config.requires.env:
52
+ if req.name in missing_names:
53
+ detail = f"{req.name} ({req.description})" if req.description else req.name
54
+ missing_lines.append(f" - {name}: {detail}")
55
+
56
+ if missing_lines:
57
+ raise OSError(
58
+ "commonadk: missing required environment variable(s) for "
59
+ f"target {self.target!r} (building '{agent_name}'):\n"
60
+ + "\n".join(missing_lines)
61
+ )
62
+
63
+ def _reachable_agents(self, project: "Project", start: str) -> list[str]:
64
+ """Every agent name reachable from `start` (via edges), including `start`."""
65
+ seen = [start]
66
+ seen_set = {start}
67
+ i = 0
68
+ while i < len(seen):
69
+ current = seen[i]
70
+ i += 1
71
+ for edge in project.graph.edges:
72
+ if edge.from_ == current and edge.to not in seen_set:
73
+ seen_set.add(edge.to)
74
+ seen.append(edge.to)
75
+ return seen