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://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/wiring.md

Wiring reference

Package names below are current as of late 2026. Always install the latest releases and verify names against the installed packages: Python via python -c "import agent_framework_ag_ui as m; print(m.__all__)", TypeScript via the .d.ts files.

1. Hosted agent

Scaffold the agent non-interactively against an existing Foundry project, in a fresh directory with a new environment:

azd ai agent init --no-prompt -e <env> -p <project-arm-id> -d <model-deployment> \
  --agent-name my-agent --protocol responses --deploy-mode code \
  --runtime python_3_13 --entry-point main.py --src src/my-agent
azd env get-values -e <env>   # FOUNDRY_PROJECT_ENDPOINT, AZURE_AI_MODEL_DEPLOYMENT_NAME and
                              # AZURE_LOCATION (= the project's region) must all be set
  • Running init in a directory that already has azure.yaml or another azd env can leave FOUNDRY_PROJECT_ENDPOINT empty, so azd deploy fails with Foundry dependencies are not ready: ... FOUNDRY_PROJECT_ENDPOINT is not set. Start over in a clean directory rather than patching the env.

  • An existing project needs no azd provision and no --infra. init then azd deploy is the whole flow.

Everything is declared in azure.yaml (camelCase). The old separate agent.yaml/agent.manifest.yaml files are obsolete. The agent service looks like this:

services:
  my-agent:
    project: src/my-agent
    host: azure.ai.agent
    language: python
    uses: [ai-project]
    env:
      AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
    codeConfiguration: { runtime: python_3_13, entryPoint: main.py }
    protocols:
      - protocol: responses
        version: 2.0.0

main.py must use a Responses-based chat client. FoundryChatClient is required for approval resume; a Chat Completions client fails with No tool output found for function call.

import asyncio, os
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential

@tool
def get_balance(account: str) -> str:
    """Read-only lookup of an account balance."""
    ...

@tool(approval_mode="always_require")
def transfer(from_account: str, to_account: str, amount: float) -> str:
    """Move money. Side-effecting, so a human must approve it first."""
    ...

async def main() -> None:
    client = FoundryChatClient(
        project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],  # injected when hosted
        model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
        credential=DefaultAzureCredential(),
    )
    agent = Agent(client=client, instructions="...", tools=[get_balance, transfer],
                  default_options={"store": False})  # the hosting layer owns history
    await ResponsesHostServer(agent).run_async()

if __name__ == "__main__":
    asyncio.run(main())

Dependencies (requirements.txt): agent-framework-foundry, agent-framework-foundry-hosting, and azure-identity. Pin each one to the latest release with ==. The hosting package exists only as pre-releases, and remote builds resolve dependencies on their own. Depend on these specific packages, not on the agent-framework meta-package.

ResponsesHostServer persists conversation state and pending approvals in Foundry by default (FoundryAgentSessionStore, FoundryFunctionApprovalStore). Don't build your own stores for these.

Fail soft at startup. If main() raises before serving, /readiness never answers and Foundry reports only HTTP 424 session_not_ready. Catch configuration errors and serve a stub agent that explains what is missing.

2. AG-UI gateway (wiring A)

The gateway is a small FastAPI app. MAF does the Responses↔AG-UI translation and the approval forwarding.

The AG-UI thread ID is a Foundry conversation ID. The UI creates the conversation first (see §3), then the gateway passes the thread ID straight through as the Foundry conversation (use_service_session=True, service_session_id_from_thread_id=True). Foundry owns the history, so the gateway keeps no thread state and needs no snapshot store.

Conversation continuity is required for hosted approvals. Without it, every run reaches the hosted agent as a new conversation, the stored approval request can't be matched, and approving just produces a second approval request.

# gateway/app.py — run: uvicorn app:app --port 8000
import os
from ag_ui.core import EventType
from agent_framework.foundry import FoundryAgent
from agent_framework_ag_ui import AgentFrameworkAgent, add_agent_framework_fastapi_endpoint
from azure.identity.aio import DefaultAzureCredential
from fastapi import FastAPI


