Back to Copilotkit

Agent Config

showcase/shell-docs/src/content/docs/agent-config.mdx

1.57.04.3 KB
Original Source

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.

<InlineDemo demo="agent-config" />

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.

<WhenFrameworkHas flag="agent_config_pattern" equals="shared-state">

How it works

Agent config is a typed object the frontend owns and keeps in sync with the agent. There are two pieces: the UI side, which owns the React state and pushes every change into agent state, and the backend node, which reads those fields out of state and turns them into a system prompt.

The UI side stays simple. Hold the typed config in React state, then mirror every change into the agent through agent.setState({...}):

tsx
function ConfigStateSync({ config }: { config: AgentConfig }) {
  const { agent } = useAgent({ agentId: "agent-config" });
  useEffect(() => {
    agent.setState({ ...config });
  }, [agent, config]);
  return null;
}

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

python
async def my_agent_node(state: AgentState, config: RunnableConfig):
    cfg = state.get("config", {})
    tone = cfg.get("tone", "casual")
    expertise = cfg.get("expertise", "intermediate")
    response_length = cfg.get("response_length", "medium")
    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.

</WhenFrameworkHas> <WhenFrameworkHas flag="agent_config_pattern" equals="runtime-properties">

How it works

The runtime owns the agent in-process, so config travels through the provider rather than agent state. There's no separate backend service to push state into, and no extra plumbing — the typed object you set on <CopilotKitProvider> becomes the input to the agent factory directly.

The UI side passes the typed object as properties on the provider:

tsx
<CopilotKitProvider
  runtimeUrl="/api/copilotkit"
  properties={{ tone, expertise, responseLength }}
  useSingleEndpoint
>
  <Demo />
</CopilotKitProvider>

The runtime hands the same object to the agent factory on every call as input.forwardedProps. The factory uses those fields to build a system prompt before returning the agent for that turn:

ts
export const agentConfigFactory = async (input: AgentFactoryInput) => {
  const { tone, expertise, responseLength } = input.forwardedProps ?? {};
  const systemPrompt = buildSystemPrompt(tone, expertise, responseLength);
  return makeAgent({ systemPrompt /* ... */ });
};
</WhenFrameworkHas> <FeatureIntegrations feature="agent-config" /> <IntegrationGrid path="agent-config" />