From 581d8099e4e8a13a0f5c7fc88cc9df9419652749 Mon Sep 17 00:00:00 2001 From: Sunil Sattiraju Date: Wed, 7 Oct 2026 14:00:22 +0800 Subject: [PATCH] Simplify foundry-hosted-agent-copilotkit skill and add plugin with Learn + Foundry MCP (#4349) * Simplify foundry-hosted-agent-copilotkit skill and add plugin Rewrite the skill around Foundry-native primitives and the current package lines (CopilotKit v2, @ag-ui 1.x, agent-framework-ag-ui 1.4, azd azure.yaml-only hosted agents), verified end to end against a deployed Foundry hosted agent with Playwright. - Consolidate 6 references into wiring.md, hitl.md, troubleshooting.md - Default wiring: FoundryAgent + AG-UI adapter gateway that keeps no thread state (Foundry conversation id is the AG-UI thread id) - HITL via CopilotKit v2 useInterrupt; document two interop shims - Drop obsolete workarounds for closed agent-framework issues - Add foundry-hosted-agent-copilotkit plugin bundling the skill with Microsoft Learn and Foundry MCP servers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review: per-interrupt approvals, at-most-once wording, pinned ag-ui install - hitl.md: render one card per open interrupt and resolve each by id; document how parallel gated calls surface with agent-framework-ag-ui 1.4.0 - wiring.md/SKILL.md: a pre-effect watermark is at-most-once; exactly-once needs a destination-enforced idempotency key or a single transaction - wiring.md: install @ag-ui/client at the version the installed runtime pins Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Show conversation startup errors instead of an endless loading state The wiring.md page example now checks the /api/thread response status, validates the conv_ id, and renders caught network/JSON/HTTP errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Aaron Powell --- .github/plugin/marketplace.json | 6 + docs/README.plugins.md | 1 + docs/README.skills.md | 2 +- .../foundry-hosted-agent-copilotkit/README.md | 24 ++ .../foundry-hosted-agent-copilotkit/mcp.json | 13 + .../plugin.json | 27 ++ .../foundry-hosted-agent-copilotkit/SKILL.md | 170 ++++++---- .../references/architecture.md | 86 ----- .../references/deploy-loop.md | 41 --- .../references/hitl.md | 199 +++++------- .../references/patterns.md | 31 -- .../references/troubleshooting.md | 89 ++--- .../references/upgrading.md | 40 --- .../references/wiring.md | 304 ++++++++++++++++++ 14 files changed, 616 insertions(+), 417 deletions(-) create mode 100644 plugins/foundry-hosted-agent-copilotkit/README.md create mode 100644 plugins/foundry-hosted-agent-copilotkit/mcp.json create mode 100644 plugins/foundry-hosted-agent-copilotkit/plugin.json delete mode 100644 skills/foundry-hosted-agent-copilotkit/references/architecture.md delete mode 100644 skills/foundry-hosted-agent-copilotkit/references/deploy-loop.md delete mode 100644 skills/foundry-hosted-agent-copilotkit/references/patterns.md delete mode 100644 skills/foundry-hosted-agent-copilotkit/references/upgrading.md create mode 100644 skills/foundry-hosted-agent-copilotkit/references/wiring.md diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 1c9af037a..d14fbed1f 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -1137,6 +1137,12 @@ "sha": "5837a9343db97d59107d146024b1dbd858625274" } }, + { + "name": "foundry-hosted-agent-copilotkit", + "source": "plugins/foundry-hosted-agent-copilotkit", + "description": "Build CopilotKit + AG-UI web apps on Microsoft Foundry hosted agents (Microsoft Agent Framework): wiring, native Foundry primitives, human-in-the-loop approvals, azd deploy, and debugging — with the Microsoft Learn and Foundry MCP servers for live grounding.", + "version": "1.0.0" + }, { "name": "frontend-web-dev", "source": "plugins/frontend-web-dev", diff --git a/docs/README.plugins.md b/docs/README.plugins.md index a7597c1de..31bdbb741 100644 --- a/docs/README.plugins.md +++ b/docs/README.plugins.md @@ -66,6 +66,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-plugins) for guidelines on how t | [flight-map-canvas](../plugins/flight-map-canvas/README.md) | A GitHub Copilot canvas that generates a view where Google Maps can be explored using 3D controls, as if a flight simulator. Agents can send the flight anywhere and report what they are working on. | 1 items | copilot-canvas, flight-simulator, geography, google-maps, interactive-canvas, session-breaks, threejs | | [flowstudio-power-automate](../plugins/flowstudio-power-automate/README.md) | Give your AI agent full visibility into Power Automate cloud flows via the FlowStudio MCP server. Connect, debug, build, monitor health, and govern flows at scale — action-level inputs and outputs, not just status codes. | 5 items | power-automate, power-platform, flowstudio, mcp, model-context-protocol, cloud-flows, workflow-automation, monitoring, governance | | [fluentui-blazor](../plugins/fluentui-blazor/README.md) | Build Blazor apps with the Microsoft Fluent UI Blazor library. Bundles the fluentui-blazor skill for correct usage patterns (providers, service registration, icons, dialogs, data grid, theming) with the official Fluent UI Blazor MCP server for live lookup of components, parameters, enums, icons, and documentation, plus v4-to-v5 migration guidance. | 1 items | fluentui, blazor, dotnet, components, aspnetcore, mcp, model-context-protocol, migration, icons, theming | +| [foundry-hosted-agent-copilotkit](../plugins/foundry-hosted-agent-copilotkit/README.md) | Build CopilotKit + AG-UI web apps on Microsoft Foundry hosted agents (Microsoft Agent Framework): wiring, native Foundry primitives, human-in-the-loop approvals, azd deploy, and debugging — with the Microsoft Learn and Foundry MCP servers for live grounding. | 1 items | copilotkit, ag-ui, microsoft-foundry, hosted-agents, agent-framework, human-in-the-loop, azure | | [frontend-web-dev](../plugins/frontend-web-dev/README.md) | Essential prompts, instructions, and chat modes for modern frontend web development including React, Angular, Vue, TypeScript, and CSS frameworks. | 4 items | frontend, web, react, typescript, javascript, css, html, angular, vue | | [gem-team](../plugins/gem-team/README.md) | Self-Learning Multi-agent orchestration framework for spec-driven development and automated verification. With smarter tool calling and leaner context. | 14 items | multi-agent, orchestration, tdd, testing, e2e, devops, security-audit, code-review, prd, mobile | | [gesture-review](../plugins/gesture-review/README.md) | Review pull requests with a live camera feed and approve or reject using thumbs-up/thumbs-down gestures. | 1 items | camera-input, gesture-control, github-prs, hands-free, mediapipe, pull-request-review | diff --git a/docs/README.skills.md b/docs/README.skills.md index cbb19e310..18f811107 100644 --- a/docs/README.skills.md +++ b/docs/README.skills.md @@ -198,7 +198,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to | [fluentui-blazor](../skills/fluentui-blazor/SKILL.md)
`gh skills install github/awesome-copilot fluentui-blazor` | Guide for using the Microsoft Fluent UI Blazor component library (Microsoft.FluentUI.AspNetCore.Components NuGet package) in Blazor applications. Use this when the user is building a Blazor app with Fluent UI components, setting up the library, using FluentUI components like FluentButton, FluentDataGrid, FluentDialog, FluentToast, FluentNavMenu, FluentTextField, FluentSelect, FluentAutocomplete, FluentDesignTheme, or any component prefixed with "Fluent". Also use when troubleshooting missing providers, JS interop issues, or theming. | `references/DATAGRID.md`
`references/LAYOUT-AND-NAVIGATION.md`
`references/SETUP.md`
`references/THEMING.md` | | [folder-structure-blueprint-generator](../skills/folder-structure-blueprint-generator/SKILL.md)
`gh skills install github/awesome-copilot folder-structure-blueprint-generator` | Comprehensive technology-agnostic prompt for analyzing and documenting project folder structures. Auto-detects project types (.NET, Java, React, Angular, Python, Node.js, Flutter), generates detailed blueprints with visualization options, naming conventions, file placement patterns, and extension templates for maintaining consistent code organization across diverse technology stacks. | None | | [foundry-agent-sync](../skills/foundry-agent-sync/SKILL.md)
`gh skills install github/awesome-copilot foundry-agent-sync` | Create and synchronize prompt-based AI agents directly within Azure AI Foundry via REST API, from a local JSON manifest. Unlike scaffolding skills that only generate local code, this skill registers agents in the Foundry service itself — making them immediately available for invocation. Use when the user asks to create agents in Foundry, sync, deploy, register, or push agents to Foundry, update agent instructions, or scaffold the manifest and sync script for a new repository. Triggers: 'create agent in foundry', 'sync foundry agents', 'deploy agents to foundry', 'register agents in foundry', 'push agents', 'create foundry agent manifest', 'scaffold agent sync'. | None | -| [foundry-hosted-agent-copilotkit](../skills/foundry-hosted-agent-copilotkit/SKILL.md)
`gh skills install github/awesome-copilot foundry-hosted-agent-copilotkit` | Ongoing development guidance for agentic web apps that pair a CopilotKit frontend with Microsoft Agent Framework agents on Azure AI Foundry hosted agents over the AG-UI protocol - add and gate agent tools, wire human-in-the-loop approvals, build generative UI and shared state, debug the event stream, upgrade pre-1.0 packages safely, and deploy hosted agent updates. | `references/architecture.md`
`references/deploy-loop.md`
`references/hitl.md`
`references/patterns.md`
`references/troubleshooting.md`
`references/upgrading.md` | +| [foundry-hosted-agent-copilotkit](../skills/foundry-hosted-agent-copilotkit/SKILL.md)
`gh skills install github/awesome-copilot foundry-hosted-agent-copilotkit` | 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. | `references/hitl.md`
`references/troubleshooting.md`
`references/wiring.md` | | [freecad-scripts](../skills/freecad-scripts/SKILL.md)
`gh skills install github/awesome-copilot freecad-scripts` | Expert skill for writing FreeCAD Python scripts, macros, and automation. Use when asked to create FreeCAD models, parametric objects, Part/Mesh/Sketcher scripts, workbench tools, GUI dialogs with PySide, Coin3D scenegraph manipulation, or any FreeCAD Python API task. Covers FreeCAD scripting basics, geometry creation, FeaturePython objects, interface tools, and macro development. | `references/geometry-and-shapes.md`
`references/gui-and-interface.md`
`references/parametric-objects.md`
`references/scripting-fundamentals.md`
`references/workbenches-and-advanced.md` | | [from-the-other-side-anitta](../skills/from-the-other-side-anitta/SKILL.md)
`gh skills install github/awesome-copilot from-the-other-side-anitta` | Rigorous challenge profile for Anitta: assumption checks, evidence calibration, and defensible reasoning patterns for Ember collaboration. | None | | [from-the-other-side-quinn](../skills/from-the-other-side-quinn/SKILL.md)
`gh skills install github/awesome-copilot from-the-other-side-quinn` | Collaboration profile for Quinn: curious, energetic, and implementation-focused partnership patterns for Ember sessions with Alison. | None | diff --git a/plugins/foundry-hosted-agent-copilotkit/README.md b/plugins/foundry-hosted-agent-copilotkit/README.md new file mode 100644 index 000000000..0cb064ee1 --- /dev/null +++ b/plugins/foundry-hosted-agent-copilotkit/README.md @@ -0,0 +1,24 @@ +# Foundry Hosted Agent + CopilotKit Plugin + +Build and evolve agentic web apps where a CopilotKit React frontend talks over the AG-UI protocol to a Microsoft Agent Framework agent running as a Microsoft Foundry hosted agent. + +## What It Includes + +### Skills + +| Skill | Description | +| --- | --- | +| `/foundry-hosted-agent-copilotkit` | Choose the wiring, use Foundry-native primitives (conversations, durable approvals, background runs, Toolbox, memory, session files), add tools and human-in-the-loop approvals, deploy with `azd`, and debug the AG-UI event stream. | + +### MCP Servers + +| Server | Description | +| --- | --- | +| `microsoft-learn` | Microsoft Learn MCP server (`https://learn.microsoft.com/api/mcp`) for current Agent Framework and Foundry documentation and code samples. | +| `foundry` | Foundry MCP server (`https://mcp.ai.azure.com`) for inspecting Foundry projects, model deployments, and agents. Signs in with your Microsoft Entra ID account. | + +## Requirements + +- An Azure subscription with a Microsoft Foundry project (hosted agents are a paid preview service). +- Azure Developer CLI (`azd`) with the `azure.ai.agents` extension, and Azure CLI. +- Node.js and Python for the frontend, CopilotKit runtime, and AG-UI gateway. diff --git a/plugins/foundry-hosted-agent-copilotkit/mcp.json b/plugins/foundry-hosted-agent-copilotkit/mcp.json new file mode 100644 index 000000000..0309249a4 --- /dev/null +++ b/plugins/foundry-hosted-agent-copilotkit/mcp.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "microsoft-learn": { + "type": "streamable-http", + "url": "https://learn.microsoft.com/api/mcp" + }, + "foundry": { + "type": "streamable-http", + "url": "https://mcp.ai.azure.com" + } + } +} diff --git a/plugins/foundry-hosted-agent-copilotkit/plugin.json b/plugins/foundry-hosted-agent-copilotkit/plugin.json new file mode 100644 index 000000000..1c1e85635 --- /dev/null +++ b/plugins/foundry-hosted-agent-copilotkit/plugin.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "foundry-hosted-agent-copilotkit", + "description": "Build CopilotKit + AG-UI web apps on Microsoft Foundry hosted agents (Microsoft Agent Framework): wiring, native Foundry primitives, human-in-the-loop approvals, azd deploy, and debugging — with the Microsoft Learn and Foundry MCP servers for live grounding.", + "version": "1.0.0", + "keywords": [ + "copilotkit", + "ag-ui", + "microsoft-foundry", + "hosted-agents", + "agent-framework", + "human-in-the-loop", + "azure" + ], + "author": { + "name": "Sunil Sattiraju" + }, + "repository": "https://github.com/github/awesome-copilot", + "license": "MIT", + "extensions": { + "com.github.awesome-copilot": { + "skills": [ + "./skills/foundry-hosted-agent-copilotkit/" + ] + } + } +} diff --git a/skills/foundry-hosted-agent-copilotkit/SKILL.md b/skills/foundry-hosted-agent-copilotkit/SKILL.md index 211c8d445..0d9af016f 100644 --- a/skills/foundry-hosted-agent-copilotkit/SKILL.md +++ b/skills/foundry-hosted-agent-copilotkit/SKILL.md @@ -1,100 +1,136 @@ --- name: foundry-hosted-agent-copilotkit -description: 'Ongoing development guidance for agentic web apps that pair a CopilotKit frontend with Microsoft Agent Framework agents on Azure AI Foundry hosted agents over the AG-UI protocol - add and gate agent tools, wire human-in-the-loop approvals, build generative UI and shared state, debug the event stream, upgrade pre-1.0 packages safely, and deploy hosted agent updates.' +description: '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.' --- -# Developing with CopilotKit + AG-UI + Azure AI Foundry Hosted Agents +# CopilotKit + AG-UI + Microsoft Foundry hosted agents -Use this skill for development work inside an EXISTING application built on this stack: a React/Next.js frontend using CopilotKit, connected over the AG-UI protocol to a Microsoft Agent Framework (MAF) agent (Python or .NET) that runs as — or is being developed against — an Azure AI Foundry hosted agent (paid Azure service; usage may incur costs). +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. -Do NOT use this skill to scaffold a new project. Dedicated scaffolders exist (the CopilotKit CLI, `azd ai agent init`); use those, then return here for everything that follows: adding tools, gating them behind approvals, generative UI, shared state, debugging, dependency upgrades, and deploying agent updates. +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 ```text -CopilotKit hooks (React) useFrontendTool / useHumanInTheLoop / - │ useRenderToolCall / useCoAgent - ▼ -CopilotKit Runtime (route handler) agents: { : new HttpAgent({ url }) } - │ AG-UI events over SSE - ▼ -AG-UI endpoint ← WHERE this lives defines your architecture - │ - ▼ -MAF Agent (tools, approval modes) → model deployment +React (CopilotKit v2) CopilotKitProvider / CopilotChat threadId= / 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 ``` -The single most important fact: **a deployed Foundry hosted agent endpoint does not speak AG-UI by default.** It exposes an OpenAI Responses endpoint (`.../protocols/openai/responses`) and/or a raw `.../protocols/invocations` endpoint. AG-UI must be produced somewhere, and where it is produced determines how every feature (especially human-in-the-loop) behaves. The three wirings are described in [references/architecture.md](references/architecture.md). +**A deployed hosted agent does not speak AG-UI.** It exposes `.../agents//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. + +| 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](references/wiring.md#background-runs-gateway)) | +| 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) | + +## Choose the wiring + +| 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](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 -Follow these steps for every task on this stack: +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 -1. **Identify the wiring first.** Inspect the codebase before changing anything: - - `add_agent_framework_fastapi_endpoint(...)` (Python) or `MapAGUI(...)` (.NET) wrapping an in-process agent → Architecture A (in-process AG-UI endpoint). - - A hosted agent whose own container serves AG-UI, declared with `protocol: invocations` in `agent.yaml` → Architecture B. - - A separate service translating between the AG-UI endpoint and a hosted agent's `/responses` endpoint (look for `previous_response_id`, `mcp_approval_response`, or a Foundry `conversation` object in the code) → Architecture C (translation bridge). - - Confirm the frontend agent name: the key in the runtime `agents` config, the `agent` prop on the `` provider, and the hosted agent name in `agent.yaml` must all agree. -2. **Ground in live documentation.** Every layer here is pre-1.0 or preview and moves between minor versions. Never trust memorized APIs: - - MAF and Foundry hosted agents: use the Microsoft Docs MCP tools when available, otherwise learn.microsoft.com (`/agent-framework/integrations/ag-ui/`, `/azure/foundry/`). - - CopilotKit: docs.copilotkit.ai (Microsoft Agent Framework section). Verify hook and runtime API names against the TypeScript declarations bundled in the installed `@copilotkit/*` packages — names have churned (`useCopilotAction` is legacy; current names include `useFrontendTool`, `useHumanInTheLoop`, `useRenderToolCall`, `useCoAgent`). - - AG-UI protocol: docs.ag-ui.com (event reference, dojo patterns). -3. **Execute the task** using the matching reference below. -4. **Verify adversarially.** A compiling build, a started dev server, or one successful chat reply is NOT proof. Apply the completion criteria at the end of this skill. + 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. -## References +## Playbooks -Load on demand; each is self-contained: +### Build a new app end to end (wiring A) -| Reference | Load when | -| --- | --- | -| [references/architecture.md](references/architecture.md) | Choosing or understanding the wiring; local-vs-deployed modes; why a translation bridge exists and what it must handle | -| [references/patterns.md](references/patterns.md) | Implementing any of the 7 AG-UI interaction patterns (frontend tools, backend tool rendering, HITL, generative UI, shared state, predictive state) | -| [references/hitl.md](references/hitl.md) | Adding or debugging human-in-the-loop approvals, including the known duplicate-execution hazard | -| [references/troubleshooting.md](references/troubleshooting.md) | Any failure: symptom → root cause → fix tables for every layer | -| [references/upgrading.md](references/upgrading.md) | Bumping any dependency; version compatibility rules; tracked upstream issues | -| [references/deploy-loop.md](references/deploy-loop.md) | Running the agent locally with `azd ai agent run`, deploying updates, deployment gotchas | +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](references/wiring.md#1-hosted-agent)). +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](references/wiring.md#2-ag-ui-gateway-wiring-a), 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](references/wiring.md#3-copilotkit-runtime-and-react)). +6. Add `useInterrupt` approval UI and a tool renderer ([hitl.md](references/hitl.md#frontend-copilotkit-v2)). +7. Verify every completion criterion, first locally and then against the deployed agent. -## Task playbooks +### Add or change an agent tool -### Add or modify 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](references/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. -1. Define the tool on the agent (`@tool` in Python; `AIFunctionFactory.Create` in .NET) with typed, described parameters. -2. Keep docstrings grounding-safe: do not put concrete example values in parameter descriptions for fields the model must derive from real data — models copy literal examples. Use placeholders and validate inside the tool. -3. Return compact, model-consumable values; rich formatting belongs in the UI render, not the tool result. -4. Decide the approval mode now: side-effecting tools get `approval_mode="always_require"` (see [references/hitl.md](references/hitl.md)); read-only tools stay unrestricted. -5. If the tool call should render in the UI, add a `useRenderToolCall`/render entry for it ([references/patterns.md](references/patterns.md)). -6. Verify live: trigger the tool through the chat UI, confirm the call and result stream as `TOOL_CALL_*` events, and confirm renamed or re-typed parameters did not break any frontend component that parses the arguments. +### Generative UI and shared state (CopilotKit v2 hooks) -### Wire human-in-the-loop onto an existing tool +| 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`) | -Follow [references/hitl.md](references/hitl.md) end to end. Summary: mark the tool (`approval_mode="always_require"` / `ApprovalRequiredAIFunction`), enable confirmation on the AG-UI wrapper, register the approval UI hook on the frontend, and make the response payload shape match what the server detection expects. Then test approve AND reject AND a follow-up turn after approval (see the duplicate-execution hazard). - -### Build generative UI or shared state - -Follow the pattern table in [references/patterns.md](references/patterns.md). Know the honesty caveat: state synchronization patterns are native when the AG-UI adapter wraps an in-process agent (Architecture A/B); through a Responses-protocol bridge (Architecture C) they require explicit synthesis work — check what the codebase actually implements before promising the feature. +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 -1. Reproduce at the lowest layer first: `curl -N` the AG-UI endpoint with a minimal `RunAgentInput` JSON body and read the raw SSE events. If the bug reproduces there, the frontend is innocent. -2. For hosted agents, go one layer lower: call the agent's `/responses` endpoint directly. This is how the known re-execution bug was isolated to the framework rather than the UI stack. -3. Match the symptom against [references/troubleshooting.md](references/troubleshooting.md) — exact error strings are listed. -4. Restart a locally running hosted agent (`azd ai agent run`) between verification passes if the agent holds in-memory state; stale state makes tests pass or fail for the wrong reason. +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: + + ```bash + 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 / -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](references/troubleshooting.md). Use `azd ai agent monitor` for hosted-agent logs. ### Upgrade dependencies -Follow [references/upgrading.md](references/upgrading.md). Never bump a single package in isolation: the version relationship rules there (runtime ↔ AG-UI client, agent-framework line consistency, hosting protocol ↔ manifest version) must hold simultaneously, and any local workaround must be re-validated against its tracked upstream issue before removal. - -### Deploy an agent update - -Follow [references/deploy-loop.md](references/deploy-loop.md): iterate locally against the real agent with `azd ai agent run`, then `azd deploy` (each deploy creates a new agent version), then verify the deployed agent — including the approval pause — before declaring success. +Follow the version rules in [troubleshooting.md](references/troubleshooting.md#upgrades). Upgrade all packages together, then re-run the completion criteria live. ## Completion criteria -A change on this stack is done only when ALL of these hold: - -1. The read/query path works through the real UI (not only via curl). -2. Every approval-gated tool was tested both ways: approve → the tool executes server-side and state visibly changes; reject → the tool does not run and the agent acknowledges. -3. At least one follow-up turn was sent in the same thread after an approval, and the gated tool did NOT silently execute again ([references/hitl.md](references/hitl.md), duplicate-execution hazard). -4. Tool calls render correctly at stream end, not just during streaming (message snapshots can differ from live events). -5. For deployed changes: the checks above were run against the deployed endpoint, not only locally — deployment success is not proof of behavior. +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. diff --git a/skills/foundry-hosted-agent-copilotkit/references/architecture.md b/skills/foundry-hosted-agent-copilotkit/references/architecture.md deleted file mode 100644 index dd811ccce..000000000 --- a/skills/foundry-hosted-agent-copilotkit/references/architecture.md +++ /dev/null @@ -1,86 +0,0 @@ -# Architecture: where AG-UI is produced - -The stack has three layers that must agree: CopilotKit (React hooks + runtime), the AG-UI protocol (SSE event stream), and the Microsoft Agent Framework (MAF) agent, optionally running as an Azure AI Foundry hosted agent. The deployed Foundry hosted agent endpoint speaks OpenAI Responses (`{project_endpoint}/agents/{name}/endpoint/protocols/openai/responses`) or a raw invocations protocol (`.../protocols/invocations`) — **not AG-UI**. Pointing `HttpAgent` from `@ag-ui/client` directly at a hosted agent's Responses endpoint does not work. - -There are three viable wirings. Identify which one the codebase uses before changing anything. - -## Architecture A — in-process AG-UI endpoint (agent runs inside your service) - -The agent object lives in the same process as the AG-UI HTTP endpoint. - -Python: - -```python -from agent_framework import Agent -from agent_framework_ag_ui import AgentFrameworkAgent, add_agent_framework_fastapi_endpoint -from fastapi import FastAPI - -agent = Agent(name="assistant", instructions="...", client=chat_client, tools=[...]) -wrapped = AgentFrameworkAgent(agent=agent, require_confirmation=True) # HITL on -app = FastAPI() -add_agent_framework_fastapi_endpoint(app, wrapped, "/") -``` - -.NET: `builder.Services.AddAGUI()` + `app.MapAGUI("/", agent)` from `Microsoft.Agents.AI.Hosting.AGUI.AspNetCore`, with approval middleware (see hitl.md). - -- All 7 AG-UI patterns (including state snapshots/deltas) work natively — the adapter sees the agent's internal events. -- The model client can still be a Foundry model deployment; "in-process" refers to where the *agent loop* runs, not the model. -- This is what the CopilotKit CLI and MAF samples produce, and the right choice when you don't need platform-managed conversations, per-user isolation, or Foundry-managed compute. - -## Architecture B — hosted agent serves AG-UI itself (invocations protocol) - -The hosted agent's own container speaks AG-UI, deployed under Foundry's `invocations` protocol ("Custom streaming protocol (AG-UI, etc.) → Invocations" per the Foundry hosted-agents docs). The `agent.yaml` declares: - -```yaml -protocols: - - protocol: invocations - version: 2.0.0 -``` - -and the container serves AG-UI requests at `/invocations` (Microsoft's `foundry-samples` repository has a bring-your-own invocations AG-UI sample under `samples/python/hosted-agents/bring-your-own/invocations/ag-ui/`). The CopilotKit runtime's `HttpAgent` points at the deployed invocations endpoint. - -- AG-UI features behave like Architecture A because the adapter still wraps the in-process agent — it just runs inside Foundry-managed compute. -- You give up the Responses protocol's platform-managed conversation history; conversation state is yours to manage. -- Calls to the deployed endpoint need Entra auth (`DefaultAzureCredential`), so the CopilotKit runtime usually still needs a thin server-side proxy to attach tokens — browsers cannot call it directly. - -## Architecture C — translation bridge to a Responses-protocol hosted agent - -The hosted agent is deployed with the `responses` protocol (platform-managed conversation history, agent versioning, per-user isolation), and a separate bridge service translates between AG-UI and the Responses stream. This is the highest-effort wiring; choose it only when you specifically need the Responses platform features. - -The bridge must handle, at minimum: - -1. **Stream translation**: OpenAI Responses SSE events → AG-UI events (`response.output_text.delta` → `TEXT_MESSAGE_CONTENT`, function call items → `TOOL_CALL_*`, `response.completed` → `RUN_FINISHED`, etc.). -2. **Turn derivation, not history replay**: derive each turn's input from the latest user message (or an approval decision). Replaying the full raw AG-UI message history to the Responses endpoint fails with 400 errors about orphaned tool calls. -3. **HITL forwarding**: surface the hosted agent's `mcp_approval_request` to the UI, and forward the user's decision back as an `mcp_approval_response` input item — approved tools then re-execute *server-side*. The stock AG-UI adapter resolves approvals locally and never forwards them to a remote agent (tracked as microsoft/agent-framework#6652), so a bridge needs explicit code for this path. Verify against the current package version whether this is still required before writing custom routing. -4. **Conversation continuity**: either `previous_response_id` chaining (local/direct mode) or a Foundry `conversation` object (deployed/platform mode). See hitl.md for the critical hazard with `previous_response_id` chaining across approval turns. -5. **State synthesis caveat**: `STATE_SNAPSHOT`/`STATE_DELTA` events are NOT produced by a Responses stream. Shared-state and predictive-state patterns require the bridge to synthesize them (e.g. from `response.function_call_arguments.delta`); if the bridge doesn't implement that, those patterns silently don't work. Check before promising the feature. - -Bridge state (response-id or conversation cache) is typically in-memory: run a single replica or externalize the cache before scaling out. - -## Local development modes - -- **Architecture A/B**: run the FastAPI/ASP.NET service directly; point the CopilotKit runtime's `HttpAgent` at `http://localhost:/`. -- **Hosted agents (B/C)**: `azd ai agent run` runs the REAL hosted agent locally (default port 8088) using your `az login` credentials and the provisioned Foundry project — there is no mock. `azd ai agent invoke --local "..."` sends a single test payload. A bridge in local mode points at the bare local endpoint instead of the deployed one (commonly switched by a single environment variable holding the direct URL). - -## CopilotKit runtime wiring (all architectures) - -The AG-UI endpoint, wherever it lives, registers in the CopilotKit runtime as an `HttpAgent`: - -```ts -import { HttpAgent } from "@ag-ui/client"; -import { CopilotRuntime } from "@copilotkit/runtime"; - -const runtime = new CopilotRuntime({ - agents: { "my_agent": new HttpAgent({ url: process.env.AGUI_BACKEND_URL! }) }, -}); -``` - -and the provider selects it by name: ``. Name drift between the `agents` key, the `agent` prop, and (for hosted agents) the name in `agent.yaml` is a recurring failure — keep one constant. - -Frontend tools registered with `useFrontendTool` flow through the runtime into the AG-UI `RunAgentInput.tools` array and become callable by the agent; this is native in all three architectures. - -## Auth to Foundry endpoints - -- Token audience is `https://ai.azure.com/.default` — the default `cognitiveservices.azure.com` scope yields 401 "audience is incorrect". -- Keyless (Entra / `DefaultAzureCredential`) is the norm; the async Python credential path needs `aiohttp` installed. -- Never send `x-ms-user-isolation-key` to a deployed agent — deployed agents derive isolation from the Entra identity and reject the header with a 400. diff --git a/skills/foundry-hosted-agent-copilotkit/references/deploy-loop.md b/skills/foundry-hosted-agent-copilotkit/references/deploy-loop.md deleted file mode 100644 index 5ad5b3acb..000000000 --- a/skills/foundry-hosted-agent-copilotkit/references/deploy-loop.md +++ /dev/null @@ -1,41 +0,0 @@ -# The hosted-agent development and deploy loop - -Foundry hosted agents are defined by `agent.yaml` (identity: `kind: hosted`, model, protocols, environment variables, resources — schema at microsoft.github.io/AgentSchema/), optionally an `agent.manifest.yaml` (parameterized template), and `azure.yaml` (azd provisioning + service config). `${VAR}` in these files is an azd environment variable; `{{ param }}` is an init-time template parameter. - -## Inner loop: develop against the REAL agent locally - -There is no mock. The `azure.ai.agents` azd extension runs the actual hosted agent locally: - -1. `az login` into the tenant that owns the Foundry project (see the 403 trap below). -2. `azd ai agent run` — starts the agent locally (default port 8088) against the provisioned project. -3. `azd ai agent invoke --local ""` — one-shot test (`-p responses|invocations` selects protocol, `-f payload.json` for structured input). -4. Point the rest of the stack (AG-UI endpoint or bridge) at the local agent URL via whatever environment variable the codebase uses for direct/local mode, and exercise features through the real UI. - -Inner-loop discipline: - -- **Restart the agent between independent verification passes** if it seeds in-memory data — every approve/reject mutates shared state, so a second test run against a dirty process passes or fails for the wrong reason. -- A stale agent process holding the port produces a confusing hypercorn "Address already in use" traceback on the next `azd ai agent run` — kill it first. -- A freshly started agent can 404 with `DeploymentNotFound` on the first request even though the model deployment exists (warm-up flake) — retry once before investigating. - -## Outer loop: deploy an update - -1. `azd deploy` (or `azd up` for provision + deploy). Code-deploy (ZIP) vs container-deploy is selected by fields in `agent.yaml`; container builds default to remote ACR builds — no local Docker needed. -2. **Each deploy creates a new agent version.** `azd ai agent show` confirms what's live. Clients pinned to a specific version won't see the update; clients using "latest" will. -3. Verify the deployed agent behaviorally: send a read query AND confirm a consequential action still pauses for approval. Deployment success output proves packaging worked, nothing more. - -## Deploy gotchas - -| Gotcha | Detail | -| --- | --- | -| `azd provision` alone deploys a placeholder | Provision creates infrastructure only; without a deploy step you get a hello-world agent. Use `azd up` or follow provision with `azd deploy` | -| Base images must come from MCR | `az acr build` pulling from Docker Hub anonymously hits `toomanyrequests` rate limits. Use `mcr.microsoft.com/...` base images | -| Shared code missing from the image | If agent code imports modules outside the agent directory, the docker build context in `azure.yaml` must reach them — and azd versions have differed in whether they accept a parent-directory `project:`/context path (a 1.27.0-era regression rejected `..`). Test packaging after azd upgrades | -| Hosted container env | `FOUNDRY_PROJECT_ENDPOINT` and `APPLICATIONINSIGHTS_CONNECTION_STRING` are injected automatically in hosted containers; don't hardcode them | -| Compute idles out | Hosted agent compute deprovisions after ~15 minutes idle; first request after idle is slow — not a bug | -| History duplication (Responses protocol) | The platform stores conversation history; if the agent's own chat client also stores (`store=True`), turns duplicate. Set the client/host options to not store | -| Bridge/front-door scaling | Any service holding per-thread response-id or conversation caches in memory must run a single replica or externalize the cache | -| Tenant-mismatch 403 | `Microsoft.MachineLearningServices/workspaces/agents/action` denied despite correct RBAC usually means the az CLI's active subscription/tenant isn't the project's. Fix with `az account set` / `az login --tenant`; no code change | - -## Production frontend wiring - -Deploy the AG-UI endpoint (Architecture A/C service, or rely on the hosted invocations endpoint for B) where the CopilotKit runtime can reach it server-side; set the runtime's agent URL env var accordingly. Keep auth keyless (Entra) with the `https://ai.azure.com/.default` audience. The browser talks only to the CopilotKit runtime route — never expose Foundry endpoints or credentials to the client. diff --git a/skills/foundry-hosted-agent-copilotkit/references/hitl.md b/skills/foundry-hosted-agent-copilotkit/references/hitl.md index 704881796..9aaa4ace1 100644 --- a/skills/foundry-hosted-agent-copilotkit/references/hitl.md +++ b/skills/foundry-hosted-agent-copilotkit/references/hitl.md @@ -1,132 +1,111 @@ # Human-in-the-loop approvals -HITL is the highest-risk feature on this stack: it gates consequential, side-effecting actions, and the failure modes are silent (a tool running without approval, or running twice). Treat every HITL change as safety-critical and verify all three outcomes: approve executes once, reject executes zero times, follow-up turns execute zero additional times. +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. -## Wiring (Python, in-process AG-UI endpoint) +## Pick the mechanism + +| Situation | Mechanism | Durable 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 agent | Foundry 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 locally | **No.** A restart while an approval is pending leaves it unresolvable | +| Multi-step plan, irreversible steps, approvals that may wait days | Invocations workflow agent with server-issued gate tokens ([wiring §6](wiring.md#6-invocations-workflow-agent-wiring-d)) | Yes (`FoundryStateStore` + `@multi_turn_task`) | + +## Server ```python -from agent_framework import Agent, tool -from agent_framework_ag_ui import AgentFrameworkAgent, add_agent_framework_fastapi_endpoint - @tool(approval_mode="always_require") -def transfer_money(from_account: str, to_account: str, amount: float) -> str: - """Transfer money between accounts.""" - ... - -agent = Agent(name="assistant", instructions="...", client=chat_client, - tools=[transfer_money, check_balance]) -wrapped = AgentFrameworkAgent(agent=agent, require_confirmation=True) -add_agent_framework_fastapi_endpoint(app, wrapped, "/") +def transfer(from_account: str, to_account: str, amount: float) -> str: ... ``` -Both halves are required: `approval_mode="always_require"` on the tool AND `require_confirmation=True` on the wrapper. Note `approval_mode` controls *approval* only — `never_require` does not mean the tool is read-only; that's the implementer's responsibility. +- On wiring A, the flag lives on the **hosted agent's** tool. The gateway must run as in [wiring §2](wiring.md#2-ag-ui-gateway-wiring-a): `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". -**`.NET`** uses a different mechanism than Python's wrapper flag: wrap the tool in `ApprovalRequiredAIFunction`, then wrap the *agent* in a `DelegatingAIAgent` that bridges approval content to/from the `request_approval` client tool call (the pattern in the official [`Step04_HumanInLoop`](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/02-agents/AGUI/Step04_HumanInLoop) sample): +## Frontend (CopilotKit v2) -```csharp -// Tool requires approval; wrap the agent so the UI can render an approval card. -AITool[] tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(ApproveExpenseReport))]; -var baseAgent = openAIChatClient.AsAIAgent(name: "assistant", instructions: "...", tools: tools); -var agent = new ServerFunctionApprovalAgent(baseAgent, jsonOptions.SerializerOptions); -app.MapAGUI("/", agent); +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`) -internal sealed class ServerFunctionApprovalAgent(AIAgent inner, JsonSerializerOptions json) - : DelegatingAIAgent(inner) -{ - protected override async IAsyncEnumerable RunCoreStreamingAsync(...) - { - // INBOUND: convert the client's `request_approval` call + result back into a matched - // ToolApprovalRequestContent / ToolApprovalResponseContent pair before the inner agent - // runs. Leaving a raw request_approval tool_call without its paired result in history - // makes Azure OpenAI 400: "tool_calls must be followed by tool messages...". - var processed = ProcessIncomingFunctionApprovals(messages.ToList(), json); - await foreach (var update in InnerAgent.RunStreamingAsync(processed, ...)) - // OUTBOUND: convert ToolApprovalRequestContent -> a `request_approval` client tool - // call so the frontend renders the approval card. - yield return ProcessOutgoingApprovalRequests(update, json); - } +Handle it with `useInterrupt`. CopilotKit then resumes the run with a spec `resume` array. + +```tsx +"use client"; +import { useState } from "react"; +import { useInterrupt } from "@copilotkit/react-core/v2"; + +type Call = { name?: string; arguments?: Record }; +type Pending = { id: string; message?: string; metadata?: unknown }; +type Resolve = (payload?: unknown, interruptId?: string) => Promise; + +function ApprovalCard({ interrupt, resolve }: { interrupt: Pending; resolve: Resolve }) { + const [decision, setDecision] = useState(); + 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 ( +
+

{interrupt.message ?? "Approve this action?"}

+
{call?.name} {JSON.stringify(call?.arguments)}
+ {decision === undefined ? ( + <> + + + + ) :

{decision ? "Approved" : "Rejected"}

} +
+ ); +} + +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) => )} + ), + }); + return null; } ``` -The content type is `ToolApprovalRequestContent`/`ToolApprovalResponseContent` (not `FunctionApprovalRequestContent`), and the fix is to *convert* the approval call/result — keeping them paired — not to delete them from message history, or Azure OpenAI fails with "tool_calls must be followed by tool messages responding to each 'tool_call_id'". +- 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](wiring.md#3-copilotkit-runtime-and-react)). +- Render tool results with `useDefaultRenderTool` or `useRenderTool`. After approval, the hosted tool's result arrives as `TOOL_CALL_RESULT`, and nothing shows it otherwise. -## Frontend +## Verify approvals by their effect -```tsx -useHumanInTheLoop({ - name: "confirm_changes", // must match what the server surfaces - render: ({ args, respond, status }) => ( - respond?.({ accepted: true })} - onReject={() => respond?.({ accepted: false })} - /> - ), -}); -``` +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. -**The payload shape is a contract, not a framework feature.** CopilotKit's `respond(...)` accepts any JSON value; the server-side code decides what counts as "approved". Read the server's detection logic and match it exactly — a UI resolving `{ approved: true }` against a server checking for an `accepted` key fails silently: the click does nothing, no error anywhere. Whenever approval "does nothing", diff the resolved payload against the server's detection first. +## Older versions -The approval tool name the server surfaces (e.g. `confirm_changes`) must be registered via `useHumanInTheLoop` or no card ever appears. +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. -## Hosted agents: how approval actually travels +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. -When the agent runs as a Foundry hosted agent behind the Responses protocol, an approval-gated tool surfaces as an `mcp_approval_request` item in the Responses stream. The decision must be sent back as an `mcp_approval_response` input item; the hosted agent then re-executes the tool **server-side** on approval. Two consequences: +The two current interop fixes in the wiring §2 shim follow the same rule. -1. **The stock AG-UI adapter does not forward approvals to a remote agent** — it resolves `confirm_changes` locally, so approve appears to succeed but the gated tool never re-executes and state never changes (tracked as microsoft/agent-framework#6652, open as of mid-2026). A bridge to a hosted agent needs explicit approval-forwarding code. Symptom signature: approval card works, approve returns a normal reply, but the side effect never happens. -2. Approve means re-execution happens out of the UI's sight. Verify by observing the *state change* (query the affected record afterwards), not by the chat transcript looking right. +## Debugging decision tree -A bridge supplies that forwarding explicitly — outbound it turns the hosted agent's `mcp_approval_request` into the frontend's approval tool call; inbound it turns the UI's `{accepted}` result back into an `mcp_approval_response` input item: - -```python -# OUTBOUND: hosted agent emits mcp_approval_request -> surface it as the frontend's -# `confirm_changes` tool call so CopilotKit's useHumanInTheLoop renders a card. -elif item["type"] == "mcp_approval_request": - _PENDING_APPROVAL[thread_id] = item["id"] # remember the request id - yield tool_call(APPROVAL_TOOL, { # APPROVAL_TOOL == "confirm_changes" - "function_name": item.get("name", ""), - "function_arguments": item.get("arguments", ""), - }) - -# INBOUND (next turn): the UI's {accepted} result -> an mcp_approval_response input -# item the hosted agent understands. The stock AG-UI adapter never does this step. -pending = _PENDING_APPROVAL.get(thread_id) -if pending and last_tool_result and "accepted" in last_tool_result: - _PENDING_APPROVAL.pop(thread_id, None) - turn_input = [{"type": "mcp_approval_response", - "approval_request_id": pending, - "approve": bool(last_tool_result["accepted"])}] -``` - -## The duplicate-execution hazard (read before shipping any HITL change) - -**Symptom:** one approval works correctly, then a LATER, unrelated turn in the same conversation silently re-executes the same gated tool — the side effect applies twice with no approval card and no visible indication. - -**Root cause (isolated by calling the hosted agent's bare `/responses` endpoint with curl — no AG-UI, no CopilotKit in the loop):** chaining `previous_response_id` through a response that resolved an `mcp_approval_response` makes the hosted runtime re-execute the approved tool on the next turn regardless of that turn's content. The bug lives in the agent-framework/Foundry hosting layer, not in the AG-UI adapter or CopilotKit. Tracked as microsoft/agent-framework#6851 (duplicate execution) and #6828 (related approval-state symptom); both were still open as of July 2026 — check current status before relying on framework behavior. - -**Mitigation for bridges using `previous_response_id` chaining:** after a turn whose input contained an `mcp_approval_response`, do NOT store that response id for chaining — let the next turn start without `previous_response_id`. This costs a sliver of conversational memory and guarantees a gated action never silently executes twice. Platform-mode conversations (Foundry `conversation` objects instead of response-id chaining) have a different mechanism — do not assume they are immune; test explicitly. - -```python -# On response.completed we normally store the id to chain the next turn via -# previous_response_id. But if THIS turn resolved an approval, do NOT store it: -# chaining through an approval-resolving response makes the hosted runtime silently -# re-execute the approved tool on the next, unrelated turn (agent-framework #6851). -if approval_turn: - _LAST_RESPONSE.pop(thread_id, None) # break the chain -> no duplicate exec -else: - _LAST_RESPONSE[thread_id] = response_id -``` - -**Regression test to keep forever:** after an approve, send several unrelated follow-up turns in the same thread and assert the gated tool's side effect did not recur (e.g. a counter incremented exactly once). Remove any local mitigation only when the upstream issues are closed AND this test passes without it — never on a version bump alone. - -## HITL debugging decision tree - -Work top-down; each step has a distinct signature: - -1. **Approve → 400/500 "No tool output found for function call ..."** → the agent's model client is Chat Completions-based. Approval resume on hosted agents requires the Responses-protocol client (`FoundryChatClient` in MAF Python). Swap the client. -2. **No approval card ever appears** → `useHumanInTheLoop` not registered for the surfaced tool name, or the tool is missing `approval_mode="always_require"` (it executes immediately — check server logs for the tool running). -3. **Clicking approve does nothing, no error** → payload-shape mismatch between `respond(...)` and the server's detection (see contract above). -4. **Approval resolves, reply streams, but state never changes** → approval was resolved locally and never reached the remote agent (#6652-class). Confirm the bridge/adapter actually forwards `mcp_approval_response`. -5. **Works once, then a later turn double-executes** → duplicate-execution hazard above. -6. **Card renders during the run but vanishes at `RUN_FINISHED`** → message-snapshot representation differs from live events (multi-tool-call turns lumped into one message; some UI versions render only the first tool call). Fix the snapshot construction or upgrade the UI layer; verify post-run DOM, not just mid-run. -7. Only then suspect environment: tenant mismatch 403s, wrong token audience 401s, stale in-memory data in a locally running agent (restart `azd ai agent run` between test passes). +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. diff --git a/skills/foundry-hosted-agent-copilotkit/references/patterns.md b/skills/foundry-hosted-agent-copilotkit/references/patterns.md deleted file mode 100644 index a721e07de..000000000 --- a/skills/foundry-hosted-agent-copilotkit/references/patterns.md +++ /dev/null @@ -1,31 +0,0 @@ -# The 7 AG-UI interaction patterns on this stack - -These are the canonical AG-UI "dojo" patterns (dojo.ag-ui.com has live Microsoft Agent Framework examples). The MAF AG-UI integration documents support for all seven. The table maps each pattern to its agent-side and CopilotKit-side implementation; the notes that follow cover what is NOT obvious. - -CopilotKit hook naming: `useCopilotAction` still exists but is documented as a legacy compatibility hook. Current names: `useFrontendTool` (agent-callable frontend tools), `useHumanInTheLoop` (approval UI; successor to `useCopilotAction` with `renderAndWaitForResponse`), `useRenderToolCall` / `useRenderTool` (render-only generative UI), `useCoAgent` / `useCoAgentStateRender` (shared state). Verify the exact exports against the installed package's TypeScript declarations — names have moved between minors. - -| # | Pattern | Agent side (MAF Python) | CopilotKit side | -| --- | --- | --- | --- | -| 1 | Agentic chat + frontend tools | plain `Agent` — frontend tools arrive via `RunAgentInput.tools` | `useFrontendTool({ name, parameters, handler })` | -| 2 | Backend tool rendering | `@tool` (executes server-side) | `useRenderToolCall` / render entry for the tool name | -| 3 | Human-in-the-loop | `@tool(approval_mode="always_require")` + `AgentFrameworkAgent(require_confirmation=True)` | `useHumanInTheLoop({ name, render })`, resolve via `respond(...)` | -| 4 | Agentic generative UI | long-running tool emitting progress state | `useCoAgentStateRender` rendering in-progress state | -| 5 | Tool-based generative UI | declaration-only tool (no executable body) the model must call | `useFrontendTool` with `render`, often `followUp: false` | -| 6 | Shared state | state schema + state updates from tools | `useCoAgent` — read `state`, write via `setState` | -| 7 | Predictive state updates | tool-argument streaming configured as optimistic state predictions | `useCoAgent` + confirmation UI | - -## Notes that save hours - -**Pattern 1 (frontend tools).** The tool executes in the browser; the agent only emits the call. If the agent reports it cannot see a frontend tool, check the CopilotKit runtime actually forwards registered tools into `RunAgentInput.tools` on the current package version — a regression in this exact forwarding existed in 1.62.x (CopilotKit/CopilotKit#5813, fixed shortly after). Upgrading or pinning past the fix matters more than any code change. - -**Pattern 2 (backend tool rendering).** The render component receives the streamed tool-call arguments and, later, the result. Two rendering phases exist: live `TOOL_CALL_*` events during the run, and the `MESSAGES_SNAPSHOT` at run end. A card that renders during streaming can vanish at `RUN_FINISHED` if the snapshot represents the turn differently (notably: multiple tool calls lumped into one assistant message when the UI renders only the first). Always verify the card is still present after the run completes. - -**Pattern 3 (HITL).** Full treatment in hitl.md — including the payload-shape contract and the duplicate-execution hazard. - -**Pattern 5 (tool-based generative UI).** The agent-side tool is a declaration without an implementation — the model "calls" it and the frontend renders the arguments as UI. Use `followUp: false` when the tool call is the terminal act of the turn, otherwise the agent narrates the tool call redundantly. If the model must always produce the UI, constrain tool choice on the agent side rather than hoping the prompt suffices. - -**Patterns 4/6/7 (state family) — architecture dependency.** `STATE_SNAPSHOT`/`STATE_DELTA` (RFC 6902 JSON Patch) events are emitted natively only when the AG-UI adapter wraps an in-process agent (Architectures A and B in architecture.md). A Responses-protocol bridge (Architecture C) does not get these events from the hosted agent; they must be synthesized by the bridge from tool-argument deltas, and `setState` from the client must be explicitly forwarded into the agent's input. Before implementing a shared-state feature, confirm which side of this line the codebase is on — otherwise you will write frontend code against events that never arrive. `useCoAgent().state` staying permanently empty usually means the agent has no state schema configured or no tool ever writes the state key, not a frontend bug. - -**Parameter renames ripple into the UI.** Render components typically parse specific fields out of the streamed tool arguments. Renaming a Python tool parameter silently breaks the card (wrong/missing field) while the agent keeps working — the bridge/adapter forwards arguments verbatim. Grep the frontend for the old field name whenever you rename a tool parameter. - -**Grounding-safe tool docstrings.** Don't embed concrete example values in parameter descriptions for fields the model should derive from live data (account numbers, IDs, amounts): models copy literal examples from descriptions into real calls. Describe the shape, use placeholders, and validate inside the tool. diff --git a/skills/foundry-hosted-agent-copilotkit/references/troubleshooting.md b/skills/foundry-hosted-agent-copilotkit/references/troubleshooting.md index d36fe5569..e24f9ed29 100644 --- a/skills/foundry-hosted-agent-copilotkit/references/troubleshooting.md +++ b/skills/foundry-hosted-agent-copilotkit/references/troubleshooting.md @@ -1,53 +1,60 @@ -# Troubleshooting: symptom → root cause → fix +# Troubleshooting and upgrades -All entries are live-verified failure modes with their exact signatures. HITL-specific failures are in hitl.md; this file covers everything else, by layer. +Reproduce at the lowest failing layer first: hosted agent → gateway (`curl -N`) → CopilotKit route → browser. Match the exact error text below. HITL failures are covered in [hitl.md](hitl.md#debugging-decision-tree). -## Debugging method +## Hosted agent and azd -Reproduce at the lowest possible layer before touching code: - -1. `curl -N -X POST -H 'Content-Type: application/json' -d ''` — read raw AG-UI SSE events. Reproduces → frontend innocent. -2. For hosted agents, call the bare `/responses` (or `/invocations`) endpoint directly. Reproduces → AG-UI adapter and CopilotKit innocent; the bug is in the framework/hosting layer. This technique is how the duplicate-execution bug (hitl.md) was isolated. -3. Restart locally running agents between passes — in-memory state from a previous test makes results lie in both directions. - -## CopilotKit runtime / frontend - -| Symptom | Root cause | Fix | +| Symptom | Cause | Fix | | --- | --- | --- | -| "Agent `` not found" | Name drift between runtime `agents` key, `` prop, and hosted `agent.yaml` name; or a single-endpoint/multi-endpoint routing mismatch in the runtime config | Use one shared constant for the agent name; check the runtime's endpoint-mode options against the installed version's docs | -| Requests to runtime sub-routes (e.g. threads) 404/405 | Route handler registered at a fixed path but the runtime version expects a catch-all route serving multiple sub-paths | Use an optional catch-all route segment (`[[...slug]]` in Next.js App Router) and export all HTTP methods the handler supports | -| `next build` type error: `HttpAgent` missing a property (e.g. `pendingInterrupts`) | Installed `@ag-ui/client` version differs from the one `@copilotkit/runtime` was built against | Pin `@ag-ui/client` to exactly the version the installed `@copilotkit/runtime` depends on (check its package.json) | -| Console: "Failed to execute 'fetch' on 'Window': Illegal invocation"; agent never runs | A library captured `fetch` as a bare reference and calls it with the wrong `this` (seen with CopilotKit v2 thread store + `@ag-ui/client` `HttpAgent`) | Bind fetch before any module loads, e.g. an inline script in the root layout ``: `if(!window.fetch.__bound){var f=window.fetch.bind(window);f.__bound=true;window.fetch=f;}` | -| Agent cannot see frontend tools | Known forwarding regression: registered frontend tools not included in `RunAgentInput.tools` (CopilotKit/CopilotKit#5813, 1.62.x era) | Upgrade past the fix; after ANY CopilotKit upgrade, re-test frontend-tool visibility explicitly | -| Stop button / error handling crashes after a run error | Event-order bug appending `TEXT_MESSAGE_END` after `RUN_ERROR` (CopilotKit/CopilotKit#5812) | Track the fix version; avoid relying on post-error events | -| Tool/approval card disappears when the run finishes | `MESSAGES_SNAPSHOT` at run end represents the turn differently than live events (e.g. multiple tool calls merged into one message; UI renders only the first) | Fix snapshot construction (one tool call per assistant message) or upgrade the UI layer; always verify post-run DOM | -| API churn after upgrade (handler factory renamed, provider props changed) | CopilotKit moves APIs between minor versions; `useCopilotAction` is legacy | Verify names against the `.d.ts` files bundled in the installed packages, not docs or memory | +| HTTP 424 `session_not_ready` | `main()` raised before serving `/readiness` (often missing env vars) | Check `azd ai agent monitor`; fail soft by serving a stub agent with an actionable message | +| `RuntimeError: ... running on protocol 1.0.0, but the agent requires protocol 2.0.0` | Hosting package and the `protocols[].version` in `azure.yaml` disagree | Bump the package and `azure.yaml` in one commit | +| `azd deploy`: `Foundry dependencies are not ready: ... FOUNDRY_PROJECT_ENDPOINT is not set` | `init` ran in a directory with an existing `azure.yaml` or env, so the project binding is incomplete (often with the wrong `AZURE_LOCATION` too) | Re-run `azd ai agent init --no-prompt -e -p ... -d ...` in a fresh directory and check `azd env get-values`; don't `azd provision` an existing project | +| `azd ai agent ...` reports the extension "Incompatible" | azd core is older than the extension needs | `azd update && azd extension upgrade --all` | +| Remote build fails but local works | Meta-package `agent-framework` pulls optional extras, or unpinned pre-releases resolve differently | Depend on specific `agent-framework-*` packages; pin versions | +| Deployed agent still runs old code | Traffic pinned to an older version | `azd ai agent show`; move traffic to the latest version | +| 400 "Hosted agents can only be called through the agent endpoint" | Called with `agent_reference` on the project Responses endpoint | Use `.../agents//endpoint/protocols/openai/responses`, or `FoundryAgent(agent_name=...)` | +| `ImportError` for `agent_framework.azure` / `agent-framework-azure-ai` | Superseded package | Migrate to `agent-framework-foundry` | -## AG-UI / adapter layer +## Gateway / AG-UI -| Symptom | Root cause | Fix | +| Symptom | Cause | Fix | | --- | --- | --- | -| 400 with "orphaned" tool-call errors when sending history | Raw AG-UI message history replayed to a Responses endpoint that manages its own history | Derive each turn's input (latest user message or approval decision); never replay the full transcript | -| UI shows a 500 mid-run during a long-running silent tool | Proxy/gateway dropped the idle SSE connection | Emit SSE keep-alive comments (`: ping`) every ~10s from the AG-UI endpoint | -| `useCoAgent().state` always empty | No state schema configured on the agent, no tool writes the state key — or Architecture C without state synthesis (see patterns.md) | Configure state schema + ensure a tool writes it; on a Responses bridge, confirm state synthesis exists at all | +| 401 "audience is incorrect" | Token requested for the wrong scope | Use `https://ai.azure.com/.default` | +| 401/403 invoking the agent | Gateway identity lacks a role on the project, or the token is from the wrong tenant | Assign Azure AI User on the project; `az login --tenant ` | +| 400 mentioning `x-ms-user-isolation-key` | Header sent to a deployed agent | Remove it; identity comes from Entra | +| Browser run fails with `INCOMPLETE_STREAM`: "Cannot send 'TOOL_CALL_END' event: No active tool call found with ID 'mcpr_...'" | Adapter closes a hosted approval id it never started | Use the gateway shim in [wiring §2](wiring.md#2-ag-ui-gateway-wiring-a) | +| `RUN_ERROR` `APPROVAL_RESUME_REQUIRED`: "Pending interrupts require a resume entry for every interruptId" | Approval sent as a tool message only (v1-style `confirm_changes` respond) | Resolve with `useInterrupt` (it sends `resume`); in curl, add a `resume` array | +| Approve → a second approval request instead of execution | Each run hits a new Foundry conversation | `use_service_session=True` + `service_session_id_from_thread_id=True` with `conv_…` thread IDs | +| After an approval, every turn returns `RUN_STARTED` → `RUN_FINISHED` with nothing | Duplicate interrupt tool message reached the adapter | Gateway shim in wiring §2 | +| `ValueError: use_service_session=True requires snapshot persistence` | Service sessions without a way to map threads to conversations | Add `service_session_id_from_thread_id=True` and use `conv_…` IDs as thread IDs ([wiring §2](wiring.md#2-ag-ui-gateway-wiring-a)) | +| HTTP 500 `server_error` from Foundry on the first turn | Thread ID is not a Foundry conversation ID (for example, a UUID generated by CopilotKit) while `service_session_id_from_thread_id=True` | Create the conversation first (`POST .../endpoint/protocols/openai/conversations`) and pass its `conv_…` ID as `threadId` | +| `RUN_ERROR` "No pending approval interrupt found for resume interruptId" | Gateway restarted, or a different replica received the resume | Ask again (the tool didn't run); use session affinity across replicas | +| Tools run but the stream has no `TOOL_CALL_RESULT` events; tool rows render empty | Gateway runs on an older `agent-framework-foundry` (seen with 1.11.0; 1.13.1 works), often a global or user-site Python instead of the project venv | Run the gateway from its own venv with the latest packages; confirm with `python -m pip list` *inside* that venv | +| Async credential errors about `aiohttp` | `azure.identity.aio` needs aiohttp | `pip install aiohttp` | +| 400 about orphaned / unanswered tool calls | Whole AG-UI transcript replayed into `/responses` (custom gateways) | Send only the latest user message plus the conversation / service session | +| `useAgent().agent.state` always empty | No `state_schema` on `AgentFrameworkAgent`, no tool writes state, or a custom gateway that never emits `STATE_*` | Declare the schema on the gateway wrapper; verify `STATE_SNAPSHOT` with curl | -## Foundry connection / auth +## CopilotKit / frontend -| Symptom | Root cause | Fix | +| Symptom | Cause | Fix | | --- | --- | --- | -| 401 "audience is incorrect" | Token requested with default `cognitiveservices.azure.com` scope | Request scope `https://ai.azure.com/.default` | -| 403 `Microsoft.MachineLearningServices/workspaces/agents/action` despite being logged in and having the role | `az` CLI's active subscription/tenant differs from the Foundry project's (multi-tenant accounts). Role lookups under the wrong tenant even fail to resolve the assignee, mimicking missing RBAC | Compare `az account show` with the project's tenant/subscription; `az account set --subscription ` or `az login --tenant `. Zero code changes — do not mistake for a package regression | -| Deployed agent returns 400 on every call from a custom client | Client sends `x-ms-user-isolation-key`; deployed agents use Entra-derived isolation | Remove the header for deployed agents | -| Async `DefaultAzureCredential` fails in the bridge | Missing async transport | `pip install aiohttp` | -| First request to a freshly started local agent 404s `DeploymentNotFound` although the model deployment exists | Warm-up flake in the hosted runtime | Retry once or restart with the same env vars | -| New `azd ai agent run` fails "Address already in use" (confusing hypercorn traceback) | Stale local hosted-agent process holds port 8088 | `ss -ltnp | grep 8088`, kill the stale process, retry | +| "Agent `` not found" | Runtime `agents` key, `agentId`, and agent name disagree | One shared constant | +| 404 on `/api/copilotkit/info` or `/agent/.../run` | v2 runtime is multi-route but the Next.js route isn't a catch-all, or `basePath` is wrong | Use `app/api/copilotkit/[[...slug]]/route.ts` with a matching `basePath` | +| TypeScript error: `HttpAgent` is missing a property | App's `@ag-ui/*` version differs from the one the runtime pins | Match `@ag-ui/client`/`@ag-ui/core` to `npm view @copilotkit/runtime@ dependencies` | +| "Failed to execute 'fetch' on 'Window': Illegal invocation" | A library calls a captured `fetch` without its `this` | Upgrade; or pass `fetch.bind(window)` where a fetch option exists | +| CORS errors from the browser | Browser calling the gateway directly | Browser → CopilotKit route (same origin) → gateway server-side; otherwise use an explicit CORS allowlist | +| Hook not exported / renamed after upgrade | v1 names (`useCopilotAction`, `useCoAgent`, `CopilotKit` provider) used against `/v2` | Use v2 imports from `@copilotkit/react-core/v2`; check the bundled `.d.ts` | +| Dev warning "The agent called the tool ... and no renderer is registered"; tool results invisible | No tool renderer registered | `useDefaultRenderTool(...)` or `useRenderTool({ name, ... })` | +| Playwright: typed text stays in the input; no `/agent/.../run` request | Typed before hydration, or `fill()` didn't trigger the input handler | Wait for `networkidle`, `click()`, `pressSequentially(text)`, then `press("Enter")` and assert the box clears | +| Tool call renders while streaming, then disappears | Final `MESSAGES_SNAPSHOT` differs from live events | Upgrade; test the DOM after `RUN_FINISHED` | -## Python dependency traps +## Upgrades -| Symptom | Root cause | Fix | -| --- | --- | --- | -| Foundry remote image build fails on exotic transitive deps (wasm-related) | Depending on the `agent-framework` meta-package, which drags optional extras | Depend on `agent-framework-core` plus only the specific extras you use (e.g. `agent-framework-foundry`, `agent-framework-ag-ui`) | -| `ImportError` in the hosted container for `mcp` | `agent_framework_foundry_hosting` imports from `mcp` but it is not pulled transitively in remote builds | Add an explicit `mcp` pin to the hosted requirements | -| `httpx` APIs missing (`AsyncClient` gone) | Installing with prerelease resolution pulled an httpx 1.0 dev build | Pin httpx to the current stable line | -| Hosted agent fast-fails: `RuntimeError: the hosted environment is running on protocol 1.0.0, but the agent requires protocol 2.0.0` | Hosting package's Responses protocol version disagrees with `version:` declared in `agent.yaml`/`agent.manifest.yaml` | Bump the package and BOTH manifests' protocol version together | -| Python `@tool` "didn't run in Foundry" when invoking via the Foundry agent client | Client-side tool callables execute client-side by design; only Foundry-native tools run server-side on that path | Expected behavior — host the agent (run the loop server-side) if tools must execute there | +Always move to the **latest** releases, and move everything together: + +1. **`@copilotkit/*` move in lockstep.** `react-core`, `runtime`, and any UI packages stay on one version, and `@ag-ui/*` matches the version the runtime pins. +2. **`agent-framework-*` stay on one release line.** Upgrade `-core`, `-foundry`, `-ag-ui`, and `-foundry-hosting` together. +3. **Protocol version and `azure.yaml` change together** (see the table above). +4. **Workarounds have exit criteria.** Keep a list mapping each local workaround to its upstream issue. Remove a workaround only when the fix has shipped **and** the regression test passes without it. +5. **Read what shipped.** CopilotKit release notes can be empty stubs, so diff the bundled `.d.ts` between versions. +6. **Re-verify live:** the chat path, frontend tools reaching `RunAgentInput.tools`, approve-once / reject-zero / follow-up-zero, cards after `RUN_FINISHED`, then redeploy and repeat against the deployed agent. diff --git a/skills/foundry-hosted-agent-copilotkit/references/upgrading.md b/skills/foundry-hosted-agent-copilotkit/references/upgrading.md deleted file mode 100644 index d90a110c2..000000000 --- a/skills/foundry-hosted-agent-copilotkit/references/upgrading.md +++ /dev/null @@ -1,40 +0,0 @@ -# Upgrading dependencies safely - -Every layer of this stack is pre-1.0, release-candidate, or preview as of mid-2026: `@ag-ui/*` is 0.0.x, `agent-framework-ag-ui` is an RC, the Foundry hosting packages are alpha/beta, hosted agents themselves are preview, and CopilotKit ships weekly minors that move APIs. Upgrades are where this stack breaks. Never bump one package in isolation. - -## Version relationship rules (all must hold simultaneously) - -1. **`@ag-ui/client` ↔ `@copilotkit/runtime`**: the runtime pins an exact `@ag-ui/client` version. Your app's `@ag-ui/client` must match it, or TypeScript breaks on `HttpAgent` shape differences (e.g. missing `pendingInterrupts`). After bumping CopilotKit, read the installed runtime's `package.json` and align. -2. **`@copilotkit/*` packages move together**: `react-core`, `react-ui`, and `runtime` are released in lockstep — never mix versions. Check the lockfile actually resolved what `package.json` asks for. -3. **`agent-framework-*` Python packages stay on one line**: `agent-framework-core`, `agent-framework-foundry`, `agent-framework-ag-ui`, and the hosting package must be from compatible releases. Depend on `agent-framework-core` + specific extras, not the `agent-framework` meta-package (it drags optional dependencies that break Foundry remote image builds). -4. **Hosting protocol version ↔ agent manifests**: the Foundry hosting package implements a specific Responses protocol version; `agent.yaml` AND `agent.manifest.yaml` must declare the same `version:` or the agent fast-fails at startup with an explicit protocol-mismatch RuntimeError. Bump package and both manifests in one commit. -5. **Deprecated package check**: `agent-framework-azure-ai` was superseded by `agent-framework-foundry`. If the codebase still imports the old one, migrate before any other upgrade. - -## The upgrade loop - -1. **Inventory local workarounds first.** Maintain a ledger mapping every patch/workaround in the codebase to the upstream issue it exists for (e.g. approval-forwarding code ↔ microsoft/agent-framework#6652; a `previous_response_id` guard ↔ #6851/#6828; a frontend fetch-bind shim ↔ the Illegal-invocation bug). An upgrade is the ONLY time these may be removed, and only when the issue is closed in the shipped version AND the regression test guarding that workaround passes without it. Never delete a workaround on a version bump alone. -2. **Read what actually shipped.** CopilotKit release notes are often empty auto-release stubs — diff the bundled `.d.ts` files between versions for API changes, and scan the issue tracker for regressions in your integration path (remote `HttpAgent` + frontend tools is a historically fragile combination). -3. **Bump coherently** per the rules above; reinstall; check the lockfile resolution. -4. **Re-verify the full matrix, live** — not just compile: - - read/query path through the real UI; - - frontend tools visible to the agent (explicitly — this has regressed before); - - approve executes the gated tool exactly once; reject executes zero times; - - follow-up turns after an approval do not re-execute (hitl.md hazard); - - tool/approval cards still present after `RUN_FINISHED`, not just during streaming. -5. **For hosted agents**: restart `azd ai agent run` after a dependency change (the local runtime caches nothing between runs, but YOUR in-memory seed data resets — re-baseline before asserting), then redeploy and spot-check the deployed endpoint; local success does not prove the remote image builds (remote builds resolve dependencies independently — explicit pins avoid drift). - -## Known upstream issues to check on every upgrade - -Statuses were accurate as of July 2026 — re-check before acting on them: - -| Issue | What it causes | Local workaround pattern | -| --- | --- | --- | -| microsoft/agent-framework#6652 | AG-UI adapter resolves HITL approvals locally; never forwards to a remote/hosted agent, so approved tools don't re-execute | Custom approval routing in the bridge | -| microsoft/agent-framework#6851 | Approval-gated tool silently re-executes on a later unrelated turn (duplicate side effect) via `previous_response_id` chaining | Don't chain the response id from an approval-resolving turn (hitl.md) | -| microsoft/agent-framework#6828 | Approval UI state reverts to "in progress" after completion; related to #6851 | Cosmetic unless paired with #6851 | -| CopilotKit/CopilotKit#5813 | Frontend tools not forwarded to `RunAgentInput.tools` with remote `HttpAgent` (1.62.x era) | Upgrade past the fix; re-test tool visibility after every bump | -| CopilotKit/CopilotKit#5812 | `TEXT_MESSAGE_END` emitted after `RUN_ERROR`, breaking error handling | Upgrade past the fix | - -## Foundry platform deadlines - -Hosted agents deployed on the pre-April-2026 preview backend (`azure-ai-agentserver-agentframework` / `-langgraph` path) reached end of support on 2026-05-22 — anything still on that path must be redeployed on the current hosting packages, not upgraded in place. See the hosted-agent migration guide on Microsoft Learn (`/azure/foundry/agents/how-to/migrate-hosted-agent-preview`). diff --git a/skills/foundry-hosted-agent-copilotkit/references/wiring.md b/skills/foundry-hosted-agent-copilotkit/references/wiring.md new file mode 100644 index 000000000..366ada399 --- /dev/null +++ b/skills/foundry-hosted-agent-copilotkit/references/wiring.md @@ -0,0 +1,304 @@ +# 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: + +```bash +azd ai agent init --no-prompt -e -p -d \ + --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 # 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: + +```yaml +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`. + +```python +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. + +```python +# 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": "", "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. + +```ts +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): + +```ts +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`: + +```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 : +
{name}: {typeof result === "string" ? result : JSON.stringify(result ?? "")}
, + }); + return null; +} + +export default function Page() { + const [threadId, setThreadId] = useState(); + const [error, setError] = useState(); + 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

{error}

; + if (!threadId) return

Starting conversation…

; + return ( + + + + + + ); +} +``` + +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@ dependencies`, then install that exact version: `npm i @ag-ui/client@`. +- 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` + +| Step | Call | +| --- | --- | +| New thread | `POST {openai}/conversations` → `conversation.id`; `POST {agentEndpoint}/sessions` → `agent_session_id`; store `{threadId → conversationId, sessionId, owner}` | +| Start a turn | `POST {openai}/responses` `{input, conversation, agent_session_id, background: true, store: true, stream: false}` → save `response.id` | +| Stream to the UI | Poll `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` | +| Reconnect | Same poll loop starting from the saved `response.id`. Disconnecting must **not** cancel the run | +| Cancel | `POST {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=` 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 + +```bash +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.