Back to Copilotkit

Agent App Context

showcase/shell-docs/src/content/docs/integrations/adk/agent-app-context.mdx

1.70.07.3 KB
Original Source

What is this?

One of the most common use cases for CopilotKit is to register app state and context using useAgentContext. This way, you can notify your agent of what is going on in your app in real time.

Some examples might be: the current user, the current page, the rows currently on screen.

When should I use this?

Use this when the agent's answer has to be about data your application holds rather than data the user retypes into the chat. A queue, a selection, a cart, a filtered table: the page already has it, and useAgentContext is how the agent gets it.

<Callout type="warn" title="ADK needs one line on the agent side"> Unlike the [built-in agent](/built-in-agent/agent-app-context), an ADK agent does **not** receive this context automatically. The AG-UI adapter stores it in ADK session state under `CONTEXT_STATE_KEY` (`"_ag_ui_context"`) and stops there. Nothing puts it in the prompt.
An `LlmAgent` built with a plain string `instruction` therefore never sees it. The agent still
answers, fluently and in the right shape, from nothing — which reads like a working integration
until you check the answer against your own records. Step 2 below is the step that connects them.
</Callout>

Implementation

<Steps> <Step> ### Share data with your agent
    The [`useAgentContext` hook](/reference/v2/hooks/useAgentContext) adds data as context to the Copilot.

    ```tsx title="YourComponent.tsx" showLineNumbers
    "use client" // only necessary if you are using Next.js with the App Router. // [!code highlight]
    import { useAgentContext } from "@copilotkit/react-core/v2"; // [!code highlight]
    import { useState } from 'react';

    export function YourComponent() {
        // Create colleagues state with some sample data
        const [colleagues, setColleagues] = useState([
            { id: 1, name: "John Doe", role: "Developer" },
            { id: 2, name: "Jane Smith", role: "Designer" },
            { id: 3, name: "Bob Wilson", role: "Product Manager" }
        ]);

        // Define agent context
        // [!code highlight:4]
        useAgentContext({
            description: "The current user's colleagues",
            value: colleagues,
        });
        return (
            // Your custom UI component
            <>...</>
        );
    }
    ```
</Step>

<Step>
    ### Read the context in your ADK agent

    Pass an [`InstructionProvider`](https://google.github.io/adk-docs/agents/llm-agents/#instructions)
    instead of a string. A provider receives a `ReadonlyContext`, and the adapter leaves the
    CopilotKit context at `ctx.state[CONTEXT_STATE_KEY]`.

    ```python title="agent.py"
    import json

    from ag_ui_adk import ADKAgent, CONTEXT_STATE_KEY, add_adk_fastapi_endpoint # [!code highlight]
    from google.adk.agents import LlmAgent
    from google.adk.agents.readonly_context import ReadonlyContext

    BASE_INSTRUCTION = """You are a helpful assistant that can help emailing colleagues.

    Answer only about the colleagues the page below sent you. If that list is empty, say the
    page sent no colleagues. If it holds colleagues but none of them match what the user asked
    about, say so and name the ones the page did send. Never invent a colleague."""


    def render_context(state) -> str:
        """Turn the entries the frontend sent into prompt text."""
        entries = state.get(CONTEXT_STATE_KEY) or [] # [!code highlight]
        if not entries:
            return "The page sent no context entries."

        blocks = []
        for entry in entries:
            description = entry.get("description", "(no description)")
            value = entry.get("value", "")
            # `value` arrives already JSON-encoded as a string. Anything else is encoded
            # here so a dict never reaches the prompt as a Python repr.
            if not isinstance(value, str):
                value = json.dumps(value, ensure_ascii=False)
            blocks.append(f"### {description}\n{value}")
        return "\n\n".join(blocks)


    # An InstructionProvider, not a string. // [!code highlight]
    def build_instruction(ctx: ReadonlyContext) -> str:
        return f"{BASE_INSTRUCTION}\n\n## Context from the page\n\n{render_context(ctx.state)}\n"


    colleagues_agent = LlmAgent(
        name="colleagues_agent",
        model="gemini-2.5-flash",
        instruction=build_instruction, # [!code highlight]
    )

    adk_agent = ADKAgent(
        adk_agent=colleagues_agent,
        app_name="demo_app",
        user_id="demo_user",
        use_in_memory_services=True,
    )
    ```

    <Callout>
        `ctx.state` is the form to reach for because it works on every `ag-ui-adk` version. On
        `ag-ui-adk` 0.7.0 with `google-adk` 1.22.0 or later the adapter also copies the same
        entries into `RunConfig.custom_metadata`, so
        `ctx.run_config.custom_metadata["ag_ui_context"]` reads them too. Neither path puts them
        in the prompt on its own — that is still the provider's job.
    </Callout>

    <Callout>
        A provider is also the safer form for the braces. Under a string instruction ADK runs
        `inject_session_state` over the text; its regex matches any `{...}` run, but substitutes
        only when the contents are a valid state name. JSON such as `{"id": 1}` is therefore left
        verbatim, while an identifier-shaped block — a `{status}` inside one of your context
        values — is looked up in session state and raises `KeyError` when it is not there. A
        provider reports `bypass_state_injection=True` from `canonical_instruction` (a string
        reports `False`), so the rendered context is never scanned at all. Checked against
        `google-adk` at the `>=1.28.1` floor `ag-ui-adk` 0.7.0 requires.
    </Callout>
</Step>

<Step>
    ### Or read it inside a tool

    The same entries reach any tool through `tool_context.state`, which is the better fit when
    only one tool needs the data rather than the whole prompt.

    ```python title="tools.py"
    from ag_ui_adk import CONTEXT_STATE_KEY
    from google.adk.tools import ToolContext

    def find_colleague(tool_context: ToolContext, name: str) -> dict:
        """Look up one colleague among the ones the page sent."""
        entries = tool_context.state.get(CONTEXT_STATE_KEY) or [] # [!code highlight]
        # ... read the entry whose `description` matches the one you registered
        return {"found": False}
    ```
</Step>

<Step>
    ### Give it a try!

    Ask your agent a question about the context. It should be able to answer.

    Then check the answer against your own records, because this is the one integration that
    fails quietly. The reliable check is a control: send the same request once with the context
    and once with an empty context. Two answers that describe the same record mean the context
    changed nothing, and the agent is answering from its own invention.
</Step>
</Steps>