Skip to main content

CommonADK — define agents once, build on any agent SDK

CommonADK

CommonADK is a LiteLLM-style layer for agent SDKs: define an agent system once, in a framework-neutral common/ folder, and materialize it into any supported agent SDK — no hand-written per-SDK agent code, no copy-pasted prompts or tool wrappers per framework.

Hypothesis: an agent system can be defined once and built, unmodified, on multiple SDKs. The bet CommonADK makes is that common/ can be the single source of truth, with a thin runtime adapter per SDK doing the translation — the same way LiteLLM lets you call any LLM provider through one interface. The v1 success criterion (see plan.md): the same common/ folder builds and runs unmodified on both Google ADK and the OpenAI Agents SDK. It does — examples/research-crew is proof.

Demo

Terminal recording of commonadk validate and examples/demo.py building the same common/ folder on all six SDK targets

Real, captured output — commonadk validate on the shipped research-crew example, then python3 examples/demo.py building the same coordinator agent, unmodified, on all six SDK targets. Run it yourself with examples/demo.py, or read the full unedited transcripts in docs/demo-runs.md.

Quickstart

commonadk is on PyPI: pip install commonadk gets you the core (loader, validator, mermaid renderer, CLI) with no agent SDK attached. Install with the extra(s) for the SDK(s) you want to build against:

pip install "commonadk[google]"   # Google ADK target
pip install "commonadk[openai]"   # OpenAI Agents SDK target
pip install "commonadk[claude]"   # Claude Agent SDK target
pip install "commonadk[crewai]"   # CrewAI target
pip install "commonadk[autogen]"   # AutoGen target
pip install "commonadk[langgraph]"   # LangGraph target
pip install "commonadk[google,openai,claude,crewai,autogen,langgraph]"   # all six

Installing multiple extras together. All six extras have been verified to resolve and install together (this repo runs its full test suite against all six at once), but two dependency tensions are worth knowing about — both harmless for everything commonadk itself touches: crewai pins openai<3, so installing it alongside openai-agents (which prefers openai>=3) resolves openai down to a 2.x release — pip then reports that as an unsatisfied requirement (pip check will flag it), but no code path this project exercises needs openai>=3. And autogen-core pins protobuf~=5.29, which pulls protobuf down from the newer major version several of google-adk's own dependencies (grpcio-status, google-api-core, ...) would otherwise prefer — again flagged by pip check, again with no observed effect on this project's own code. Neither tension breaks import commonadk, commonadk validate|render, or any adapter's build().

Point it at the shipped example, a three-agent research crew (coordinator → researcher → writer):

import commonadk

project = commonadk.load("examples/research-crew/common")   # parse + validate
agent = project.build("coordinator", target="google-adk")   # live google.adk.Agent
agent = project.build("coordinator", target="openai")       # live agents.Agent
options = project.build("coordinator", target="claude")     # claude_agent_sdk.ClaudeAgentOptions
crew = project.build("coordinator", target="crewai")         # live crewai.Crew
agent = project.build("coordinator", target="autogen")       # AssistantAgent here; Swarm if the root has handoff edges
graph = project.build("coordinator", target="langgraph")     # compiled langgraph.graph.StateGraph

(target="claude" needs each agent's agent-config.yaml to carry a targets.claude.model override, since the example's default models are gemini — see "Supported targets" below.)

Or from the command line:

commonadk validate examples/research-crew/common
commonadk render examples/research-crew/common
commonadk run examples/research-crew/common --target openai "Research electric vehicle adoption"
commonadk new examples/research-crew/common reviewer --from writer --type handoff
commonadk import path/to/skills examples/research-crew/common

validate loads and checks a project, printing each agent's resolved model, tools, and required env vars (flagging which are actually set in your shell). render regenerates interaction-layer.md from interactions.yaml so the diagram never drifts from the spec. run builds one agent for a target SDK and executes a single turn — this one needs real API keys. new scaffolds a conforming agent folder (skill.md, tools.py, agent- config.yaml) inside an existing common/ — output that passes commonadk validate immediately — and refuses to overwrite an existing folder; --from <agent> --type {delegate,handoff} additionally appends an edge to interactions.yaml and regenerates interaction-layer.md for you. import turns a directory of SKILL.md files into a conforming common/ project — one agent folder per skill, skill.md copied verbatim (frontmatter included; see "skill.md's frontmatter" below), agent-config.yaml and a stub tools.py generated to match — and, like new, its output passes commonadk validate immediately.