class HostedApprovalSafeAgent(AgentFrameworkAgent):
    """Interop fixes verified with agent-framework-ag-ui 1.4.0 + CopilotKit 1.76 / @ag-ui/client 1.0.1.

    1. CopilotKit sends an interrupt decision twice: as `resume` AND as a tool message keyed by
       the interrupt id. The duplicate makes the adapter skip the model's reply and leaves the
       Foundry conversation returning empty runs. Keep only `resume`.
    2. The adapter emits TOOL_CALL_END for the hosted approval id (`mcpr_...`) without a
       TOOL_CALL_START; @ag-ui/client aborts the run with INCOMPLETE_STREAM. Drop it.
    Delete this class when a release fixes both and the HITL regression test passes without it.
    """

    async def run(self, input_data, **kwargs):
        # The endpoint passes snake_case keys (interrupt_id, tool_call_id) by the time run() is called.
        resumed = {r.get("interrupt_id") or r.get("interruptId") or r.get("id")
                   for r in (input_data.get("resume") or []) if isinstance(r, dict)}
        if resumed:
            input_data = {**input_data, "messages": [
                m for m in input_data.get("messages", [])
                if not (m.get("role") == "tool" and (m.get("tool_call_id") or m.get("toolCallId")) in resumed)]}
        started: set[str] = set()
        async for event in super().run(input_data, **kwargs):
            if event.type == EventType.TOOL_CALL_START:
                started.add(event.tool_call_id)
            elif event.type == EventType.TOOL_CALL_END and event.tool_call_id not in started:
                continue
            yield event


hosted = FoundryAgent(
    project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
    agent_name=os.environ["FOUNDRY_AGENT_NAME"],   # same name as in azure.yaml
    credential=DefaultAzureCredential(),
    allow_preview=True,                             # hosted-agent session APIs are preview
)
app = FastAPI()
add_agent_framework_fastapi_endpoint(
    app,
    HostedApprovalSafeAgent(agent=hosted, use_service_session=True,
                            service_session_id_from_thread_id=True),  # threadId == Foundry conv_ id
    "/",
)

Dependencies: agent-framework-ag-ui, agent-framework-foundry, azure-identity, aiohttp (needed by the async credential), uvicorn.

  • Thread IDs must be real conv_… IDs. An arbitrary UUID makes Foundry return HTTP 500. Without service_session_id_from_thread_id=True, use_service_session=True instead requires a snapshot_store + snapshot_scope_resolver that maps thread IDs to conversations, which puts state back in the gateway.

  • What the gateway still holds: in-flight approvals only. agent-framework-ag-ui keeps pending approvals in a process-local InMemoryAGUIApprovalStateStore, which can't be replaced. If the gateway restarts while an approval is pending:

    • The resume fails with RUN_ERROR "No pending approval interrupt found for resume interruptId 'mcpr_…'".

    • The tool does not run.

    • The same conversation keeps working, so the user simply asks again.

    Everything else survives restarts. For more than one replica, use session affinity so a resume reaches the replica that issued the approval.

  • Authorization: the gateway trusts the thread ID it receives. In a multi-user app, record which user created each conversation (in the §3 route) and reject other users' thread IDs. This mapping is written once per conversation, not on every turn.

  • Resume shape, for curl tests: "resume": [{"interruptId": "<mcpr_ id from RUN_FINISHED.outcome.interrupts>", "status": "resolved", "payload": {"approved": true}}]. Send it with the previous MESSAGES_SNAPSHOT as messages.

3. CopilotKit runtime and React

The runtime runs server-side. In Next.js, use an App Router route at app/api/copilotkit/[[...slug]]/route.ts. The v2 runtime is multi-route (/info, /agent/:id/run, …), so the catch-all segment is required.

import { HttpAgent } from "@ag-ui/client";
import { CopilotRuntime, createCopilotRuntimeHandler } from "@copilotkit/runtime/v2";

const runtime = new CopilotRuntime({
  agents: { default: new HttpAgent({ url: process.env.AGUI_URL! }) }, // gateway URL
});
const handler = createCopilotRuntimeHandler({ runtime, basePath: "/api/copilotkit" });
export const GET = handler;
export const POST = handler;

For Hono or Express servers, use createCopilotHonoHandler / createCopilotExpressHandler from @copilotkit/runtime/v2.

