Skip to content

Wrap a framework for chat

The Agent Adapter chat page talks to one function: agent(message, session), returning text or an async iterator of text chunks (Chat with your agent). Each wrapper below turns a framework's own entry point into that function, in a few lines.

flowchart LR
  P["chat page"] --> F["agent(message, session)"]
  F --> LG["LangGraph app.ainvoke"]
  F --> ADK["ADK Runner.run_async"]
  F --> CR["CrewAI crew.kickoff_async"]
  F --> H["your HTTP backend"]

Tool calls show as cards when they run through session.executor (with ctx=session.context()); a framework that runs its tools itself shows only its answer. A framework's own "approved" never counts: only the page's one-use decision does.

LangGraph

A compiled graph with a messages state. Earlier turns go in as (role, text) pairs.

def langgraph_agent(app: Any) -> ChatAgent:
    """A compiled LangGraph app (messages state) as a chat agent."""

    async def reply(message: ChatMessage, session: ChatSession) -> str:
        history = [(m.role, m.content) for m in session.history]
        out = await app.ainvoke({"messages": [*history, ("user", message.text)]})
        return str(out["messages"][-1].content)

    return reply

ADK

Each chat is one ADK session, made on first use; ADK keeps the history itself. An edit or Replay branch has a new session.id, so ADK starts a fresh session there without the earlier turns (seed it from session.history if the agent needs them).

def adk_agent(runner: Any, user_id: str = "local") -> ChatAgent:
    """An ADK `Runner` as a chat agent; each chat is one ADK session."""

    async def reply(message: ChatMessage, session: ChatSession) -> AsyncIterator[str]:
        from google.genai import types  # installed with google-adk

        sessions, app = runner.session_service, runner.app_name
        if await sessions.get_session(app_name=app, user_id=user_id, session_id=session.id) is None:
            await sessions.create_session(app_name=app, user_id=user_id, session_id=session.id)
        content = types.Content(role="user", parts=[types.Part(text=message.text)])
        async for event in runner.run_async(
            user_id=user_id, session_id=session.id, new_message=content
        ):
            if event.is_final_response() and event.content and event.content.parts:
                yield "".join(part.text or "" for part in event.content.parts)

    return reply

CrewAI

The crew's task reads the user's text as {question}.

def crewai_agent(crew: Any) -> ChatAgent:
    """A CrewAI `Crew` as a chat agent; its task reads `{question}`."""

    async def reply(message: ChatMessage, session: ChatSession) -> str:
        result = await crew.kickoff_async(inputs={"question": message.text})
        return str(result.raw)

    return reply

Your HTTP backend

A FastAPI (or any) service answering POST {message, session_id} with {answer}. The function is sync, so it runs in a worker thread.

def http_agent(url: str, timeout: float = 60.0) -> ChatAgent:
    """Your own backend (FastAPI or any) that answers POST {message, session_id} with {answer}."""

    def reply(message: ChatMessage, session: ChatSession) -> str:  # sync: runs in a thread
        body = json.dumps({"message": message.text, "session_id": session.id}).encode("utf-8")
        headers = {"Content-Type": "application/json"}
        request = urllib.request.Request(url, data=body, headers=headers)
        with urllib.request.urlopen(request, timeout=timeout) as response:
            return str(json.loads(response.read())["answer"])

    return reply

Run one

Pass the wrapped agent to serve, for example serve(agent=langgraph_agent(app)), or put it in a module and start agent-adapter ui --agent app:chat. The wrappers are tested with stand-ins for each framework; LangGraph also runs for real when it is installed.