Importing existing SKILL.md libraries. If you already have a directory of SKILL.md files — Spotify's portal-ai-plugins-style nested skills/<name>/SKILL.md, or a flat directory of *.md files — commonadk import <skills-dir> <common-dir> turns it into a common/ project with no edges invented between the imported agents (interactions.yaml gets an entry: and nothing else — you wire real relationships in afterward, e.g. with commonadk new --from). Run it again pointed at the same <common-dir> to add more skills to an already-imported project; it refuses to touch a directory that's neither empty nor already a valid commonadk project.

For a runnable, offline tour of all of the above — including project.build() against all six targets and real captured CLI output — see examples/demo.py and docs/demo-runs.md.

The common/ folder

common/
├── config.yaml              # project-wide config
├── interactions.yaml        # typed interaction edges (source of truth)
├── interaction-layer.md     # GENERATED mermaid rendering of interactions.yaml
└── <agent-name>/
    ├── skill.md              # instructions/persona -> system instructions
    ├── tools.py               # plain typed Python functions with docstrings
    └── agent-config.yaml     # per-agent config

config.yaml — project name, entry agent, and the model-alias table every agent's model: resolves against:

name: my-project
entry: coordinator
targets: [google-adk, openai]
default_model: fast
model_aliases:
  fast: gemini/gemini-2.5-flash
  smart: anthropic/claude-sonnet-5

<agent>/agent-config.yaml — model (an alias above, or a raw LiteLLM-format string), tools, and required env vars. requires.env declares names only — never values; commonadk validate and every adapter's build-time preflight check presence, not content:

name: researcher
model: gemini/gemini-2.5-pro
tools:
  - search_web
  - fetch_page
requires:
  env:
    - name: TAVILY_API_KEY
      description: Search API key used by search_web
      required: true

<agent>/skill.md — plain Markdown instructions, passed through as the agent's system prompt. Optional YAML frontmatter (---/YAML/--- at the very start of the file) is stripped from the instructions text; if it sets name or description, those are reconciled against agent-config.yaml (which always wins on a conflict) rather than just discarded — see docs/file-contracts.md for the full precedence rules. This is what lets commonadk import (above) carry a SKILL.md file's frontmatter straight through unmodified.

<agent>/tools.py — plain functions. Type hints on every parameter and a docstring are required, checked at validate time — they're what every adapter turns into that SDK's native tool schema:

def search_web(query: str) -> str:
    """Search the web for a query and return a short summary of top results.

    Args:
        query: The search query.
    """
    ...

interactions.yaml — the interaction graph, source of truth:

entry: coordinator
edges:
  - from: coordinator
    to: researcher
    type: delegate     # v1: delegate | handoff
  - from: researcher
    to: writer
    type: handoff

interaction-layer.md — GENERATED. commonadk render regenerates its mermaid block from interactions.yaml, so the diagram documents the graph instead of drifting from it:

flowchart TD
    coordinator(["coordinator (entry)"])
    researcher["researcher"]
    writer["writer"]
    coordinator -- delegate --> researcher
    researcher -. handoff .-> writer

Supported targets

Target target= Native model path LiteLLM-wrapped otherwise
Google ADK google-adk bare model id when the LiteLLM string is gemini/... google.adk.models.lite_llm.LiteLlm
OpenAI Agents SDK openai bare model id when the LiteLLM string is openai/... agents.extensions.models.litellm_model.LitellmModel
Claude Agent SDK claude bare model id when the LiteLLM string is anthropic/... no LiteLLM path — any other provider is a clear build-time error
CrewAI crewai crewai.LLM(model=...) takes the resolved LiteLLM-format string directly, for every provider — no allowlist, no build-time error routes to a native provider client when it recognizes one, else falls back to litellm's completion() itself
AutoGen autogen bare model id for openai/..., anthropic/..., and gemini/... (native autogen_ext model clients) no LiteLLM path — any other provider is a clear build-time error
LangGraph langgraph "provider:model" via init_chat_model for gemini/... (→ google_genai), openai/..., and anthropic/... no LiteLLM path — any other provider is a clear build-time error

