An SDK for consuming Agent Fabric capabilities — governed model and tool access — from your own agent framework, in your own IDE, without adopting Mule.
Project status — alpha. This is an early release (
Development Status :: 3 - Alpha). The LLM data plane is live-verified; most other surfaces are verification-gated (see What's verified below). Install it from PyPI withpip install donkey-kit— see Install. Unofficial: an independent project, not affiliated with or endorsed by Salesforce or MuleSoft.
"Agent Fabric" is a MuleSoft (Salesforce) product name, not a generic term.
MuleSoft,Anypoint,Omni Gateway, andAgent Fabricare Salesforce trademarks.Maintainer & support. This is an independent, community-maintained project, published under the org-scoped
Donkey-Development-Kitname — it is not affiliated with, endorsed by, or supported by Salesforce or MuleSoft. It is provided as-is, without warranty of any kind; the maintainers triage issues and pull requests on a best-effort basis, with no SLA. Because it ships under a distinct, org-scoped name, only the descriptive form ("an SDK for MuleSoft Agent Fabric") appears in prose — the package does not represent itself as a first-party, official-status SDK.Licensed under Apache-2.0. See the unsupported boundary for exactly which platform APIs this SDK calls and their support classification.
Security. Report vulnerabilities privately, never in a public issue. See
SECURITY.mdfor supported versions and how to report.Python versions. CPython 3.10–3.14, each tested in CI; a version is dropped in the first minor release after its end of life. See
docs/python-support.md.
Contents
- Documentation
- Install
- Framework support
- What's verified (verification discipline)
- Conformance exemptions
Two audiences, two doc sets:
- Use the SDK → the documentation site: https://docs.donkey-kit.dev/. Install and configure, per-framework model access, the governed error taxonomy, and what to trust today — everything you need to point your agent at a governed proxy.
- See it run → runnable demos live in the companion repo donkey-development-kit-demos: the framework-free client, native framework objects, the governed error taxonomy, and the screen-recording scripts.
- Understand or contribute to the repo:
ARCHITECTURE.md— how the SDK is built: the layered stack, the framework-free core, verification discipline, the error taxonomy, and framework tiering.CONTRIBUTING.md— how to work in the repo: the branch/PR/release workflow, the testing strategy, coding conventions, and the docs-sync rule.docs/verified-apis.md— the verification ledger: the single source of truth for what is confirmed against a real sandbox and what is still blocked.
pip install "donkey-kit[llm,langgraph]" # base + raw client + one frameworkTo work on the SDK itself, install from source with the contributor tooling
(the dev dependency group needs pip 25.1 or later):
git clone https://github.com/Donkey-Development-Kit/donkey-development-kit.git
cd donkey-development-kit/python
pip install -e ".[llm,cli]" --group devExtras are one per framework (langgraph, adk, strands, agent_framework,
openai-agents, anthropic, crewai, llamaindex) plus otel, cli, local,
a2a (the official a2a-sdk, for donkey.serve()), mcp (the MCP client a
governed ToolSet binds with), test (the conformance pytest plugin —
pytest --donkey-conformance --donkey-agent=my_app.agent:build), and all. all is
everything a user runs that installs together — llm, langgraph, otel, cli,
local, a2a, mcp — with no test runner, so add test for the conformance plugin:
donkey-kit[all,test]. It leaves out the seven other framework extras, because
whether they install together depends on their upstream releases. The generated
co-installability table shows which combinations resolve, re-checked
nightly. Add the one framework you use: donkey-kit[all,crewai].
Configuration and first-agent walkthroughs live on the
documentation site. Features that call the Anypoint
control plane (registry, tool discovery, donkey publish) need a connected app
with the scopes listed per operation in docs/scopes.md.
The roster is deliberately two deep, six shallow (BG §1.8): LangGraph and
the OpenAI Agents SDK are held to the full conformance bar, the rest supported
through the three-line connection_kwargs() escape hatch. Every framework below returns its framework's
own native object — never a wrapper.
Each factory's status is its constructor row in the docs/verified-apis.md §8; its
depth is the adapter roster's conformance_tested.
| Framework | Factory | Depth | Ledger status |
|---|---|---|---|
| LangGraph | donkey.langgraph.chat_model() |
Conformance-tested | Live-verified |
| Google ADK | donkey.adk.model() |
connection_kwargs() |
Live-verified |
| Google ADK | donkey.adk.gemini() |
connection_kwargs() |
Live-verified |
| Strands | donkey.strands.model() |
connection_kwargs() |
Live-verified |
| MS Agent Framework | donkey.agent_framework.chat_client() |
connection_kwargs() |
Live-verified |
| OpenAI Agents SDK | donkey.openai_agents.model() |
Conformance-tested | Live-verified |
| Anthropic SDK | donkey.anthropic.client() |
connection_kwargs() |
Live-verified |
| CrewAI | donkey.crewai.llm() |
connection_kwargs() |
Live-verified |
| LlamaIndex | donkey.llamaindex.llm() |
connection_kwargs() |
Live-verified |
Signature-confirmed means the factory builds its native object against the
installed framework (scripts/verify_frameworks.py), with no live round-trip.
Live-verified means a real round-trip through a governed proxy. The raw
client (donkey.llm.client()) is conformance-tested too.
connection_kwargs() works for all eight; a deep adapter is promoted from demand
evidence, one at a time — never guessed up front. The OpenAI Agents SDK was the
second, on Phase 1 demand (#223); the next is #244. See the
framework pages
for each. Which framework versions are supported and tested is on the site's
version compatibility
page and in docs/framework-support.md.
The LLM data plane — governed model access through the Omni Gateway proxy — is live-verified against a real Anypoint sandbox. The framework-free client and the framework adapters are wired to that contract; the raw client is conformance-tested against the simulator. For the adapters (each factory is listed under Framework support):
LangGraph and OpenAI Agents SDK are conformance-tested against the simulator in CI. Each adapter factory's constructor row in §8 of the ledger reads live-verified.
Everything still gated raises NotImplementedError("blocked on verification: …")
rather than guessing at an unverified endpoint, header, or class name. That
currently covers a Publication's descriptor derivation (preview(), export()),
the lockfile (donkey.tools.lock(), discover(locked=True)), the governed-state
search filters (search(governed=, environment=, domain=)) and the hidden
donkey status command. Exchange search and the publication of scanned MCP
servers and agents (donkey publish, donkey verify) are verified against a
live sandbox. MCP tool discovery and every framework's ToolSet binding are
verified through the governed gateway: behind Client ID Enforcement all eight
frameworks called a bound tool (#1312); behind JWT Validation seven did and
ADK's tools were listed but not called (#221). Their types (ToolSet,
GovernanceCriteria, UngovernedMcpServer, …) are exported from donkey_kit. The SDK
does not ship a provisioning control plane (ADR 0008 in docs/adr/). The adapters build their framework's native object directly; they
refuse only when the installed framework version lacks the class or field the
adapter depends on.
The discipline behind this is documented in
ARCHITECTURE.md → Verification discipline;
the row-by-row worklist is docs/verified-apis.md.
The conformance plugin
holds the SDK to the same bar it asks of your agent. Where a framework
legitimately cannot satisfy a scenario, the reason is asserted in code
(KNOWN_LIMITATIONS) and published here as credibility — never a silent skip
(the conformance kit):
| Framework | Scenario | Why it's exempt |
|---|---|---|
| CrewAI | correlation ID propagated | CrewAI's native OpenAI provider builds both its sync OpenAI and its AsyncOpenAI from one client_params dict, and with an interceptor set it replaces http_client with its own httpx client, so the SDK's async client cannot be injected and the correlation ID ends up per-client, not per-run. ADK (model() and gemini()), LlamaIndex and Microsoft Agent Framework send through the SDK's shared client and record no exemption (#691, #740). |
| CrewAI | gateway identity observed | For the same reason, no response reaches the SDK's _on_response hook. When every resolved adapter is non-observing, donkey.last_call reports UNAVAILABLE and names them in surface. |
| CrewAI | JWT refreshed per send | CrewAI's native OpenAI provider owns the transport and builds its own clients, so the rotating JWT the SDK adds per send never reaches its requests. donkey.crewai.llm() and connection_kwargs() raise ConfigError in jwt mode; use client-id auth with CrewAI. ADK's model() and gemini(), LlamaIndex and Microsoft Agent Framework send through the SDK's client and carry the rotating JWT on async calls (jwt mode). |
| CrewAI | budget refusal not retried | CrewAI wraps every LLM call in its own rate-limit retry (3 attempts) and treats any 429 as a rate limit, so a TokenBudgetExceeded or RequestRateLimitExceeded refusal is sent 3 times. CrewAI has no setting to turn it off; the OpenAI client underneath has max_retries=0. Every other adapter sends a budget refusal once (#734). |
| CrewAI | typed refusal bridged | CrewAI's native OpenAI provider owns the transport, so its errors come from a client the SDK never saw, and donkey.run() and typed_refusals() cannot tell a gateway refusal from any other failure there and pass those errors through (#724). ADK's gemini() and model() send through the SDK's transport and are bridged (conformance-tested in #955 and #969). LiteLLM, under model(), re-raises a refusal as its own exception, and the adapter's llm_client marks that exception with the refusal the SDK's transport received. |
| CrewAI | refusal handler cannot un-refuse | CrewAI's native OpenAI provider owns the transport, so its refusals never reach the SDK's _on_refusal hook and handlers registered with donkey.on(...) do not run for its calls. Every other adapter runs them, and none can turn a refusal into a success (#208, #209). |
| CrewAI | custom policy typed | For the same reason, a refusal shape registered with classify.register() is neither typed by donkey.run() nor seen by donkey.on(...) handlers for CrewAI's calls. Every other adapter raises the registered type, runs its handlers and records its slug as donkey.policy.type (#210). |
| CrewAI | agent kill stops cleanly | CrewAI's native OpenAI provider owns the transport, so a kill-switch refusal never reaches the SDK's transport: it does not stop donkey.budget.pace() loops or flush spans, and CrewAI's own rate-limit retry decides whether the call is re-sent. Every other adapter sends the kill once, stops every pace loop on the same Donkey, and flushes spans before the AgentKilled reaches your code (#220). |
| CrewAI | approval round trip | CrewAI's native OpenAI provider owns the transport, so when an approved @donkey.tool(approval="required") resumes, its model call carries neither the approval's correlation ID nor a typed refusal. The pause and the approver's span event work as for any Python code. The round trip runs on the raw client and LangGraph (#211, #212), the OpenAI Agents SDK, which pauses on its own needs_approval and resumes from the RunState (#213), and Strands' and ADK's own pauses (#1234, #1235): each resumes the call under the decision's correlation ID, and the gateway still judges it, so a refusal comes back typed. |
| Google ADK | multi-server name collision | ToolSet.adk() binds each MCP server as one McpToolset, which takes an allow-list of tool names but cannot rename a single tool. When two servers expose a tool under the same name, the bind raises ConfigError before it connects; filter(deny=["<server>__<tool>"]) keeps one of each. The LangGraph, LlamaIndex and Anthropic bindings bind each tool on its own and expose both as <server>__<tool> (#991). |
| Strands Agents | multi-server name collision | For the same reason: ToolSet.strands() binds each server as one MCPClient, filtered by tool_filters. |
| OpenAI Agents SDK | multi-server name collision | For the same reason: ToolSet.openai_agents() binds each server as one MCPServerStreamableHttp, filtered by a static tool_filter. Its model calls record no exemption. |
| Microsoft Agent Framework | multi-server name collision | For the same reason: ToolSet.agent_framework() binds each server as one MCPStreamableHTTPTool, filtered by allowed_tools. |
| CrewAI | multi-server name collision | For the same reason: ToolSet.crewai() binds each server as one crewai_tools.MCPServerAdapter, filtered by its tool_names. |
| Google ADK | MCP tool denial typed | ToolSet.adk() leaves each tool call to McpToolset, which turns the gateway's MCP access denial (JSON-RPC -32008 "Unable to access") into an error result whose text has lost the code, so the SDK cannot raise ToolNotInProfile from it. |
| Strands Agents | MCP tool denial typed | For the same reason: MCPClient returns the denial as a status: error tool result without the code. |
| OpenAI Agents SDK | MCP tool denial typed | ToolSet.openai_agents() leaves each tool call to MCPServerStreamableHttp, which raises the mcp package's McpError with the code intact; tool_access_denial(exc) turns it into ToolNotInProfile. |
| Microsoft Agent Framework | MCP tool denial typed | For the same reason: MCPStreamableHTTPTool raises its ToolExecutionException with the McpError as its cause; tool_access_denial(exc) converts it. |
| CrewAI | MCP tool denial typed | For the same reason: MCPServerAdapter's tools raise the McpError; tool_access_denial(exc) converts it. |
Each transport exemption matches what the adapter reports in donkey.<framework>.capabilities() (a frozen AdapterCapabilities per factory): an adapter is exempt from the transport scenarios exactly when it reports transport="framework", from gateway identity exactly when observes_last_call is false, and from the typed-refusal bridge exactly when typed_refusals is false. A unit test keeps the two in step (#726). The refusal-handler, custom-policy and kill scenarios run against every factory that reports transport="shared", and a factory added without a run fails the suite (#1441). The multi-server and MCP tool denial rows are about binding MCP tools with ToolSet, not the model transport, so capabilities() does not report them.