Create the Foundry conversation server-side. Its ID becomes the AG-UI thread ID. Put this in app/api/thread/route.ts (needs @azure/identity, plus FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_AGENT_NAME in the Next.js server env):

import { DefaultAzureCredential } from "@azure/identity";

const credential = new DefaultAzureCredential();

export async function POST() {
  const { token } = await credential.getToken("https://ai.azure.com/.default");
  const url = `${process.env.FOUNDRY_PROJECT_ENDPOINT}/agents/${process.env.FOUNDRY_AGENT_NAME}` +
    `/endpoint/protocols/openai/conversations?api-version=v1`;
  const res = await fetch(url, {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: "{}",
  });
  if (!res.ok) return Response.json({ error: `Foundry ${res.status}` }, { status: 502 });
  const { id } = (await res.json()) as { id: string };   // conv_...
  return Response.json({ threadId: id });                 // multi-user: record (user, id) here
}

The React side, in app/page.tsx:

"use client";
import { useEffect, useState } from "react";
import { CopilotKitProvider, CopilotChat, useDefaultRenderTool } from "@copilotkit/react-core/v2";
import "@copilotkit/react-core/v2/styles.css";
import { ApprovalUI } from "./approval";   // useInterrupt, see hitl.md

function ToolRows() {
  // Hosted tools run remotely; without a renderer their results are invisible in the chat.
  useDefaultRenderTool({
    render: ({ name, result }) => name === "confirm_changes" ? null :
      <div>{name}: {typeof result === "string" ? result : JSON.stringify(result ?? "")}</div>,
  });
  return null;
}

export default function Page() {
  const [threadId, setThreadId] = useState<string>();
  const [error, setError] = useState<string>();
  useEffect(() => {
    fetch("/api/thread", { method: "POST" })
      .then(async (r) => {
        const d = await r.json().catch(() => ({}));
        if (!r.ok || typeof d.threadId !== "string" || !d.threadId.startsWith("conv_")) {
          throw new Error(d.error ?? `Could not start a conversation (HTTP ${r.status})`);
        }
        setThreadId(d.threadId);
      })
      .catch((e: unknown) => setError(e instanceof Error ? e.message : String(e)));
  }, []);
  if (error) return <p role="alert">{error}</p>;
  if (!threadId) return <p>Starting conversation…</p>;
  return (
    <CopilotKitProvider runtimeUrl="/api/copilotkit">
      <ToolRows />
      <ApprovalUI />
      <CopilotChat agentId="default" threadId={threadId} />
    </CopilotKitProvider>
  );
}

Passing a stored conv_… ID back as threadId continues that conversation on the agent side, because Foundry holds the history. Re-displaying the earlier transcript in the chat UI is a separate concern, and this setup doesn't cover it.