Every agent's model: (an alias from config.yaml or a raw LiteLLM string like anthropic/claude-sonnet-5) resolves the same way regardless of target; each adapter then decides whether its SDK can speak that provider natively or needs the SDK's own LiteLLM wrapper. A per-agent targets.<sdk>.model override in agent-config.yaml bypasses resolution entirely and is passed through as-is, for when you need an SDK-native model form commonadk doesn't infer.

The Claude Agent SDK runs Anthropic models only — it has no LiteLLM wrapper at all, so an agent whose model resolves to any provider other than anthropic/... fails to build for target="claude" with a clear error naming the agent, its resolved model string, and how to fix it (an anthropic/... model, a different alias, or a targets.claude.model override). This is why research-crew's agent-config.yaml files each carry a targets.claude.model: claude-sonnet-5 override — the project's default models are gemini.

AutoGen is similar in spirit: no LiteLLM path, but three native model clients (openai/..., anthropic/..., gemini/...) instead of one, so the shipped example builds unmodified with no targets.autogen.model overrides needed at all. Any other provider still fails to build with the same style of clear, actionable error.

Delegate and handoff, per SDK

interactions.yaml has one edge vocabulary — delegate and handoff — but each of the six SDKs expresses "one agent routes work to another" through a genuinely different data shape, and each adapter honors that shape rather than flattening it away. Starting with the two SDKs v1 targeted: Google ADK's sub_agents form a strict tree: an agent instance can have exactly one parent, so a common/ graph reachable from the build root must itself be a tree, or the adapter raises before constructing anything (a cycle, or an agent reachable from two parents, is rejected up front). The OpenAI Agents SDK's handoffs are a plain list of references: the same Agent instance can sit in more than one parent's handoffs list, and cycles are just references wired up after construction. So the identical interactions.yaml — including graphs with a shared sub-agent or even a cycle — builds happily as an OpenAI Agents handoff graph while the same shape can be legitimately rejected for Google ADK. Since v0.0.2 both adapters also distinguish delegate from handoff within themselves: Google ADK maps delegate to an AgentTool invocation that returns and handoff to a sub_agents transfer that does not, and the OpenAI Agents SDK maps delegate to as_tool() and handoff to its handoffs list. A delegate edge is exempt from the one-parent tree constraint on Google ADK, because AgentTool never sets a parent.

