Generated banner for Foundry Hosted Agent Copilotkit
Generated logo for Foundry Hosted Agent Copilotkit

Foundry Hosted Agent Copilotkit

GitHubOfficialFirst committed by lordlinusDistributed by GitHub

  • Skill
  • Tool integration
  • Automation and integrations
  • MIT
  • Low risk

From the author

Build and evolve agentic web apps that pair a CopilotKit React frontend with a Microsoft Agent Framework agent running as a Microsoft Foundry hosted agent, connected over the AG-UI protocol. Covers choosing the wiring, using Foundry-native primitives instead of hand-rolled plumbing, agent tools, human-in-the-loop approvals, generative UI and shared state, local run and azd deploy, debugging the event stream, and safe upgrades.
— the description by lordlinus

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a framework for building agentic web applications that pair a CopilotKit React frontend with a Microsoft Agent Framework agent hosted on Microsoft Foundry. It details the architecture for connecting these components over the AG-UI protocol, emphasizing the use of native platform primitives for conversation management, tool approvals, and state persistence. The guide covers the full lifecycle from local development and deployment to debugging event streams and managing dependencies.

What it does

  • Defines the architecture for connecting CopilotKit frontends to Foundry hosted agents
  • Provides patterns for handling conversation history and durable tool approvals
  • Outlines methods for managing background runs and multi-step workflows
  • Explains how to implement generative UI and shared state using CopilotKit hooks
  • Offers playbooks for building new apps and modifying existing agent tools
  • Provides a structured approach for debugging communication between the frontend and the hosted agent

When to use it

  • When building a new React or Next.js application using CopilotKit and Microsoft Foundry
  • When integrating a Microsoft Agent Framework agent into an existing web application
  • When implementing human-in-the-loop approval workflows for agent tools
  • When troubleshooting connectivity or state synchronization issues between a frontend and a hosted agent

Advantages

  • Reduces custom plumbing by utilizing native Foundry and AG-UI primitives
  • Ensures conversation continuity and durable tool execution
  • Supports complex agentic patterns like generative UI and human-in-the-loop approvals
  • Provides clear guidance on deployment and environment management

Usage examples

“How do I connect a CopilotKit frontend to a Microsoft Foundry hosted agent?”
The agent provides a step-by-step guide on selecting the appropriate wiring, setting up the AG-UI gateway, and configuring the CopilotKit runtime to communicate with the hosted agent.
“How can I implement human-in-the-loop approvals for my agent tools?”
The agent explains how to use the approval_mode flag on tools and how to handle the resulting interrupts in the React frontend using the appropriate hooks.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/github/foundry-hosted-agent-copilotkit/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip foundry-hosted-agent-copilotkit.zip -d .agents/skills

Documentation by lordlinus

CopilotKit + AG-UI + Microsoft Foundry hosted agents

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.

Mental model

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.

Use the platform first

Before you write any plumbing, check this table. Each row is something older codebases built by hand.