Version rules:

  • Install next, @copilotkit/react-core, and @copilotkit/runtime at @latest.

  • @copilotkit/runtime pins an exact @ag-ui/client version. Read it from the installed runtime with npm view @copilotkit/runtime@<installed version> dependencies, then install that exact version: npm i @ag-ui/client@<pinned version>.

  • Keep all @copilotkit/* packages on one version.

Names must agree in three places: the runtime agents key, agentId on CopilotChat/useAgent, and the hosted agent name the gateway is given. Use one constant.

4. In-process AG-UI (wiring B)

Same as §2, but wrap a local Agent(client=FoundryChatClient(...), tools=[...]) in a plain AgentFrameworkAgent instead of FoundryAgent; use_service_session and the §2 shim aren't needed. All state patterns work natively. Approval state is process-local, so a restart while an approval is pending leaves that approval unresolvable. Use this wiring for prototypes, or put durable approvals behind a hosted agent instead.

5. Background-runs gateway (wiring C) {#background-runs-gateway}

Use this when runs must continue after the browser disconnects and users must be able to reopen conversations. It is a custom AbstractAgent (from @ag-ui/client) in a Node gateway that drives Foundry's REST API directly. Derive the URLs from the agent's Responses endpoint E = {project}/agents/{name}/endpoint/protocols/openai/responses:

  • openai = E minus /responses

  • agentEndpoint = openai minus /protocols/openai

StepCall
New threadPOST {openai}/conversations → conversation.id; POST {agentEndpoint}/sessions → agent_session_id; store {threadId → conversationId, sessionId, owner}
Start a turnPOST {openai}/responses {input, conversation, agent_session_id, background: true, store: true, stream: false} → save response.id
Stream to the UIPoll GET {openai}/responses/{id} about once a second while status ∈ {queued, in_progress}. Emit MESSAGES_SNAPSHOT built from GET {openai}/conversations/{id}/items (paginate with after until has_more is false) merged with response.output. Finish with RUN_FINISHED, or RUN_ERROR carrying response.error
ReconnectSame poll loop starting from the saved response.id. Disconnecting must not cancel the run
CancelPOST {openai}/responses/{id}/cancel

Every call uses ?api-version=v1 and a bearer token for https://ai.azure.com/.default.

  • Register a custom AgentRunner with CopilotSseRuntime({ agents, runner }) so Foundry is the only replay store. The default InMemoryAgentRunner keeps a process-global thread cache that disagrees with Foundry after a restart.

  • Derive each turn's input from the latest user message only. Replaying the whole AG-UI transcript into /responses fails with 400 errors about orphaned tool calls.

  • Return 409 if a thread already has an active response.

6. Invocations workflow agent (wiring D)

Use this for plan → approve → execute flows where an irreversible step must not run twice, even across container restarts:

  • Declare protocol: invocations (version 2.0.0) in azure.yaml.

  • In the agent, use @multi_turn_task from azure.ai.agentserver.core.tasks, with application checkpoints in FoundryStateStore (azure.ai.agentserver.core.storage).

  • Each POST to /invocations?agent_session_id=<id> returns 202 with an invocation_id. Poll GET /invocations/{id} for status and output.

  • Gate each irreversible step with a server-issued token (invocation ID + step index + a random value, compared with hmac.compare_digest).

  • A watermark recorded before the side effect gives at-most-once: a crash between the watermark and the action skips the action. For exactly-once effects, send a stable per-step idempotency key that the destination enforces, or commit the effect and the completion checkpoint in one transaction.

  • On recovery (ctx.entry_mode == "recovered"), the same turn is re-invoked with the same input. Reconcile any step that was started but not checkpointed using that idempotency key (ask the destination whether it happened) instead of treating the watermark as proof of completion.

The browser reaches this agent through a server-side REST proxy that checks session ownership. It does not go through AG-UI.

7. Auth and identity

  • Token audience is https://ai.azure.com/.default. The cognitiveservices scope returns 401 "audience is incorrect".

  • Never send x-ms-user-isolation-key to a deployed agent. Isolation comes from the Entra identity, and the header causes a 400.

  • The gateway's identity, and the Next.js server identity that creates conversations, need the Azure AI User role on the Foundry project. That's a managed identity in Azure, or your az login locally.

  • The gateway is the authorization boundary:

    • Validate the user's Entra token (issuer, audience, tenant, scope).

    • Keep a user → thread/session ownership index.

    • Check ownership before every Foundry call.

  • The Foundry Memory Store calls models as the project's managed identity. Grant that identity the Foundry User role on the parent account, or memory writes fail with 401.

  • Each hosted agent gets its own Entra identity. Model and session-storage access are granted in-project, but any other Azure resource needs an explicit RBAC assignment.

8. Local loop and deploy

azd ai agent run                       # the real agent on http://localhost:8088, using your az login
azd ai agent invoke --local "hi"       # smoke-test the local agent
azd deploy                             # code deploy; each deploy creates a new agent version
azd ai agent invoke "hi"               # call the deployed agent
azd ai agent show / monitor            # status and logs
  • The §2 gateway targets a deployed agent by name, so deploy before you test the gateway. Iterate on agent logic locally with azd ai agent run.

  • Update azd and the azure.ai.agents extension first (azd update, azd extension upgrade --all). An "Incompatible" extension status blocks every azd ai agent command.

  • If the agent's traffic routing has been pinned explicitly (FixedRatio on a version), azd deploy does not move traffic to the new version. Check with azd ai agent show.

  • Remote builds resolve dependencies independently. A local success does not prove the deployed image builds, so pin versions.

  • Restart the local runtime between verification passes if tools keep in-memory state.

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