The Claude Agent SDK is session/query-based, not agent-object-based: instead of a live agent, project.build(..., target="claude") returns a claude_agent_sdk.ClaudeAgentOptions — the config object you pass to that SDK's query(). Its subagents (options.agents) are declared once, in a single flat dict[str, AgentDefinition], not a nested tree — and per the SDK's own docs, any agent whose tools include the Agent tool can invoke any name in that flat registry, not just a parent-declared child. So this adapter registers every agent reachable from the build root (the whole transitive closure, however deep) into that one flat dict, and grants Agent tool access only to the reachable agents that actually have an outgoing edge in interactions.yaml — a deep edge (e.g. researcher -> writer when building coordinator) is fully representable this way, and — like OpenAI Agents' reference-based handoffs — a shared sub-agent or a cycle needs no special rejection either: the flat dict just holds one entry per logical agent name. See adapters/claude_agent.py's module docstring for the full investigation and the tool-isolation mechanics (each agent's own tools.py functions become an in-process MCP server it alone can see).

CrewAI is the one target where interactions.yaml's edge targets are coarsened, not fully honored: project.build(..., target="crewai") builds the requested agent as a hierarchical crew's manager_agent (or, if it has no reachable agents at all, as the sole member of a solo sequential crew) with every other reachable agent as a flat crew member — but CrewAI's one delegation mechanism (allow_delegation=True) is crew-wide: an agent that can delegate can reach any other crew member, not just the agents interactions.yaml actually points it at. Both delegate and handoff edges map to this one mechanism. What the graph still controls: whether an agent can delegate at all (only agents with an outgoing edge get allow_delegation=True) and scope (only agents reachable from the build root join the crew). See adapters/crewai_adapter.py's module docstring for the full investigation, including why the manager role can't carry its own tools.

LangGraph is the one target where interactions.yaml's edge targets are honored precisely, not coarsened — the opposite end of the spectrum from CrewAI above: project.build(..., target="langgraph") gives each reachable agent a prebuilt react-agent node (langchain.agents.create_agent) in one compiled langgraph.graph.StateGraph, and for every outgoing interactions.yaml edge it adds exactly one clearly-named tool to the source agent — an agent can reach only the destinations the graph actually names, nothing more. Since v0.0.2 the two edge types get genuinely different tools: a handoff edge adds transfer_to_<destination>, which returns LangGraph's own Command(goto=<destination>, graph=Command.PARENT) so control transfers and does not come back, while a delegate edge adds delegate_to_<destination>, which invokes an independently built graph for the destination and returns its result to the caller. Neither needs a langgraph-supervisor/langgraph-swarm dependency.

One consequence worth knowing: a build root whose only outgoing edges are delegates is no longer wrapped in a multi-node graph at all — it comes back as a bare, directly runnable agent, since the wrapper only ever encoded handoff. The same is true of AutoGen and its Swarm. Multi-parent graphs and cycles need no special handling: every reachable agent is built once into a flat, name-keyed node registry, so a shared destination or a cycle back to the build root is just another named handoff tool pointing at a node that already exists. See adapters/langgraph_adapter.py's module docstring for the full investigation, including why langchain.agents.create_agent is used instead of the now-deprecated langgraph.prebuilt.create_react_agent.

Mixed-target spawning

Every example above builds one common/ project under one SDK at a time. project.build_mixed(agent_name, default_target=...) relaxes that: any agent can pin itself to its own SDK with runtime: in its agent-config.yaml, and agents that leave it unset fall back to default_target — a project with no runtime: set anywhere behaves exactly as before. Agents sharing a runtime and connected by interactions.yaml edges are still built as one true native sub-graph by their own adapter (an ADK sub_agents tree stays a tree); only the edges that cross runtimes are bridged, as a plain callable "transfer to <agent>" tool on the crossing edge's source agent — the same "every SDK wraps a plain typed function into a tool" mechanism this whole project is built on. google-adk, openai, crewai, and claude can source a cross-runtime edge in v1; autogen and langgraph cannot yet (see docs/mixed-target-design.md for exactly why, verified against each installed SDK) — every target can be the destination of one either way. See examples/mixed-crew for a small two-runtime project, and the design doc for the full three-layer model, the island algorithm, and the failure modes. This is in-process only — no network protocol between SDKs yet; that's future work (issue #9).

Running and observing

commonadk run executes one turn and, by default, still just prints the final text — unchanged for anyone already using it. For google-adk and openai (src/commonadk/runners/, the execution/telemetry layer built for #22), two more flags turn that one line into a full, normalized step trace:

commonadk run examples/research-crew/common --target openai --stream \
  "Research electric vehicle adoption"

commonadk run examples/research-crew/common --target google-adk \
  --trace trace.json "Research electric vehicle adoption"

--stream prints one line per normalized runtime event (run_started/agent_started/llm_call/tool_call/transfer/ agent_finished/run_finished-or-run_error) as it happens; --trace PATH writes the full ordered event log plus roll-up totals (tokens, cost, wall time, a breakdown per agent) to PATH as JSON. Both work programmatically too, against the same Project any adapter builds from:

from commonadk import load
from commonadk.runners import get_runner

project = load("examples/research-crew/common")
runner = get_runner("openai")               # or "google-adk"
trace = runner.run_sync(project, "coordinator", "Research EV adoption")

print(trace.rollup())                         # totals, honest about gaps
trace.write("trace.json")

Every token/cost field is Optional, and None means "this SDK did not report it" — never 0, never a guess. A trace's roll-up marks itself usage_complete: false (with an explanation, not a silently wrong number) whenever any LLM call in the run didn't report usage; cost is derived only from tokens an SDK actually reported, against a small static pricing table that's labeled as a snapshot, not a live feed. Multi-turn conversations use a RunSession:

from commonadk.runners import RunSession

session = RunSession()
runner.run_sync(project, "coordinator", "What's the latest on EV adoption?", session=session)
runner.run_sync(project, "coordinator", "Now compare that to 2020.", session=session)  # same conversation

Hooks observe every event live (v1 is observe-only — a hook can't block or rewrite anything, only watch):

from commonadk.runners import HookRegistry

hooks = HookRegistry()
hooks.register(lambda event: print(event.kind, event.seq))
runner.run_sync(project, "coordinator", "hi", hooks=hooks)

All six targets have a runner as of v0.0.2, so --stream and --trace work everywhere. Should an adapter ever land before its runner does, those two flags give a clear error naming which targets do support them, rather than a silent no-op. See docs/runner-design.md for the full design: the normalized event model, the per-SDK mapping (including exactly where token/cost data is and isn't available), the session model, and the hook contract.

What each SDK reports differs, and the traces say so rather than papering over it. The Claude Agent SDK computes its own cost, so that runner uses the SDK's figure instead of the static price table. CrewAI reports genuine per-call usage from its event bus. Where an SDK reports nothing, the trace carries null with usage_complete: false — never a 0, which would read as a call that was free.

Verified live runs

Five of the six targets have now executed a real turn from the same unmodified agent definition — see the captured run in docs/demo-runs.md for the verbatim results, including real token counts and cost. examples/live-smoke and scripts/live_smoke.py (issue #8) are what produce them: a minimal project whose default_model is anthropic/claude-haiku-4-5 routes all six targets through the same Anthropic model with no per-target override, and the script runs one real turn per target, recording outcome, wall time, and — for google-adk/openai — a full token/cost trace via commonadk.runners (the four other targets have no runner yet, so their report entries say "usage": "unavailable" explicitly, never 0). It's wired up as a workflow_dispatch-only GitHub Actions workflow (.github/workflows/live-runs.yml) so it only runs — and only spends money — when a maintainer explicitly triggers it. See docs/demo-runs.md for how to trigger it and what the report looks like.

Roadmap

The full feature map — shipped, next up, and planned — lives in ROADMAP.md, with a GitHub issue per open item where discussion happens. Headliners: mixed-target spawning (pinning individual agents to different SDKs within one project, #9) and richer edge semantics (pipelines, loops, shared state, #10). Design history and settled decisions stay in plan.md.

Metadata

Release files for commonadk 0.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for commonadk 0.0.2
File Size Uploaded Provenance Actions
commonadk-0.0.2.tar.gz 658.1 kB Without provenance Details for commonadk-0.0.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for commonadk 0.0.2
File Interpreter ABI Platform Actions
commonadk-0.0.2-py3-none-any.whl Python 3 none any Details for commonadk-0.0.2-py3-none-any.whl

Total release size: 797.0 kB

Release files / commonadk-0.0.2.tar.gz

Download URL commonadk-0.0.2.tar.gz
Size 658.1 kB
Tags Source
SHA-256 checksum
How to use checksums
4b47819e0d9edcc926d26322e9dfabf7ab8b529bfe82dea26915f52b639fc858
BLAKE2b-256 checksum
How to use checksums
76b2a620768c1ed24ffa313470e4fd3905c522e80f7265e637967218cefe6c24
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / commonadk-0.0.2-py3-none-any.whl

Download URL commonadk-0.0.2-py3-none-any.whl
Size 138.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8c6a2e3db8307c6629649240d83f0c9a4bbb77a71becc1e27eee0e9175adbf35
BLAKE2b-256 checksum
How to use checksums
554096198ad48db0a8ab28f2b214a584ae950610449af2f5de4dfa0db17536cc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for commonadk 0.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page