| name | ak-dev-new-framework-integration | ||||
|---|---|---|---|---|---|
| description | Step-by-step guide for adding a new agent framework adapter to Agent Kernel. Use this skill when you need to integrate a new agent framework (beyond OpenAI, CrewAI, LangGraph, Google ADK, Smolagents). Covers creating the adapter module, implementing Agent/Runner/Module subclasses, adding optional dependencies, exports, and tests. | ||||
| license | Apache-2.0 | ||||
| metadata |
|
This guide walks through adding support for a new agent framework to Agent Kernel. Use the existing OpenAI adapter (ak-py/src/agentkernel/framework/openai/) as the canonical reference implementation.
- Understand the architecture skill (
.agents/skills/ak-dev-architecture/SKILL.md) - Familiarity with the target framework's API
- The target framework must support async execution (or provide an async wrapper)
ak-py/src/agentkernel/framework/<name>/
├── __init__.py
└── <name>.py
Replace <name> with the framework's lowercase identifier (e.g., openai, langgraph).
If the framework requires per-session state (e.g., conversation history), create a session data class:
class <Name>Session:
"""Stores framework-specific session data."""
def __init__(self):
self._history = [] # or whatever state the framework needs
def get_history(self):
return self._history
def add_to_history(self, item):
self._history.append(item)
def clear_session(self):
self._history.clear()The session data is stored in the Agent Kernel Session via session.set("<name>", <Name>Session()) and retrieved via session.get("<name>").
Subclass Runner from agentkernel.core.base:
from agentkernel.core.base import Runner, Session
from agentkernel.core.model import AgentReply, AgentReplyText, AgentRequest, AgentRequestText
from agentkernel.core.tool import ToolContext
class <Name>Runner(Runner):
FRAMEWORK = "<name>"
def __init__(self):
super().__init__("<Name>Runner")
def _session(self, session: Session) -> <Name>Session:
"""Get or create framework-specific session data."""
data = session.get("<name>")
if data is None:
data = <Name>Session()
session.set("<name>", data)
return data
async def run(self, agent, session: Session, requests: list[AgentRequest]) -> AgentReply:
# 1. Create ToolContext for tool functions to access
tool_context = ToolContext(
runtime=Runtime.current(),
agent=agent,
session=session,
requests=requests
)
with tool_context:
tool_context.set()
try:
# 2. Get framework-specific session state
fw_session = self._session(session)
# 3. Convert AgentRequest models to framework-native format
# e.g., extract text from AgentRequestText
prompt = ""
for req in requests:
if isinstance(req, AgentRequestText):
prompt = req.text
# 4. Call the framework's execution API
result = await self._execute(agent, fw_session, prompt) # framework-specific
# 5. Update session state
fw_session.add_to_history({"input": prompt, "output": result})
# 6. Return as AgentReply
return AgentReplyText(text=str(result), prompt=prompt)
finally:
tool_context.reset()Key requirements:
- Always create a
ToolContextand set it so tool functions can accessToolContext.get() - Always reset
ToolContextin afinallyblock - Handle all
AgentRequestsubtypes (AgentRequestText,AgentRequestImage,AgentRequestFile) - Return an
AgentReply(AgentReplyTextorAgentReplyImage)
Runner declares stream() as @abstractmethod, so every framework adapter must implement it — even if the framework doesn't support token streaming.
If the framework's SDK exposes a token-delta stream (e.g. an async event stream with text-delta events), implement it directly:
from collections.abc import AsyncGenerator
async def stream(self, agent, session: Session, requests: list[AgentRequest]) -> AsyncGenerator[str, None]:
tool_context = ToolContext(Runtime.current(), agent, session, requests)
try:
tool_context.set()
fw_session = self._session(session)
prompt = "".join(req.text for req in requests if isinstance(req, AgentRequestText))
result = await self._execute_streamed(agent, fw_session, prompt) # framework-specific
async for event in result:
delta = self._extract_text_delta(event) # framework-specific
if delta:
yield delta
finally:
tool_context.reset()If the framework has no native token streaming (e.g. CrewAI, smolagents), implement stream() as a generator that always raises, so it satisfies the abstract method contract but fails fast with a clear message:
async def stream(self, agent: Any, session: Session, requests: list[AgentRequest]) -> AsyncGenerator[str, None]:
"""
<Name> does not support SSE streaming.
:raises NotImplementedError: Always raised — use rest_sync mode instead.
"""
raise NotImplementedError("<Name> does not support SSE streaming. Use rest_sync mode.")
yield # make this an async generator to satisfy the type contractRuntime.stream() wraps each yielded token in a StreamChunk, runs it through PostHook.on_stream_chunk(), and forwards it to the caller (REST SSE endpoint or AWS Lambda WebSocket/SQS pipeline). No other core changes are needed to support a new framework's streaming — just implement Runner.stream().
Subclass Agent from agentkernel.core.base:
from agentkernel.core.base import Agent, Runner, Session
class <Name>Agent(Agent):
def __init__(self, name: str, runner: Runner, native_agent):
super().__init__(name, runner)
self._native_agent = native_agent
def get_description(self) -> str:
# Return the agent's description from the native framework object
return self._native_agent.instructions # framework-specific
def get_a2a_card(self):
from agentkernel.core.builder import A2ACardBuilder
return A2ACardBuilder.build(
name=self.name,
description=self.get_description(),
skills=[...] # extract from agent tools
)Key requirements:
get_description()must return a meaningful description from the native framework agentget_a2a_card()must return a valid A2A agent card built viaA2ACardBuilder- Store the native agent in
self._native_agentfor access in the Runner
Subclass ToolBuilder from agentkernel.core.tool:
from agentkernel.core.tool import ToolBuilder
class <Name>ToolBuilder(ToolBuilder):
@classmethod
def bind(cls, funcs: list) -> list:
"""Wrap plain Python functions into framework-native tool objects."""
tools = []
for func in funcs:
# Convert func to framework-specific tool format
tool = framework_specific_tool_wrapper(func)
tools.append(tool)
return toolsSubclass Module from agentkernel.core.module:
from agentkernel.core.module import Module
from agentkernel.core.hooks import PreHook, PostHook
from agentkernel.trace.trace import Trace
class <Name>Module(Module):
def __init__(self, agents: list):
super().__init__()
# Check if tracing is enabled, use traced runner if so
trace_runner = Trace.get().<name>() # returns Runner or None
self.runner = trace_runner if trace_runner else <Name>Runner()
self.load(agents)
def _wrap(self, agent, agents) -> <Name>Agent:
return <Name>Agent(agent.name, self.runner, agent)
def load(self, agents: list) -> "Module":
return super().load(agents)
def pre_hook(self, agent, hooks: list[PreHook]) -> "Module":
wrapped = self.get_agent(agent.name)
if wrapped:
wrapped.pre_hooks.extend(hooks)
return self
def post_hook(self, agent, hooks: list[PostHook]) -> "Module":
wrapped = self.get_agent(agent.name)
if wrapped:
wrapped.post_hooks.extend(hooks)
return selfKey requirements:
- Constructor takes native framework agents, creates a Runner, calls
self.load(agents) _wrap()creates the Agent wrapper — the agentnamemust come from the native agent- Support trace runners via
Trace.get().<name>()
# ak-py/src/agentkernel/framework/<name>/__init__.py
from .<name> import <Name>Module, <Name>ToolBuilderCreate ak-py/src/agentkernel/<name>.py:
from .framework.<name> import <Name>Module, <Name>ToolBuilderThis allows users to import as from agentkernel.<name> import <Name>Module.
Add the framework to ak-py/src/agentkernel/__init__.py if appropriate (following the existing pattern).
In ak-py/pyproject.toml, add an optional dependency group:
[project.optional-dependencies]
<name> = [
"framework-package>=x.y.z",
# Add any instrumentation packages for tracing support
]When adding tracing, create ak-py/src/agentkernel/trace/langfuse/<name>.py:
from .<name>_runner import <Name>Runner as Base<Name>Runner
class LangFuse<Name>Runner(Base<Name>Runner):
def __init__(self, langfuse_client):
super().__init__()
self._client = langfuse_client
async def run(self, agent, session, requests):
with self._client.start_as_current_span(name=agent.name):
return await super().run(agent, session, requests)Also update ak-py/src/agentkernel/trace/base.py and ak-py/src/agentkernel/trace/trace.py to add the new framework method.
Create tests in ak-py/tests/:
# ak-py/tests/test_module_<name>.py
# ak-py/tests/test_tool_<name>.pyFollow the existing test patterns — use DummyRunner, DummyAgent for unit tests, monkeypatch for config overrides, @pytest.mark.asyncio for async tests.
Create at minimum:
examples/cli/<name>/— CLI demo withdemo.py,pyproject.toml,demo_test.pyexamples/api/<name>/— API demo (optional but recommended)
- Add a page under
docs/docs/frameworks/<name>.md - Update
docs/sidebars.jsto include the new framework
-
ak-py/src/agentkernel/framework/<name>/directory with__init__.pyand<name>.py -
<Name>Session(if needed),<Name>Runner,<Name>Agent,<Name>Module,<Name>ToolBuilder -
<Name>Runner.stream()implemented — either real token streaming or aNotImplementedErrorstub - Public alias at
ak-py/src/agentkernel/<name>.py - Optional dependency group in
ak-py/pyproject.toml - Trace runner in
ak-py/src/agentkernel/trace/langfuse/<name>.py(optional) - Updates to
trace/base.pyandtrace/trace.py(if adding tracing) - Unit tests in
ak-py/tests/ - CLI example in
examples/cli/<name>/ - Documentation in
docs/docs/frameworks/<name>.md