Back to Copilotkit

Agent Config

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

1.64.16.0 KB
Original Source
<InlineDemo demo="agent-config" />

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.

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

How it works

<FrameworkSetup concept="agent-config-setup" />

Agent config is a typed object the frontend owns and publishes to the agent as runtime context. The backend reads that context entry and turns it into a system prompt.

<FrontendOnly frontend="react"> Hold the typed config in React state, then mirror every change into the agent through `useAgentContext`:
tsx
function ConfigContextRelay({ config }: { config: AgentConfig }) {
  useAgentContext({
    description: "Agent response preferences",
    value: {
      tone: config.tone,
      expertise: config.expertise,
      responseLength: config.responseLength,
    },
  });
  return null;
}
</FrontendOnly> <FrontendOnly frontend="angular"> Hold the config in an Angular signal and publish a computed context with `connectAgentContext`. The runnable Angular Showcase uses this exact signal-to-context bridge:

<AngularSnippet region="agent-config-context" title="showcase/angular/src/app/features/app-settings/app-settings-feature.component.ts" /> </FrontendOnly>

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:

python
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.

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

How it works

The runtime owns the agent in-process, so config travels through frontend runtime properties rather than agent state. There's no separate backend service to push state into: the typed object becomes the input to the agent factory directly.

<FrontendOnly frontend="react"> Pass the typed object as `properties` on `<CopilotKit>`:
tsx
<CopilotKit
  runtimeUrl="/api/copilotkit"
  properties={{ tone, expertise, responseLength }}
  useSingleEndpoint
>
  <Demo />
</CopilotKit>
</FrontendOnly> <FrontendOnly frontend="angular"> Update runtime properties whenever the Angular signal changes:
ts
import { Component, effect, inject, signal } from "@angular/core";
import { CopilotKit } from "@copilotkit/angular";

@Component({
  selector: "app-agent-config",
  standalone: true,
  template: `
    <button type="button" (click)="tone.set('concise')">Concise</button>
    <button type="button" (click)="tone.set('detailed')">Detailed</button>
  `,
})
export class AgentConfigComponent {
  private readonly copilotKit = inject(CopilotKit);
  protected readonly tone = signal<"concise" | "detailed">("concise");

  constructor() {
    effect(() => {
      this.copilotKit.updateRuntime({
        properties: { tone: this.tone() },
      });
    });
  }
}
</FrontendOnly>

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> <IntegrationGrid path="agent-config" />