Use this skill when a React/Next.js app uses CopilotKit and talks over AG-UI to a Microsoft Agent Framework (MAF) agent hosted in Microsoft Foundry. This covers new builds and changes to existing apps. Foundry hosted agents are a paid Azure service.
Scaffold the agent with azd ai agent init and the frontend with your usual React/Next.js tooling. Then follow this skill for everything that connects them.
React (CopilotKit v2) CopilotKitProvider / CopilotChat threadId=<conv_ id> / useInterrupt
│ HTTP
CopilotKit runtime CopilotRuntime({ agents: { default: new HttpAgent({ url }) } })
│ AG-UI (SSE)
AG-UI gateway (FastAPI) add_agent_framework_fastapi_endpoint(app, AgentFrameworkAgent(FoundryAgent(...),
use_service_session=True, service_session_id_from_thread_id=True)) ← keeps no thread state
│ OpenAI Responses + Entra token (https://ai.azure.com/.default)
Foundry hosted agent ResponsesHostServer(Agent(tools=[...])) ← deployed with azd
A deployed hosted agent does not speak AG-UI. It exposes .../agents/<name>/endpoint/protocols/openai/responses (Responses protocol) or .../protocols/invocations. Translate with MAF's own adapter (FoundryAgent + agent-framework-ag-ui) instead of writing an SSE translator by hand. Browsers cannot hold the Entra token, so the gateway is always server-side.
Before you write any plumbing, check this table. Each row is something older codebases built by hand.
| Need | Native primitive (don't hand-roll) |
|---|---|
| Conversation history across turns and restarts | Foundry conversations: create one per chat, use its conv_… ID as the AG-UI thread ID, and the gateway passes it through (no snapshot store) |
| Durable tool approvals | ResponsesHostServer persists approval requests by default (FoundryFunctionApprovalStore). The AG-UI adapter turns them into AG-UI interrupts and forwards decisions back to the hosted agent |
| Runs that survive browser disconnects | Responses background: true + store: true, then poll or reconnect (see wiring) |
| Durable multi-step workflows with gates | Invocations hosted agent + @multi_turn_task + FoundryStateStore |
| Per-user files for the agent | Session Files API; the agent reads $HOME with its own tools. Never inline large documents into the prompt |
| Long-term user memory | Foundry Memory Store (user scope) or FoundryMemoryProvider |
| Tool catalogs, web search, code interpreter | Foundry Toolbox (FoundryToolbox) with Tool Search, instead of listing every tool up front |
| Live docs while coding | The Microsoft Learn and Foundry MCP servers (bundled with this skill's plugin) |
| If the app needs… | Use | Gateway size |
|---|---|---|
| Hosted agent + chat + tools + approvals (default) | A. AG-UI gateway over a Responses hosted agent | ~50 lines of Python |
| Every state pattern, local prototype, no Foundry compute | B. In-process AG-UI (agent runs inside the FastAPI app) | ~15 lines |
| Background runs that keep going after disconnect, reconnect, per-user conversation lists | C. Background-runs gateway (custom AG-UI agent over Foundry conversations) | ~150 lines of TypeScript |
| Multi-step plan → approve → execute, irreversible steps that must not run twice | D. Invocations hosted agent alongside A or C | REST proxy |
Code for each is in references/wiring.md. In an existing codebase, identify the wiring before you change anything:
FoundryAgent(...) wrapped by add_agent_framework_fastapi_endpoint means A.Agent(client=...) wrapped directly means B.AbstractAgent calling /responses with background means C.protocol: invocations in azure.yaml means D.@copilotkit/*, @ag-ui/*, agent-framework-*, azd and its azure.ai.agents extension). Then ground on live sources:.d.ts files bundled in the installed @copilotkit/* packagesAPIs move between minor versions, so don't trust memorized names.
azd ai agent init --no-prompt ... against the project. Confirm azd env get-values shows FOUNDRY_PROJECT_ENDPOINT, then write tools in main.py (wiring §1).azd ai agent run (port 8088) and smoke-test it with azd ai agent invoke --local "hi".azd deploy (each deploy creates a new agent version), then azd ai agent invoke "hi".curl -N using its conv_… ID as threadId before touching the UI.useInterrupt approval UI and a tool renderer (hitl.md).@tool (Python) or AIFunctionFactory.Create (.NET), using typed and described parameters.approval_mode="always_require" (hitl.md).TOOL_CALL_* events stream, and that any component parsing the arguments still works.| Pattern | Agent side | Frontend hook |
|---|---|---|
| Frontend tool (agent calls the UI) | none; arrives in RunAgentInput.tools | useFrontendTool |
| Render a backend tool call | normal @tool | useRenderTool / useRenderToolCall / useDefaultRenderTool |
| Human approval of a tool call | approval_mode="always_require" | useInterrupt → resolve({approved}) |
| Agent asks the user for input (frontend-resolved tool) | none | useHumanInTheLoop → respond(...) |
| Shared or predictive state | state_schema / predict_state_config on AgentFrameworkAgent | useAgent (read agent.state) |
State patterns emit STATE_SNAPSHOT/STATE_DELTA only when the adapter can see the agent's state:
AgentFrameworkAgent. Test this before you promise the feature.Work from the lowest layer upward:
azd ai agent invoke or curl against /responses.```bash
CONV=$(curl -s -X POST "$FOUNDRYPROJECTENDPOINT/agents/$FOUNDRYAGENTNAME/endpoint/protocols/openai/conversations?api-version=v1" \
-H "Authorization: Bearer $(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)" \
-H 'content-type: application/json' -d '{}' | jq -r .id)
curl -N -X POST
```
The first layer that fails owns the bug. Then match the symptom in troubleshooting.md. Use azd ai agent monitor for hosted-agent logs.
Follow the version rules in troubleshooting.md. Upgrade all packages together, then re-run the completion criteria live.
A change is done only when all of these hold:
RUN_FINISHED, not only while streaming.