NeedNative primitive (don't hand-roll)
Conversation history across turns and restartsFoundry 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 approvalsResponsesHostServer 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 disconnectsResponses background: true + store: true, then poll or reconnect (see wiring)
Durable multi-step workflows with gatesInvocations hosted agent + @multi_turn_task + FoundryStateStore
Per-user files for the agentSession Files API; the agent reads $HOME with its own tools. Never inline large documents into the prompt
Long-term user memoryFoundry Memory Store (user scope) or FoundryMemoryProvider
Tool catalogs, web search, code interpreterFoundry Toolbox (FoundryToolbox) with Tool Search, instead of listing every tool up front
Live docs while codingThe Microsoft Learn and Foundry MCP servers (bundled with this skill's plugin)

Choose the wiring

If the app needs…UseGateway 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 computeB. In-process AG-UI (agent runs inside the FastAPI app)~15 lines
Background runs that keep going after disconnect, reconnect, per-user conversation listsC. 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 twiceD. Invocations hosted agent alongside A or CREST 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.

  • A custom AbstractAgent calling /responses with background means C.

  • protocol: invocations in azure.yaml means D.

Workflow

  1. Pick or identify the wiring (table above).

  2. Use the latest release of every package (@copilotkit/*, @ag-ui/*, agent-framework-*, azd and its azure.ai.agents extension). Then ground on live sources:

    • the Microsoft Learn MCP server for MAF and Foundry

    • the Foundry MCP server for project state

    • docs.copilotkit.ai and the .d.ts files bundled in the installed @copilotkit/* packages

    • docs.ag-ui.com

    APIs move between minor versions, so don't trust memorized names.

  3. Build or change it with a playbook below.

  4. Verify adversarially against the completion criteria. A compiling build, a started server, or one good chat reply is not proof.

Playbooks

Build a new app end to end (wiring A)
  1. In a fresh directory, run 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).

  2. Run it locally with azd ai agent run (port 8088) and smoke-test it with azd ai agent invoke --local "hi".

  3. azd deploy (each deploy creates a new agent version), then azd ai agent invoke "hi".

  4. Add the AG-UI gateway exactly as in wiring §2, including the thread-ID-is-conversation-ID flags and the interop shim. Create a conversation, then test chat and an approve resume with curl -N using its conv_… ID as threadId before touching the UI.

  5. Add the conversation route, the CopilotKit runtime route and the React provider (wiring §3).

  6. Add useInterrupt approval UI and a tool renderer (hitl.md).

  7. Verify every completion criterion, first locally and then against the deployed agent.

Add or change an agent tool
  1. Define it with @tool (Python) or AIFunctionFactory.Create (.NET), using typed and described parameters.

  2. Keep parameter descriptions free of concrete example values for data the model must derive. Models copy literal examples.

  3. Return compact values. Rich formatting belongs in the UI renderer.

  4. Side-effecting tools get approval_mode="always_require" (hitl.md).

  5. Redeploy the hosted agent. Tools live in the hosted agent, not the gateway.

  6. Verify through the UI that TOOL_CALL_* events stream, and that any component parsing the arguments still works.

Generative UI and shared state (CopilotKit v2 hooks)
PatternAgent sideFrontend hook
Frontend tool (agent calls the UI)none; arrives in RunAgentInput.toolsuseFrontendTool
Render a backend tool callnormal @tooluseRenderTool / useRenderToolCall / useDefaultRenderTool
Human approval of a tool callapproval_mode="always_require"useInterrupt → resolve({approved})
Agent asks the user for input (frontend-resolved tool)noneuseHumanInTheLoop → respond(...)
Shared or predictive statestate_schema / predict_state_config on AgentFrameworkAgentuseAgent (read agent.state)

State patterns emit STATE_SNAPSHOT/STATE_DELTA only when the adapter can see the agent's state:

  • Wiring B gets them natively.

  • Wiring A needs them declared on the gateway's AgentFrameworkAgent. Test this before you promise the feature.

  • Wiring C has no state events unless you synthesize them.

Debug a broken flow

Work from the lowest layer upward:

  1. Call the hosted agent with azd ai agent invoke or curl against /responses.

  2. Call the gateway with a real conversation ID. A made-up thread ID returns HTTP 500 with this wiring:

    CONV=$(curl -s -X POST "$FOUNDRY_PROJECT_ENDPOINT/agents/$FOUNDRY_AGENT_NAME/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 <gateway>/ -H 'content-type: application/json' -d '{"threadId":"'$CONV'","runId":"r1","messages":[{"id":"m1","role":"user","content":"hi"}],"tools":[],"context":[],"state":{},"forwardedProps":{}}'
  3. Test the CopilotKit route.

  4. Test the browser.

The first layer that fails owns the bug. Then match the symptom in troubleshooting.md. Use azd ai agent monitor for hosted-agent logs.

Upgrade dependencies

Follow the version rules in troubleshooting.md. Upgrade all packages together, then re-run the completion criteria live.

Completion criteria

A change is done only when all of these hold:

  1. The chat path works through the real UI, not only through curl.

  2. Every approval-gated tool was tested both ways:

    • Approve: the tool runs once on the server and the side effect is observable.

    • Reject: the tool does not run, and the agent acknowledges it.

  3. After an approval, a follow-up turn in the same thread does not re-run the gated tool.

  4. Tool and approval cards still render after RUN_FINISHED, not only while streaming.

  5. For deployed changes, checks 1–4 passed against the deployed agent and gateway. A successful deployment proves nothing about behavior.

Files

references/hitl.md

Human-in-the-loop approvals

HITL gates actions with side effects, and its failures are silent: a tool runs without approval, runs twice, or never runs after approval. Every HITL change must be verified three ways:

  • Approve runs the tool once.

  • Reject runs it zero times.

  • Later turns run it zero more times.

Pick the mechanism

SituationMechanismDurable across restarts?
Chat with a gated tool on a hosted agent (wiring A)approval_mode="always_require" on the hosted agent's tool. The AG-UI adapter surfaces it as an AG-UI interrupt and forwards the decision to the hosted agentFoundry side: yes (FoundryFunctionApprovalStore). The gateway's in-flight registry is not: a pending approval interrupted by a gateway restart is rejected without running the tool, and the user asks again
In-process agent (wiring B)Same tool flag; the adapter resolves the approval locallyNo. A restart while an approval is pending leaves it unresolvable
Multi-step plan, irreversible steps, approvals that may wait daysInvocations workflow agent with server-issued gate tokens (wiring §6)Yes (FoundryStateStore + @multi_turn_task)

Server

@tool(approval_mode="always_require")
def transfer(from_account: str, to_account: str, amount: float) -> str: ...
  • On wiring A, the flag lives on the hosted agent's tool. The gateway must run as in wiring §2: use_service_session=True with service_session_id_from_thread_id=True (thread ID = Foundry conv_… ID), plus the interop shim. Without conversation continuity, every approval turns into a fresh approval request.

  • approval_mode controls approval only. never_require does not make a tool read-only.

  • .NET: wrap the tool in ApprovalRequiredAIFunction. Bridge ToolApprovalRequestContent/ToolApprovalResponseContent to a client approval tool call in a DelegatingAIAgent, following the official AGUI Step04_HumanInLoop sample. Keep the call and its result paired in history; removing one makes Azure OpenAI return 400 "tool_calls must be followed by tool messages".

Frontend (CopilotKit v2)

The gateway finishes the run with RUN_FINISHED.outcome = {type: "interrupt", interrupts: [...]}. Each interrupt has:

  • id (the hosted mcpr_... approval id)

  • message (for example, "Approve running transfer?")

  • metadata.agent_framework.function_call (name, arguments)

Handle it with useInterrupt. CopilotKit then resumes the run with a spec resume array.

"use client";
import { useState } from "react";
import { useInterrupt } from "@copilotkit/react-core/v2";

type Call = { name?: string; arguments?: Record<string, unknown> };
type Pending = { id: string; message?: string; metadata?: unknown };
type Resolve = (payload?: unknown, interruptId?: string) => Promise<unknown>;

function ApprovalCard({ interrupt, resolve }: { interrupt: Pending; resolve: Resolve }) {
  const [decision, setDecision] = useState<boolean>();
  const call = (interrupt.metadata as { agent_framework?: { function_call?: Call } })
    ?.agent_framework?.function_call;
  const decide = (approved: boolean) => { setDecision(approved); void resolve({ approved }, interrupt.id); };
  return (
    <div data-testid="approval-card">
      <p>{interrupt.message ?? "Approve this action?"}</p>
      <pre>{call?.name} {JSON.stringify(call?.arguments)}</pre>
      {decision === undefined ? (
        <>
          <button onClick={() => decide(true)}>Approve</button>
          <button onClick={() => decide(false)}>Reject</button>
        </>
      ) : <p>{decision ? "Approved" : "Rejected"}</p>}
    </div>
  );
}

export function ApprovalUI() {
  useInterrupt({
    // One card per open interrupt, each resolved by its own id. CopilotKit resumes the run
    // only after every open interrupt has a decision; resolve() without an id targets the first.
    render: ({ interrupts, resolve }) => (
      <>{interrupts.map((i) => <ApprovalCard key={i.id} interrupt={i as Pending} resolve={resolve} />)}</>
    ),
  });
  return null;
}
  • Mount ApprovalUI inside CopilotKitProvider. The card renders inside CopilotChat by default (renderInChat).

  • Parallel gated calls: with agent-framework-ag-ui 1.4.0, a hosted agent's parallel gated calls surface one interrupt per run. Each decision resumes the run, which then asks for the next. Nothing executes until the last decision; then approved calls run once each and rejected ones return "rejected by user". Rendering all interrupts keeps the UI correct if a run ever carries several.

  • The payload is a contract: {approved: boolean}, with accepted accepted as a legacy alias. Anything else does nothing, with no error. Reject with resolve({approved: false}), not cancel(). Cancelling discards the decision instead of telling the agent the call was rejected.

  • The adapter also emits a confirm_changes tool call for older frontends. With useInterrupt, don't register useHumanInTheLoop("confirm_changes"); hide it in your tool renderer instead (see wiring §3).

  • Render tool results with useDefaultRenderTool or useRenderTool. After approval, the hosted tool's result arrives as TOOL_CALL_RESULT, and nothing shows it otherwise.

Verify approvals by their effect

On wiring A, the approved tool runs on the hosted agent, out of the UI's sight. A correct-looking transcript proves nothing. Check the side effect itself: query the record, or have the tool write an observable marker. Keep this regression test permanently:

  • After one approval, send several unrelated follow-up turns in the same thread.

  • Assert the side effect happened exactly once.

Older versions

Before mid-2026 releases, the AG-UI adapter resolved approvals locally and never forwarded them to a hosted agent (microsoft/agent-framework#6652). Chaining previous_response_id through an approval-resolving response could also re-run the tool on a later turn (#6851). Both were fixed upstream: #7271, #7345, #7480, #7594.

If a codebase still contains a hand-written bridge that converts mcp_approval_request/mcp_approval_response or skips storing response IDs after approval turns, follow these steps:

  1. Upgrade to the latest agent-framework-* packages.

  2. Run the regression test without the workaround.

  3. Delete the workaround only if the test passes.

The two current interop fixes in the wiring §2 shim follow the same rule.

Debugging decision tree

Work top-down:

  1. 400/500 "No tool output found for function call" on approve: the hosted agent uses a Chat Completions client. Switch to FoundryChatClient.

  2. No approval card, browser shows INCOMPLETE_STREAM / "Cannot send 'TOOL_CALL_END' event: No active tool call found": the gateway is missing the shim from wiring §2.

  3. No approval card, no error: useInterrupt isn't mounted inside the provider, or the tool lacks approval_mode and ran immediately. Check RUN_FINISHED.outcome with curl, and the hosted agent logs with azd ai agent monitor.

  4. Approve produces another approval request instead of running the tool: the gateway isn't using use_service_session=True.

  5. Click does nothing, no error: payload mismatch (see the contract above). A gateway reply of APPROVAL_RESUME_REQUIRED means the client sent a tool message without a resume entry.

  6. Approve runs the tool, but every later turn returns an empty run (RUN_STARTED → RUN_FINISHED only): the duplicate interrupt tool message reached the adapter. Apply the shim from wiring §2.

  7. Approve shows a reply, but the side effect never happened: the decision didn't reach the hosted agent. Upgrade packages, then curl the gateway with the resume to see whether a TOOL_CALL_RESULT comes back.

  8. Works once, then a later turn repeats the side effect: an outdated workaround or an old package (see "Older versions").

  9. Card visible during the run but gone after RUN_FINISHED: the final MESSAGES_SNAPSHOT differs from the live events. Check the DOM after the run, and upgrade CopilotKit.

  10. RUN_ERROR "No pending approval interrupt found for resume interruptId 'mcpr_…'": the gateway restarted, or the resume reached a different replica than the one that issued the approval. The tool did not run and the conversation is still usable, so ask again. With more than one replica, use session affinity. For approvals that must survive restarts, use an Invocations workflow.

Requirements and compatibility

Written for: GitHub Copilot

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server

Security

What our scanner noticed· Low risk

  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

First committed by
GitHub avatar of lordlinuslordlinus
Publisher
GitHub (Official)
Distributed by
GitHub
Source
github/awesome-copilot · skills/foundry-hosted-agent-copilotkit · commit 797337f
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Contributors
lordlinus

AutoGineer is not affiliated with or endorsed by lordlinus or GitHub.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download foundry-hosted-agent-copilotkit.zip