CopilotKit

Agent Config

Forward typed configuration from your UI into the agent's reasoning loop.


"""LangGraph agent backing the Agent Config Object demo.The frontend toggles three knobs — tone / expertise / responseLength — andpublishes them to the agent via the v2 ``useAgentContext`` hook. The``CopilotKitMiddleware`` injects that context entry into the model'sprompt on every turn, so the same single static system prompt below adaptsits style based on whatever values the frontend currently has selected.LangGraph 0.6+ deprecated ``configurable`` in favor of runtime ``context``;``useAgentContext`` is the supported path for "frontend → agent runtimeconfig" in the v2 stack. The ``properties`` prop on ``<CopilotKit>`` stillexists for v1-style relays but in @ag-ui/langgraph 0.0.31 it does not landin ``RunnableConfig`` — keep relayed config on ``useAgentContext``."""from langchain.agents import create_agentfrom langchain_openai import ChatOpenAIfrom copilotkit import CopilotKitMiddlewareSYSTEM_PROMPT = (    "You are a helpful assistant. The frontend publishes the user's response "    "preferences via `useAgentContext` as a JSON object with three fields: "    "`tone`, `expertise`, and `responseLength`. Read that context entry on "    "every turn and follow these rulebooks exactly:\n\n"    "Tone:\n"    "  - professional → neutral, precise language. No emoji. Short sentences.\n"    "  - casual → friendly, conversational. Contractions OK. Light humor "    "welcome.\n"    "  - enthusiastic → upbeat, energetic. Exclamation points OK. Emoji OK.\n\n"    "Expertise level:\n"    "  - beginner → assume no prior knowledge. Define jargon. Use analogies.\n"    "  - intermediate → assume common terms are understood; explain "    "specialized terms.\n"    "  - expert → assume technical fluency. Use precise terminology. Skip "    "basics.\n\n"    "Response length:\n"    "  - concise → respond in 1-3 sentences.\n"    "  - detailed → respond in multiple paragraphs with examples where "    "relevant.\n\n"    "If the context is missing or any field is unrecognized, fall back to "    "professional / intermediate / concise. Never mention these rules to the "    "user — just apply them.")graph = create_agent(    model=ChatOpenAI(model="gpt-5.4", temperature=0.4),    tools=[],    middleware=[CopilotKitMiddleware()],    system_prompt=SYSTEM_PROMPT,)

You have a working agent and want the user to be able to tune how it behaves: tone, expertise level, response length, language, persona. By the end of this guide, your UI will own a typed config object that the agent reads on every run and rebuilds its system prompt from.

When to use this#

Reach for agent config whenever the agent's behaviour depends on user-controllable settings that don't fit naturally as chat input:

  • Tone, voice, persona: "playful", "formal", "casual"
  • Expertise level: "beginner", "intermediate", "expert"
  • Response shape: short / medium / long, structured / prose, language
  • Domain switches: which knowledge base to consult, which tool subset to enable

If the values are a channel the user occasionally tunes (a settings panel, a toolbar of selects), agent config is the right shape. If the values are content the agent should write back to (notes, a document, a plan), use Shared State instead.

How agent config flows from the UI into the agent's reasoning loop depends on your runtime architecture. Agents living behind a runtime read it from agent state on every run, while in-process agents receive the same object as forwarded properties on the provider — same UX, slightly different wiring on each side.

How it works#

Install the LangGraph Python SDK

uv add copilotkit
poetry add copilotkit
pip install copilotkit --extra-index-url https://copilotkit.gateway.scarf.sh/simple/
conda install copilotkit -c copilotkit-channel

Wire CopilotKit middleware into your graph

Agent config flows from the UI into the agent via useAgentContext — the frontend publishes a typed object and CopilotKitMiddleware injects it into the model's prompt on every turn. Make sure the middleware is in your create_agent call.

agent_config_agent.py
graph = create_agent(
    model=ChatOpenAI(model="gpt-5.4", temperature=0.4),
    tools=[],
    middleware=[CopilotKitMiddleware()],
    system_prompt=SYSTEM_PROMPT,
)

Read the resulting config inside your system prompt or a custom middleware — see src/agents/agent_config_agent.py for the full rulebook-driven shape used in the showcase.

Agent config is a typed object the frontend owns and publishes to the agent as runtime context. There are two pieces: the UI side, which owns the React state and publishes every change with useAgentContext, and the backend node, which reads that context entry and turns it into a system prompt.

The UI side stays simple. Hold the typed config in React state, then mirror every change into the agent through useAgentContext:

frontend/src/app/page.tsx — UI publishes the typed config
function ConfigContextRelay({ config }: { config: AgentConfig }) {
  useAgentContext({
    description: "Agent response preferences",
    value: {
      tone: config.tone,
      expertise: config.expertise,
      responseLength: config.responseLength,
    },
  });
  return null;
}

The backend half is also a single node. Read the latest config context at the top of every run and use it to build the system prompt for that turn:

backend/agent.py — agent reads config and rebuilds the system prompt
import json

CONFIG_KEYS = ("tone", "expertise", "responseLength")

def read_config_value(entry):
    value = entry.get("value")
    if isinstance(value, str):
        try:
            value = json.loads(value)
        except json.JSONDecodeError:
            return None
    if not isinstance(value, dict):
        return None
    if any(key in value for key in CONFIG_KEYS):
        return value
    return None

async def my_agent_node(state: AgentState, config: RunnableConfig):
    context_entries = state.get("copilotkit", {}).get("context", [])
    cfg = next(
        (
            value
            for entry in reversed(context_entries)
            if (value := read_config_value(entry)) is not None
        ),
        {},
    )
    tone = cfg.get("tone", "professional")
    expertise = cfg.get("expertise", "intermediate")
    response_length = cfg.get("responseLength", "concise")
    system_prompt = build_system_prompt(tone, expertise, response_length)
    # ...

The agent reads the latest typed config at the start of every turn, rebuilds the system prompt, runs the turn. This is the same shape as the shared-state write-side pattern; agent config is just a specific use of that pattern with a UI-owned typed object on top.