# Agent Mission Control pricing One plan: $1,500 a month per installation, billed through Microsoft Marketplace. No per-seat fees and no usage charges from Cay Digital. The Azure resources AMC deploys are billed separately on the customer's own Azure invoice. Annual billing, additional instances and custom terms are available by private offer from the listing. Microsoft Marketplace listing: https://marketplace.microsoft.com/en-us/product/caydigitalllc1786477853901.agent-mission-control # Agent Mission Control user guide > Task-based help for Agent Mission Control (AMC), the business operations center for your agentic workforce. Every article below is also available individually as Markdown. # Get started with Agent Mission Control Agent Mission Control user guide > Getting started > What AMC does, how the app is laid out, and where to begin for your role. **Who can do this:** Everyone Agent Mission Control (AMC) is the business operations center for your agentic workforce. It manages the work your agents do, measures what that work costs, and governs it against the standards your business sets. AMC runs in your organization's own Azure tenant. ## Sign in 1. Open the AMC address your administrator gave you. 2. Select **Sign in**. AMC sends you to your organization's identity provider (Microsoft Entra or Auth0). 3. After sign-in you land on **Active Work**. > **Note:** If you see a banner saying no department has been assigned to you yet, you can only see workflows that aren't in a department. Ask your administrator to add you to one. See [Roles and permissions](https://www.agentmissioncontrol.ai/guide/roles-and-permissions/). ## Find your way around The left sidebar shows icons. Hover an icon to see its name. You only see the items your role allows. | Sidebar item | What it's for | | --- | --- | | **Active Work** | Live and recent workflow runs, plus the items waiting on a person. | | **ROI** | Savings, cost, tokens, and human interventions by department and workflow type. | | **Flagged Reviews** | The Review queue: human interventions, failures to attest, and flagged steps. | | **Governance** | Budgets, overrides, golden runs, failures, lifecycle, and the audit log. | | **Agents** | Every registered workflow and agent: register, run, manage, and assign owners. | | **Settings** | Users, departments, budgets, email, sign-in, triggers, and system details. | The top bar holds the **Department** filter (shown when you can see two or more departments), the notification bell, and your avatar, which opens your profile. ## Where to begin - **You answer agents:** read [Respond to a human intervention](https://www.agentmissioncontrol.ai/guide/respond-to-interventions/) and [Choose which notifications you get](https://www.agentmissioncontrol.ai/guide/notification-preferences/). - **You run a department:** read [Monitor runs in Active Work](https://www.agentmissioncontrol.ai/guide/active-work/) and [Manage department budgets and overrides](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/). - **You administer AMC:** start with [Add users, departments, and roles](https://www.agentmissioncontrol.ai/guide/users-and-departments/), then [Set up email notifications](https://www.agentmissioncontrol.ai/guide/email-setup/). ## Related - [Roles and permissions](https://www.agentmissioncontrol.ai/guide/roles-and-permissions/) - [Monitor runs in Active Work](https://www.agentmissioncontrol.ai/guide/active-work/) - [Choose which notifications you get](https://www.agentmissioncontrol.ai/guide/notification-preferences/) --- Source: https://www.agentmissioncontrol.ai/guide/get-started/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Roles and permissions Agent Mission Control user guide > Getting started > The four governance roles, what each can do, and why your profile role grants nothing. **Who can do this:** Everyone Access in AMC comes from **governance roles**. There are four, in rank order. A user with no governance role shows as **None**. | Role | Scope | | --- | --- | | **Super Admin** | The whole installation. Also the only role that can request in-app updates and revoke Super Admin or Tenant Admin. | | **Tenant Admin** | Everything in one organization: users, departments, email, sign-in, triggers, and all departments' work. | | **Department Manager** | The departments they manage: runs, interventions, budgets, overrides, golden runs, and department audit. | | **Agent Manager** | The agents they manage: register and run them, with read access to those agents' departments. | > **Important:** The **Role** on your profile (Admin, Manager, or Viewer) is a **base role**. It is a label only and grants no access. Access comes only from governance roles, which a Tenant Admin grants under **Settings > Users**. ## What each role can do | Action | Tenant Admin | Department Manager | Agent Manager | | --- | --- | --- | --- | | Start or cancel a run | Yes | Yes | Yes | | Register, edit, deactivate agents | Yes | Yes | Yes | | Reassign an agent's owner | Yes | Yes | No | | Reply to an intervention | Yes | Yes | No | | Promote or retire a golden run | Yes | Yes | No | | Mark a run Failed or Complete | Both ways | Complete to Failed only | No | | Edit budgets, submit overrides | Yes | Yes | No | | Open the Governance tab | Yes | Yes | No | | Manage users, departments, email, sign-in, triggers | Yes | No | No | Super Admin can do everything in the table. Department Managers act only inside their own departments. Approving a budget override also depends on the approval tier; see [Manage department budgets and overrides](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/). ## Departments decide what you see Runs, interventions, ROI, and budgets are scoped to the departments you can access. If you have no department, you see only workflows that aren't assigned to one. Use the **Department** filter in the top bar to narrow every page to one department. ## Related - [Add users, departments, and roles](https://www.agentmissioncontrol.ai/guide/users-and-departments/) - [Get started with Agent Mission Control](https://www.agentmissioncontrol.ai/guide/get-started/) - [Troubleshooting and FAQ](https://www.agentmissioncontrol.ai/guide/troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/guide/roles-and-permissions/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Monitor runs in Active Work Agent Mission Control user guide > Daily work > Read the dashboard, filter all runs, open a run, and cancel one. **Who can do this:** Everyone with department access. Cancel: Tenant Admin, Department Manager, Agent Manager. **Active Work** is the home page. It shows what your agents are doing now and what needs a person. ## Read the dashboard - **Time range:** choose **Today**, **This Week** (default), **This Month**, or **This Year**. Ranges are rolling, so This Week means the last 7 days. - **Tiles:** **Running**, **Completed**, **Pending**, and **Failed**. Select a tile to open the matching list of runs. - **Attention queue:** counts of human interventions (with how many are past SLA), failures pending attestation, and flagged steps. Select **Open Reviews queue** to work them. - **Recent Workflow Runs:** up to 12 run cards with status, start time, and step progress. Select a card to open the run. ## Status and outcome are different **Status** is where a run is in its lifecycle: Running, Intervention, Pending, Completed, Failed, or Cancelled. **Outcome** is the verdict: Success or Failure. An admin can change a run's outcome without changing its status. Cancelled runs have no outcome. ## Find any run 1. On Active Work, select **See All Runs**. 2. Filter by search text, **Status**, **Outcome**, date range, or workflow **Type**. 3. Hover a status badge to see why a run ended. Select a row to open it. ## Inside a run - The header shows status, department, start time, the agents involved, and step progress. - **Steps** lists each step in order. Select a step to see its result, model usage, attached files, and any intervention thread. Select **Show details** for the agent's thinking and tool calls. - **Artifacts** lists files the run produced. HTML artifacts open in a sandboxed viewer; other files download. - Runs update live while you watch. ## Cancel a run 1. Open the run, or find it in **See All Runs**. 2. Select **Cancel run**. 3. Confirm. Completed steps are kept, but the run won't continue. This can't be undone. > **Note:** Runs can't be paused or restarted. A run waiting on a person resumes when someone replies to its intervention. ## Related - [Start a workflow run manually](https://www.agentmissioncontrol.ai/guide/start-a-run/) - [Respond to a human intervention](https://www.agentmissioncontrol.ai/guide/respond-to-interventions/) - [Diagnose a failed run](https://www.agentmissioncontrol.ai/guide/diagnose-failures/) --- Source: https://www.agentmissioncontrol.ai/guide/active-work/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Start a workflow run manually Agent Mission Control user guide > Daily work > Use Run now to start a workflow, fill its inputs, and pick who answers its interventions. **Who can do this:** Tenant Admin, Department Manager, Agent Manager 1. Select **Run now** on Active Work, on **See All Runs**, or next to a workflow on the **Agents** page. 2. In **Start a workflow run**, choose the **Workflow type**. You only see enabled types in departments you can access. 3. Fill in the workflow's inputs. Required inputs are marked with an asterisk. 4. Optional: set **Assignee** to route this run's interventions to a specific person. Leave it as **Workflow default** to use the workflow's normal routing. 5. Optional: add a **Label** (up to 80 characters) so you can find the run later. 6. Select **Start run**. The run starts immediately and AMC opens it. Runs you start by hand carry a **Manual** badge. ## If the run won't start | Message | What to do | | --- | --- | | A budget block with **Request a budget override** | The department has hit its budget cap. Request an override; see [budgets and overrides](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/). | | Runs are switched off for this workflow type | Someone disabled the workflow. Re-enable it from **Agents > Manage > Workflow settings**. | | Some run inputs need attention | Fix the fields highlighted in the dialog. | | This workflow is sensitive | Sensitive workflows can only be assigned to the department's manager. Pick them or keep the default. | | No workflow types available for your role | You have no runnable workflow types. Ask a Tenant Admin. | ## Related - [Monitor runs in Active Work](https://www.agentmissioncontrol.ai/guide/active-work/) - [Schedule workflows and set up webhooks](https://www.agentmissioncontrol.ai/guide/schedules-and-webhooks/) - [Register agents and workflows](https://www.agentmissioncontrol.ai/guide/register-agents/) --- Source: https://www.agentmissioncontrol.ai/guide/start-a-run/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Respond to a human intervention Agent Mission Control user guide > Daily work > When an agent pauses to ask a person, reply with context and attachments so the run continues. **Who can do this:** Department Managers for the workflow's department, Tenant Admins, Super Admins A **human intervention** is a question an agent asks a person mid-run. The run waits at that step until someone replies. ## Find interventions waiting on you - The **notification bell** shows up to five open interventions assigned to you. - **Flagged Reviews > Human interventions** lists every intervention you can see. - The **Human interventions** tile on Active Work shows how many are past SLA. - Email, if your organization has email set up and you haven't turned it off. ## Reply 1. Open the intervention from the bell or the Review queue. 2. Read **Workflow context** (what the agent was doing) and **Request for review** (its question and any files). 3. Optional: under **Suggested response**, select **Use this reply** or a suggested choice. This only fills the reply box; nothing is sent yet. 4. Write or edit your answer in **Your reply**. Markdown bold, italic, code, and lists work. 5. Optional: attach files with the paperclip, or drag them onto the reply box. 6. Select **Send reply**. Sending closes the intervention. Its status becomes **Completed** and the agent continues the run. There is no separate approve or resolve button. ## Attachments Every file is scanned for malware before it can be sent. Each file shows **Uploading**, **Scanning**, then **Ready**. A file marked **Quarantined** must be removed before you can send. | Default limit | Value | | --- | --- | | File size | 25 MB per file | | Files per reply | 5 | | Allowed types | PNG, JPG, GIF, PDF, DOCX, XLSX, PPTX, TXT, CSV, JSON | | Retention | 30 days, then files are purged | Your administrator can change these limits. See [Set intervention SLAs and upload limits](https://www.agentmissioncontrol.ai/guide/intervention-settings/). ## Statuses and SLA | Status | Meaning | | --- | --- | | **Awaiting response** | Open and within its SLA. | | **Overdue** | Past its SLA. AMC re-notifies, then escalates to the department fallback admin, then to the org admin. | | **Completed** | Someone replied and the run continued. | | **Withdrawn** | The run ended before anyone answered. No reply is possible. | ## Who it's assigned to The **Assigned to** bar shows the assignee. **Unassigned** means any manager of that department can respond. If a department has no managers, the intervention escalates to the org admin. Assignment is set per workflow (its **Default assignee**) or per run when it's started; it can't be changed from the thread. ## Interventions handled in another system Some workflows route interventions to ServiceNow, HubSpot, or Salesforce. Those threads show **Continue this response in** the other platform, with a button to open the ticket. Status syncs back to AMC automatically. ## Related - [Work the Review queue](https://www.agentmissioncontrol.ai/guide/review-queue/) - [Choose which notifications you get](https://www.agentmissioncontrol.ai/guide/notification-preferences/) - [Set intervention SLAs and upload limits](https://www.agentmissioncontrol.ai/guide/intervention-settings/) --- Source: https://www.agentmissioncontrol.ai/guide/respond-to-interventions/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Work the Review queue Agent Mission Control user guide > Daily work > One queue for interventions, failures to attest, and steps someone flagged. **Who can do this:** Everyone, scoped to the departments you can access Open **Flagged Reviews** in the sidebar. The page is titled **Review queue** and has four tabs. | Tab | What's in it | | --- | --- | | **Human interventions** | Questions agents asked people, with assignee, SLA, and status. | | **Failures** | Failed runs, with an AI-suggested cause and whether a cause is still required. | | **Flagged steps** | Steps someone flagged for review, with the note, output, and reasoning. | | **All** | Everything above in one list. | ## Filter the queue - **Window:** 30, 60, or 90 days, or a custom range. Default is 30 days. - **Department:** use the Department filter in the top bar. - **Cause:** on the Failures tab, filter by failure cause. ## Flag a step for review 1. Open a run and select a step, or open an intervention thread. 2. Select **Flag** (or the flag icon in a thread). 3. Optional: add a **Note** of up to 500 characters for whoever triages it. 4. Select **Flag**. The step appears under **Flagged steps**. To clear it, select **Remove flag** on the Flagged steps tab or in the flag dialog. ## Related - [Respond to a human intervention](https://www.agentmissioncontrol.ai/guide/respond-to-interventions/) - [Diagnose a failed run](https://www.agentmissioncontrol.ai/guide/diagnose-failures/) - [Monitor runs in Active Work](https://www.agentmissioncontrol.ai/guide/active-work/) --- Source: https://www.agentmissioncontrol.ai/guide/review-queue/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Diagnose a failed run Agent Mission Control user guide > Daily work > Attest a failure's cause, compare against the golden run, and correct a run's outcome. **Who can do this:** Tenant Admin, Department Manager, Agent Manager (attest). Changing outcomes: see below. Every failed run can be attested with a confirmed cause. Attestation builds the failure record your team uses to fix agents, and some workflows require it before they run again. ## Attest a cause 1. Go to **Flagged Reviews > Failures**, or select **Failures pending attestation** on Active Work. 2. Open the failed run. Review the failed step, the reason, and the side-by-side diff. 3. If AMC shows an **AI-suggested cause**, select **Accept**, **Edit**, or **Reject**. Otherwise select **Confirm cause**. 4. Pick a cause from the taxonomy. If you pick **Other**, notes are required. 5. Select **Save attestation**. The confirmed cause shows who attested it, and **View history** shows earlier versions. ## Compare with the golden run When the workflow has a golden run, the **Side-by-side diff** lines up each step of the failed run against it and marks each one Matched, Diverged, Failed, Skipped, or Missing. With no golden run, promote a recent successful run first. See [Set and retire golden runs](https://www.agentmissioncontrol.ai/guide/golden-runs/). ## Mark a run Failed or Complete If a run's outcome is wrong, correct it. The run's status badge stays the same; its outcome changes, and ROI pass rates follow. 1. Open the run. 2. Select **Mark Failed** (on a completed run, or one waiting on a person) or **Mark Complete** (on a failed run). 3. Enter a **Reason** of 10 to 500 characters. 4. Confirm. The change is recorded under **Governance > Completion Overrides** and in the audit log. > **Note:** Tenant Admins can change outcomes in both directions. Department Managers can only mark completed runs as failed, in their own departments. Failure causes come from a core taxonomy managed centrally. A Tenant Admin can turn on AI cause suggestions under **Settings > Failure Causes > AI suggestions** by entering a Foundry project endpoint and model. ## Related - [Set and retire golden runs](https://www.agentmissioncontrol.ai/guide/golden-runs/) - [Work the Review queue](https://www.agentmissioncontrol.ai/guide/review-queue/) - [Review governance and the audit log](https://www.agentmissioncontrol.ai/guide/governance-audit/) --- Source: https://www.agentmissioncontrol.ai/guide/diagnose-failures/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Set and retire golden runs Agent Mission Control user guide > Daily work > A golden run is the reference a workflow's failures are compared against. **Who can do this:** Tenant Admin. Department Managers for workflows in their own department. A **golden run** is a completed run you trust, saved as the change-control reference for a workflow. Failure diagnosis compares failed runs against it step by step. ## Promote a run 1. Open a completed run. 2. Select **Set as Golden Run**. 3. Explain **Why is this the new golden?** (at least 8 characters). 4. If you're replacing a golden run, optionally comment on the previous one. 5. Select **Promote**. You can also promote from the agent's page under **Agents > Manage > Golden run**. ## Retire without a replacement 1. Go to **Governance > Golden Runs** and select the workflow. 2. Select **Retire without replacement**. 3. Enter a **Reason for retirement** (at least 8 characters) and confirm. The workflow is then flagged as missing a golden run until a new one is promoted. **Governance > Golden Runs** lists every workflow with its current golden run, who promoted it, and the full history. Filter by **Set** or **Not set** to find gaps. ## Related - [Diagnose a failed run](https://www.agentmissioncontrol.ai/guide/diagnose-failures/) - [Review governance and the audit log](https://www.agentmissioncontrol.ai/guide/governance-audit/) --- Source: https://www.agentmissioncontrol.ai/guide/golden-runs/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Choose which notifications you get Agent Mission Control user guide > Account & notifications > Turn in-app and email notifications on or off, and understand the notification bell. **Who can do this:** Everyone ## Change your notification preferences 1. Select your avatar in the top bar to open your profile. 2. Select **Notification preferences**. 3. Check or clear a box in the **In-app** or **Email** column. Each change saves immediately. You can also go straight to `/preferences` in your AMC address. ## What you can change today | Notification | In-app | Email | Default | | --- | --- | --- | --- | | Intervention assigned to me | Yes | Yes | On | | Intervention waiting for my answer | Not available | Yes | On | | Workflow completed | Not available | Yes | Off | | Workflow failed | Not available | Yes | On | | Orchestrator unreachable | Not available | Yes | On | | Notification delivery failed | Not available | Yes | On | Other rows on the page (new thread messages, overdue interventions, unassigned interventions, budget events, and skipped runs) are marked **Coming soon** and can't be changed yet. ## The notification bell - The bell shows open interventions assigned to you where the agent spoke last. The badge counts them, up to **9+**. - The dropdown lists up to five. Overdue ones are marked. Select one to open it. - There is no mark-as-read. An item leaves the bell when you reply or the intervention closes. - Select **View all human interventions** for the full list. - Budget, workflow, and system events are sent by email, not to the bell. > **Note:** Email only arrives once your administrator connects an email service. If you get no email at all, see [Set up email notifications](https://www.agentmissioncontrol.ai/guide/email-setup/) or ask your Tenant Admin. ## Related - [Respond to a human intervention](https://www.agentmissioncontrol.ai/guide/respond-to-interventions/) - [Set up email notifications (SMTP)](https://www.agentmissioncontrol.ai/guide/email-setup/) - [Sign in and manage your profile](https://www.agentmissioncontrol.ai/guide/profile-and-sign-in/) --- Source: https://www.agentmissioncontrol.ai/guide/notification-preferences/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Sign in and manage your profile Agent Mission Control user guide > Account & notifications > What your profile shows, where your name comes from, and how to sign out. **Who can do this:** Everyone ## Open your profile Select your avatar in the top bar. The profile panel shows your **Email**, **First Name**, **Last Name**, **Description**, and **Role**, plus links to **Notification preferences** and **Sign out**. Your name and email come from your organization's identity provider, so the panel is read-only. To change them, update your account with your IT team. > **Note:** The **Role** on your profile is a label. What you can do comes from governance roles. See [Roles and permissions](https://www.agentmissioncontrol.ai/guide/roles-and-permissions/). ## Sign out Open your profile and select **Sign out**. ## Sign-in problems If sign-in shows a message, look it up in [Troubleshooting and FAQ](https://www.agentmissioncontrol.ai/guide/troubleshooting/). ## Related - [Choose which notifications you get](https://www.agentmissioncontrol.ai/guide/notification-preferences/) - [Troubleshooting and FAQ](https://www.agentmissioncontrol.ai/guide/troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/guide/profile-and-sign-in/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Read the ROI dashboard and export reports Agent Mission Control user guide > Measure & govern > See savings, cost, tokens, and interventions, trace every figure to its runs, and export. **Who can do this:** Everyone (dashboard). ROI Inspector: Tenant Admin, Department Manager. Open **ROI** in the sidebar. Choose **Today**, **This Week** (default), **This Month**, or **This Year**. ## What the tiles mean | Tile | Meaning | | --- | --- | | **Est. Savings** | Manual cost of the work minus what AMC spent on it. Shows its share of gross value and the pass rate. | | **Est. Cost** | Model cost plus human cost for the period. | | **Tokens** | Tokens used, split into agent and host. | | **Human Interventions** | How many times a person stepped in. | ## How ROI is calculated - **Net savings** = manual-run cost baseline - (model cost + human cost). - **Human cost** = interventions x the workflow's HITL cost. - **Pass rate** = passing runs / runs with an evaluation signal. It follows any outcome corrections. - Each workflow has a **Manual cost / run** and a **HITL cost**, set under **Agents > Manage > Workflow settings**. Until someone sets them, AMC uses defaults of $150 per run and $50 per intervention and labels them **Default**. ## Drill down If you can see more than one department, the page opens on **Savings by Department**. Select a department to see **Savings Breakdown by Workflow Type**. Select **Back to all departments** to return. ## Trace a figure in the ROI Inspector Select any tile or workflow row to open the **ROI Inspector**. It lists every run behind the number, with tokens, model cost, interventions, human cost, baseline, net savings, and pass or fail. **Reconciles to** confirms the per-run figures add up to the dashboard total. Model cost is labeled **Provider-actuals** when reconciled against Azure billing, **Pending reconciliation** before that, or **Metadata-derived** when computed from token counts. ## Export 1. Select **Download ROI Report** on the dashboard or in the Inspector. 2. Choose **CSV**, **Excel**, or **JSON**. Exports cover the selected period and department, include 11 columns that match the Inspector, carry a schema version for data pipelines, and are recorded in the audit log. ## Related - [Manage department budgets and overrides](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/) - [Register agents and workflows](https://www.agentmissioncontrol.ai/guide/register-agents/) --- Source: https://www.agentmissioncontrol.ai/guide/roi-dashboard/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Manage department budgets and overrides Agent Mission Control user guide > Measure & govern > Set budgets and per-workflow caps, understand hard caps, and request or approve overrides. **Who can do this:** Tenant Admin, Department Manager (own departments). Override tiers: Tenant Admin. ## Set a department budget 1. Go to **Settings > Budgets**. 2. On the department's card, enter the **Budget** amount. 3. Choose the **Period**: Monthly, Rolling 30-day, or Fiscal Quarter. 4. Optional: select **Show workflow budgets** and set a budget for each workflow. 5. Select **Save**. > **Important:** A budget's period is set when it's first saved and can't be changed afterwards. If workflow budgets add up to more than the department budget, AMC warns you but still saves. Caps are enforced on actual spend. ## What happens as spend grows | Consumed | What happens | | --- | --- | | 80% or more | **Approaching cap.** The department head and workflow owners are notified. Runs continue. | | 90% or more | **Request override** appears on the Governance Budgets tab. | | 100% or more | **Over cap.** New runs are halted until an override is approved. | Track spend on **Governance > Budgets**. Select a department for spend against budget, days remaining, and per-workflow consumption. ## Request a budget override 1. On **Governance > Budgets**, select **Request override** on the department, or go to **Budget Overrides** and select **New request**. 2. Enter the **Amount over budget (USD)**. AMC shows which approval tier the request routes to. 3. Write a **Justification** of 10 to 500 characters. Optionally add a supporting link. 4. Select **Submit request**. A department can have one pending request at a time. Track yours under **Budget Overrides > Mine**. ## Approve or deny an override 1. Go to **Governance > Budget Overrides** and select your tier. 2. Review the request and its **Justification chain**. 3. Select **Approve** (a note is optional) or **Deny** (a reason is required and sent to the requester). Approval takes effect immediately. You can only act on tiers your role can approve; others show a lock. ## Configure approval tiers Under **Settings > Budgets > Override tiers**, a Tenant Admin sets the approval ladder. Each tier has a name, an amount threshold, and an approver: **Super Admin**, **System Admin**, **Department Admin** (routes to the requesting department's admin), or a **Specific Person**. The top tier covers every amount above the previous threshold. Select **Save tiers**. ## Related - [Read the ROI dashboard and export reports](https://www.agentmissioncontrol.ai/guide/roi-dashboard/) - [Review governance and the audit log](https://www.agentmissioncontrol.ai/guide/governance-audit/) - [Start a workflow run manually](https://www.agentmissioncontrol.ai/guide/start-a-run/) --- Source: https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Review governance and the audit log Agent Mission Control user guide > Measure & govern > Use the Governance page for the period overview, lifecycle, audit log, and completion overrides. **Who can do this:** Tenant Admin, Department Manager (department scope) **Governance** appears once your organization has at least one department. Tabs your role can't open show a lock. | Tab | What it shows | | --- | --- | | **Overview** | The period at a glance: active agents, spend against cap, budgets configured, pending overrides, failure rate, confirmed causes, and workflows without a golden run. | | **Budgets** | Each department's budget, spend, and days remaining. See [budgets and overrides](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/). | | **Golden Runs** | The reference run for each workflow. See [golden runs](https://www.agentmissioncontrol.ai/guide/golden-runs/). | | **Failures** | Failure rate over time, top causes, and a per-workflow breakdown. | | **Lifecycle** | How many workflows are enabled or disabled, and recent changes. | | **Budget Overrides** | Requests by approval tier, and your own requests. | | **Audit Log** | Every governed action: who did what, when, why, and the before and after values. | | **Completion Overrides** | Every run outcome someone changed, with the reason. | ## Search the audit log 1. Open **Governance > Audit Log**. 2. Set the window (30, 60, 90 days, or custom). 3. Filter by actor, action type, or target and department. Audit scope follows your role: Super Admins see the installation, Tenant Admins their organization, Department Managers their departments, and Agent Managers their agents. ## Enable or disable a workflow Go to **Agents**, select **Manage** on the workflow, then use **Workflow settings > Scheduling**. Disabling stops AMC scheduling new runs; runs already in progress finish normally. Every change shows on the Lifecycle tab. ## Related - [Manage department budgets and overrides](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/) - [Diagnose a failed run](https://www.agentmissioncontrol.ai/guide/diagnose-failures/) - [Roles and permissions](https://www.agentmissioncontrol.ai/guide/roles-and-permissions/) --- Source: https://www.agentmissioncontrol.ai/guide/governance-audit/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Add users, departments, and roles Agent Mission Control user guide > Administration > Invite people, organize work into departments, and grant governance roles. **Who can do this:** Tenant Admin. Granting Super Admin or Tenant Admin: Super Admin. ## Add a user 1. Go to **Settings > Users** and select **Add User**. 2. Enter **Full name** and **Email address**. The email is the person's sign-in identity and can't be changed later. 3. Choose a base **Role** (Admin or Manager). This is a label only. 4. Save. Then grant a governance role so the person can do anything. ## Grant or revoke a governance role 1. In **Settings > Users**, select **Manage** in the person's **Governance Roles** column. 2. Select **Add role**, choose the **Role**, and for Department Manager pick the departments. 3. Optional: add a reason. Select **Grant role**. To revoke, remove the role and enter a reason; revoking always requires one. - A person must sign in once before they can be granted a role. - Super Admins grant Super Admin and Tenant Admin. Tenant Admins grant Department Manager. - You can't remove your own Super Admin role, and the last Super Admin can't be removed. - Every grant and revoke is written to the audit log. ## Create a department 1. Go to **Settings > Departments** and select **Add department**. 2. Enter a **Department name**. 3. Use **Add a workflow** to assign workflows, and **Add a department manager** to assign managers. 4. Select **Save changes**. Creating the first department turns on budgets, overrides, and the Governance page. You can also assign someone to departments from the **Departments** column in **Settings > Users**. ## Deactivate a user In **Settings > Users**, select **Deactivate user** on the person's row. They're refused at sign-in and signed out everywhere. Select **Reactivate user** to restore access with the roles they still hold. You can't deactivate yourself, and only a Super Admin can deactivate a Super Admin or Tenant Admin. ## Related - [Roles and permissions](https://www.agentmissioncontrol.ai/guide/roles-and-permissions/) - [Configure your sign-in provider](https://www.agentmissioncontrol.ai/guide/sign-in-provider/) - [Manage department budgets and overrides](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/) --- Source: https://www.agentmissioncontrol.ai/guide/users-and-departments/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Set up email notifications (SMTP) Agent Mission Control user guide > Administration > Connect your email service so AMC can send notifications, then check every delivery. **Who can do this:** Tenant Admin, Super Admin > **Important:** Email is off for every organization until an admin connects an email service. Notifications that would have been sent before then are logged as **Skipped** and are never sent later. ## Connect your email service 1. Go to **Settings > Notifications**. 2. Under **Service**, choose **Exchange Online (OAuth)** or **Other SMTP server**. 3. For Exchange Online, enter the **Directory (tenant) ID** and **Application (client) ID** of your Entra app and its **Client secret**. 4. For another server, enter the **Server** host name, **TLS mode** (STARTTLS on 587 or implicit TLS on 465), **Username**, and **Password or API key**. 5. Under **Sender**, enter the **From address** and **From name**. Your email service must allow that address. 6. Select **Save Changes**, then **Send test email**. The test goes to your own address. Secrets are write-only and stored in a key vault. Leave the field blank to keep the stored one. ## Test without emailing people Turn on **Test mode** and enter a **Catch-all address**. Every email then goes to that address instead of its recipient. Turn it off when you're done. ## Check deliveries The **Delivery log** at the bottom of the page lists every notification email, newest first. Filter by status or notification type. | Status | Meaning | | --- | --- | | **Sent** | Your email server accepted it. Later bounces aren't shown. | | **Queued** | Waiting to send. | | **Failed** / **Bounced** | Not delivered. Check **Details**. | | **Opted out** | The recipient turned that notification off. | | **Skipped** | Email wasn't configured when it was due. | The connection badge reads **Working**, **Failing**, **Not tested**, or **Not configured**. Each person chooses their own notifications; see [Choose which notifications you get](https://www.agentmissioncontrol.ai/guide/notification-preferences/). ## Related - [Choose which notifications you get](https://www.agentmissioncontrol.ai/guide/notification-preferences/) - [Set intervention SLAs and upload limits](https://www.agentmissioncontrol.ai/guide/intervention-settings/) --- Source: https://www.agentmissioncontrol.ai/guide/email-setup/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Configure your sign-in provider Agent Mission Control user guide > Administration > Connect Microsoft Entra or Auth0, map new users, and test changes before they go live. **Who can do this:** Tenant Admin, Super Admin Go to **Settings > Identity**. AMC supports **Microsoft Entra** and **Auth0**. ## Enter the connection - **Authority:** for Entra, `https://login.microsoftonline.com//v2.0`. For Auth0, your domain including `https://`. - **Client ID** and **Client secret** (write-only). - **Audience:** for Entra, the same value as the Client ID. - **Scopes:** must include `openid`. Usually `openid, profile, email`. - **Redirect URIs:** register `/api/auth/callback` as the callback and your AMC address as the sign-out URL in your provider. ## Map new users Under **New users**, add **Claim mappings** from a provider group to an AMC base role and, optionally, a department. Mappings apply at a person's first sign-in and only set how they're listed. Access still comes from roles granted under **Settings > Users**. ## Test, then activate 1. Select **Save Changes**. Changes that affect sign-in become a pending change; people keep signing in with the current settings. 2. Select **Test sign-in** and sign in with an admin account in the window that opens. 3. Within 15 minutes of a passing test, select **Activate**. Select **Discard changes** to drop a pending change. If the test fails, the result names the reason, such as no matching AMC user or an unverified email. ## Related - [Add users, departments, and roles](https://www.agentmissioncontrol.ai/guide/users-and-departments/) - [Troubleshooting and FAQ](https://www.agentmissioncontrol.ai/guide/troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/guide/sign-in-provider/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Register agents and workflows Agent Mission Control user guide > Administration > Connect an agent or workflow to AMC, import from Foundry, and manage its lifecycle and owner. **Who can do this:** Tenant Admin, Department Manager, Agent Manager. Reassign owner: Tenant Admin, Department Manager. The **Agents** page lists every workflow and agent connected to AMC. Filter by department, kind, adapter, source, or status, or check **Orphaned only** to find resources whose owner has left. ## Register a workflow or agent 1. On **Agents**, select **Register**. 2. Enter a **Name**, choose the **Kind** (Workflow or Agent) and **Adapter**, and pick a **Department**. Kind and adapter can't change later. 3. Under **Connection**, enter the **Endpoint URL**. For custom endpoints, add path segments, or check that the resource uses the AMC SDK. 4. Under **Authentication**, choose **Managed Identity** or **API key**. API keys are stored securely and never shown again. 5. Optional: mark the resource as sensitive, set artifact retention, declare an **Input contract** (the parameters a run accepts), and list **Declared steps**. 6. Select **Register**. AMC checks the endpoint and reports **Healthy**, **Unreachable**, or **Access not granted**. > **Note:** With managed identity, grant AMC's identity a least-privilege role on the resource first. Without it you'll see **Access not granted**. Only a Tenant Admin can use managed identity with a self-hosted endpoint. ## Import from Foundry Select **Import from Foundry**, enter the **Foundry project endpoint** (the project URL, not an agent URL), choose a department and authentication, and select **Import project**. Re-running is safe: existing registrations stay, and only new ones are added. ## Manage a registration Select **Manage** on a row. From there you can **Run**, **Edit**, set an SLA override, set a golden run, and under **Workflow settings** turn scheduling on or off and set the **Default assignee**, **Manual cost / run**, and **HITL cost**. ## Deactivate or delete | Action | Effect | | --- | --- | | **Deactivate** | Archives the resource so it can't run. Run history and ROI are kept. | | **Delete** | Permanently removes the resource with its version history, triggers, and all run history, including ROI and interventions. | Use Deactivate to retire something while keeping its history. Neither works while a run is in progress. ## Reassign an owner Select **Reassign** on the row (or **Reassign owner** on its page), choose the **New owner**, and confirm. Resources whose owner no longer has access show an **Orphaned** badge. ## Related - [Start a workflow run manually](https://www.agentmissioncontrol.ai/guide/start-a-run/) - [Schedule workflows and set up webhooks](https://www.agentmissioncontrol.ai/guide/schedules-and-webhooks/) - [Read the ROI dashboard and export reports](https://www.agentmissioncontrol.ai/guide/roi-dashboard/) --- Source: https://www.agentmissioncontrol.ai/guide/register-agents/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Schedule workflows and set up webhooks Agent Mission Control user guide > Administration > Run workflows on a recurring cadence, or let other systems start them. **Who can do this:** Tenant Admin, Super Admin ## Schedule a workflow 1. Go to **Settings > Scheduled Work** and select **New schedule**. 2. Choose the **Workflow**. 3. Choose a **Frequency**: every minute, every N minutes, hourly, every N hours, once a day, monthly on a day, or **Advanced** (a 5-field cron expression). 4. Set **Active hours** and **Active days** to limit when it fires, and choose a **Timezone**. 5. Check **Next 5 runs (preview)**, then select **Create trigger**. The scheduler checks about every 30 seconds, so very short intervals can fire slightly late. Turn a schedule off with its **Enabled** toggle. ## Expose a workflow as a webhook 1. Go to **Settings > Integrations** and select **New webhook**. 2. Choose the **Workflow**. Turn on **HMAC signature verification** to require signed requests. 3. Select **Create trigger**, then open it again to see its **Endpoint**. 4. Under **Signing keys**, select **Add a key** and name the calling system, for example Salesforce. Select **Create key** and copy it. Give each calling system its own key so every run traces back to its caller. **Rotate** issues a new key and keeps the old one valid for an overlap window. **Revoke** rejects that caller immediately without affecting others. A disabled webhook rejects every request. ## Related - [Start a workflow run manually](https://www.agentmissioncontrol.ai/guide/start-a-run/) - [Register agents and workflows](https://www.agentmissioncontrol.ai/guide/register-agents/) --- Source: https://www.agentmissioncontrol.ai/guide/schedules-and-webhooks/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Set intervention SLAs and upload limits Agent Mission Control user guide > Administration > Control how long people have to respond, how overdue items escalate, and what files they can attach. **Who can do this:** Tenant Admin (edit). Everyone can view. Go to **Settings > Human-in-the-Loop**. Changes save as you make them. ## SLA and escalation - **Default SLA:** 24 hours by default. Set it in hours (1 to 168) or business days (1 to 10). - An intervention becomes overdue when its SLA runs out from the time it opened. - **Escalation tiers** (all on by default): at 1x SLA re-notify the primary responder, at 2x notify the department fallback admin, at 4x notify the org admin. - To give one workflow a different SLA, open it under **Agents > Manage** and set an SLA override. ## File-upload limits | Setting | Default | Range | | --- | --- | --- | | **Max file size (MB)** | 25 | 1 to 100 | | **Max files per message** | 5 | 1 to 10 | | **Attachment retention (days)** | 30 | 1 to 3650 | | **Allowed file types** | PNG, JPG, GIF, PDF, DOCX, XLSX, PPTX, TXT, CSV, JSON | At least one | Every upload is scanned for malware before anyone can see it. Limits apply to every workflow. ## Related - [Respond to a human intervention](https://www.agentmissioncontrol.ai/guide/respond-to-interventions/) - [Set up email notifications (SMTP)](https://www.agentmissioncontrol.ai/guide/email-setup/) --- Source: https://www.agentmissioncontrol.ai/guide/intervention-settings/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Check your version and request updates Agent Mission Control user guide > Administration > Find the release you're running and ask for a newer one. **Who can do this:** Any assigned role (version). Super Admin (updates). Go to **Settings > System**. **Version** shows the release this deployment runs. Quote it when you contact support. Older installations may not report a version; that's expected. ## Request an update 1. In **Settings > System**, check **Updates**. It says whether a newer release is available and links to its release notes. 2. Select **Request update**. 3. AMC sends the request to your publisher, who performs the upgrade. The page shows when it's in progress and when it's done. If a release requires an earlier version first, AMC tells you which. If the request couldn't reach the publisher, select **Retry notification**. ## Related - [Troubleshooting and FAQ](https://www.agentmissioncontrol.ai/guide/troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/guide/system-and-updates/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Troubleshooting and FAQ Agent Mission Control user guide > Troubleshooting > What sign-in messages mean and answers to the questions people ask most. **Who can do this:** Everyone ## Sign-in messages | Message says | What to do | | --- | --- | | Your session expired | Sign in again. | | We couldn't restore your session | Check your connection, then sign in again. | | Your identity provider hasn't verified your email | Verify your email with your identity provider, then sign in. | | This account has been deactivated | Ask your administrator to reactivate you. | | Sign-in isn't set up for this organisation | Check the address you used, or ask your administrator. | | This email address already belongs to another account | Ask your administrator to fix the duplicate. | | Sign-in didn't complete | Try again. If it keeps happening, contact your administrator. | ## Common questions | Question | Answer | | --- | --- | | I signed in but see almost nothing. | You have no department yet. Ask a Tenant Admin to add you to one and grant you a role. | | I can't see Governance or Agents. | Your role doesn't include them, or your organization has no departments yet. See [Roles and permissions](https://www.agentmissioncontrol.ai/guide/roles-and-permissions/). | | My profile says Admin but I can't do admin things. | The profile role is a label. Ask for a governance role. | | I don't get any email. | Email is off until an admin connects it under **Settings > Notifications**. Also check your [notification preferences](https://www.agentmissioncontrol.ai/guide/notification-preferences/). | | Can I pause or restart a run? | No. You can cancel a run. A run waiting on a person resumes when someone replies. | | Why can't I reply to an intervention? | Replying needs the Department Manager role for that department, or Tenant Admin. Withdrawn or completed threads can't take replies. | | Run now is grayed out. | You have no enabled workflow types you're allowed to run in your departments. | | My run was blocked by budget. | The department hit its cap. [Request a budget override](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/). | | A registration says Access not granted. | Grant AMC's managed identity a role on the resource, then save it again. | ## Still stuck Contact your AMC administrator first. Admins can reach the AMC team with the version number from **Settings > System**. ## Related - [Get started with Agent Mission Control](https://www.agentmissioncontrol.ai/guide/get-started/) - [Roles and permissions](https://www.agentmissioncontrol.ai/guide/roles-and-permissions/) --- Source: https://www.agentmissioncontrol.ai/guide/troubleshooting/ · Last updated 2026-10-21 · Full guide index: https://www.agentmissioncontrol.ai/llms.txt # Agent Mission Control developer docs > How to build an agent or workflow and connect it to Agent Mission Control (AMC): the integration contract, registration, and four build paths. # How agents connect to AMC Agent Mission Control developer docs > Overview > How AMC starts, observes, governs and meters a run on your agent, and the terms the rest of these docs use. **Before you start:** None. Read this first. AMC does not run your agent's logic. Your agent runs where you deploy it: in Microsoft Foundry, or behind your own HTTP endpoint. AMC holds a **registration** that says where the agent lives and how to authenticate to it. It then starts runs on it, watches them, routes its questions to people, and prices what each run cost. Everything AMC does to an agent goes through an **adapter**. An adapter translates AMC's run model into one orchestrator's protocol. There are two you can register against: | Adapter | Key | Talks to | Used by | | --- | --- | --- | --- | | **Azure AI Foundry** | `foundry` | A Foundry hosted agent or workflow, over the Foundry Responses protocol (streamed). | [Claude skill on Microsoft Foundry](https://www.agentmissioncontrol.ai/developers/foundry-hybrid-overview/), [Microsoft Agent Framework](https://www.agentmissioncontrol.ai/developers/af-overview/) | | **Flight Director / custom** | `flight-director` | Any HTTP service that implements AMC's run contract: JSON over HTTP plus a Server-Sent Events stream. | [Bring your own agent](https://www.agentmissioncontrol.ai/developers/byo-overview/), [Claude skill, self-hosted](https://www.agentmissioncontrol.ai/developers/self-hosted-overview/) | ## Key terms | Term | Meaning | | --- | --- | | **Workflow** / **Agent** | The two registration kinds (`workflow`, `agent`). Both run the same way. The kind decides who may register it: an Agent Manager can register only agents. Kind can't change after registration. | | **Registration** | AMC's record of one deployed resource: name, kind, adapter, department, endpoint, authentication, input contract and declared steps. AMC adopts resources that already exist. It never deploys or changes your agent. | | **Run** | One execution of a registered resource. Your side issues a run id (`orchestratorRunId`); AMC keeps its own run record keyed to it. | | **Step** | One unit of progress inside a run, with its own status, tokens, model and content. Steps are what people see on the run page. | | **Declared steps** | The step plan you register up front (the golden path). AMC shows every declared step as pending from the moment a run starts, then swaps each for the real step as it reports. | | **Input contract** | The parameters a run accepts. AMC validates every manual, scheduled and webhook start against it before calling your agent. | | **Intervention** | A question your agent asks a person mid-run. The run waits at that step until someone replies in AMC. | | **Assignee** | The person a run (and its interventions) belongs to. Set per workflow, per run at start, from an input marked as the assignee, or by the agent itself. | | **Outcome** | The verdict on a finished run: `success`, `partial`, `failure` or `cancelled`. `partial` is a completed run whose agent reported that part of the work wasn't done; AMC shows it as **Completed with issues**. Separate from status, and an admin can change it. | | **Golden run** | A completed run someone trusted and promoted as the reference. Failure diagnosis compares failed runs against it step by step. See [Set and retire golden runs](https://www.agentmissioncontrol.ai/guide/golden-runs/). | | **Budget** | A department's spending cap. AMC checks it before every run starts and blocks the start when the cap is reached. See [budgets and overrides](https://www.agentmissioncontrol.ai/guide/budgets-and-overrides/). | ## The run lifecycle `What happens on one run` ```text Trigger: Run now | schedule | webhook | v AMC API 1. validate inputs against the input contract (400 on a bad input) 2. check the workflow type is enabled (409 if it is switched off) 3. pre-run budget gate (blocked at the cap) 4. adapter.StartRun ------------------------------> your agent POST /runs (or Foundry Responses) 5. store the run as "running" <----------- { orchestratorRunId, status } Status sync (every 60 s by default, per run) 6. GET /runs/{id}/status and GET /runs/{id}/tokens - steps and current step -> run page - status human_intervention -> Intervention: Review queue, bell, email - tokens and cost -> ROI, department budget spend - completed | failed | cancelled -> outcome, failure reason, notifications On demand 7. someone watches the run -> GET /runs/{id}/events (SSE, live view only) 8. someone opens a step -> GET /runs/{id}/steps/{stepId}/content 9. someone answers -> POST /runs/{id}/messages 10. someone cancels -> POST /runs/{id}/cancel ``` The paths in this diagram are the custom adapter's. The Foundry adapter does the same jobs over the Foundry Responses protocol, reading status, steps and tokens from the streamed response and its markers. ## Invoke A run starts from **Run now**, a schedule, or a webhook (see [Schedule workflows and set up webhooks](https://www.agentmissioncontrol.ai/guide/schedules-and-webhooks/)). Whatever the trigger, AMC sends your agent the same thing: the resource id and a flat map of string inputs. A rejected start (bad input, disabled type, budget block) never reaches your agent and leaves no run behind. ## Observe AMC's status sync polls every live run. That poll is the source of truth: it records status, steps, tokens, cost, the assignee, the run's name and why it failed. The SSE stream only drives the live view for someone watching the run, so an agent with no working stream still records correctly, one poll behind. AMC does not store step content. The step list and each step's content are read through to your agent when someone opens the run, so keep finished runs available on your side. ## Govern - **Departments and roles:** every registration belongs to a department, and only people with access to it see its runs. See [Roles and permissions](https://www.agentmissioncontrol.ai/guide/roles-and-permissions/). - **Interventions:** when your agent reports `human_intervention`, AMC opens an intervention, routes it to the run's assignee, tracks its SLA, and delivers the reply back to your agent. - **Golden runs and failure review:** failed runs carry the reason your agent reported and are compared against the golden run. - **Sensitive resources:** a registration marked sensitive hides its runs from people without a grant. - **Audit:** registrations, runs, replies and overrides are written to the audit log. ## Meter Your agent reports tokens per step, split into input and output, with the model that produced them. AMC prices each run from that report, or uses the cost your agent computed itself when it reports one. The cost feeds the **ROI** views and the department budget the next run is checked against. See [Report tokens and cost](https://www.agentmissioncontrol.ai/developers/byo-tokens-and-cost/). > **Note:** Next: [Choose a build path](https://www.agentmissioncontrol.ai/developers/choose-a-build-path/), then check your design against [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/). ## Related - [Choose a build path](https://www.agentmissioncontrol.ai/developers/choose-a-build-path/) - [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) - [Register your agent: the technical reference](https://www.agentmissioncontrol.ai/developers/register-your-agent/) - [Bring your own agent](https://www.agentmissioncontrol.ai/developers/byo-overview/) --- Source: https://www.agentmissioncontrol.ai/developers/how-agents-connect/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Choose a build path Agent Mission Control developer docs > Overview > Compare the four ways to build an agent for AMC and pick the one that fits what you already have. **Before you start:** Read [How agents connect](/developers/how-agents-connect/) first. Every path ends at the same place: a registration AMC can start, watch and price. They differ in who hosts the agent, what reasons inside it, and which adapter AMC uses. | | Bring your own agent | Claude skill on Microsoft Foundry | Claude skill, self-hosted | Microsoft Agent Framework | | --- | --- | --- | --- | --- | | **What you build** | Any agent or workflow, in any language, behind an HTTP endpoint you implement | A Foundry hosted agent: Agent Framework as the host shell, `claude-agent-sdk` running your skill | A container that runs your skill with `claude-agent-sdk` and serves AMC's contract with `amc_sdk` | An Agent Framework agent or multi-agent workflow, deployed to Foundry as a hosted agent | | **What reasons** | Whatever you choose | Claude, through your `SKILL.md` | Claude, through your `SKILL.md` | Agent Framework agents, typically on GPT models | | **Who hosts it** | You | Microsoft Foundry | You | Microsoft Foundry | | **AMC adapter** | **Flight Director / custom** | **Azure AI Foundry** | **Flight Director / custom**, with **This resource uses the AMC SDK** | **Azure AI Foundry** | | **How it reports to AMC** | You implement the run endpoints directly | Informational markers in the Responses stream | `amc_sdk` serves the endpoints for you | Informational markers in the Responses stream | | **Auth AMC uses** | API key, or managed identity on an approved https host | Managed identity or a Foundry key | API key, or managed identity on an approved https host | Managed identity or a Foundry key | | **Start here** | [Bring your own agent](https://www.agentmissioncontrol.ai/developers/byo-overview/) | [Foundry overview](https://www.agentmissioncontrol.ai/developers/foundry-hybrid-overview/) | [Self-hosted overview](https://www.agentmissioncontrol.ai/developers/self-hosted-overview/) | [Agent Framework overview](https://www.agentmissioncontrol.ai/developers/af-overview/) | ## When to pick each - **Bring your own agent:** you already have an agent or workflow (LangGraph, a .NET service, a script, another vendor's platform) and want AMC to run and govern it without rewriting it. You own the endpoint and implement the contract. In Python, [the amc_sdk package](https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/) does most of it for you. - **Claude skill on Microsoft Foundry:** you have a Claude skill and want it to run unattended as a first-class Foundry resource, with Foundry owning the endpoint, sessions, scale and Entra auth. Pick this when your organization standardizes on Foundry. - **Claude skill, self-hosted:** you have a Claude skill and want to run it in your own container, on your own network, without Foundry. Pick this for full control of the runtime or when Foundry hosting isn't available to you. - **Microsoft Agent Framework:** you are building new agents or a multi-agent graph (sequential, concurrent, handoff, group chat) on Agent Framework, usually on Azure OpenAI models, and will host them in Foundry. > **Note:** Not sure? If the agent exists, bring your own. If you are starting from a Claude skill, pick Foundry when Foundry is your platform and self-hosted otherwise. If you are starting from scratch on Microsoft's stack, pick Agent Framework. Whichever you pick, the agent must meet [the integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/). ## Related - [How agents connect to AMC](https://www.agentmissioncontrol.ai/developers/how-agents-connect/) - [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) - [Bring your own agent](https://www.agentmissioncontrol.ai/developers/byo-overview/) --- Source: https://www.agentmissioncontrol.ai/developers/choose-a-build-path/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # The integration contract Agent Mission Control developer docs > Overview > The checklist every agent meets, whatever the build path, so AMC can start, watch, govern and price it. **Before you start:** Read [How agents connect](/developers/how-agents-connect/) first. These are the obligations AMC places on any agent. On the custom adapter you meet each one with an HTTP endpoint. On Foundry you meet the same obligations through the Responses stream and its markers; each Foundry section shows how. | # | Obligation | What AMC needs | Detail | | --- | --- | --- | --- | | 1 | **Endpoint** | One absolute `http(s)` base URL. AMC calls fixed paths under it (`/health`, `/runs`, `/runs/{id}/...`). | [Endpoint and authentication](https://www.agentmissioncontrol.ai/developers/byo-endpoint-and-auth/) | | 2 | **Authentication** | Accept the credential AMC presents: an `api-key` header (key mode) or an Entra bearer token for your app's `api://` audience (managed identity). | [Endpoint and authentication](https://www.agentmissioncontrol.ai/developers/byo-endpoint-and-auth/) | | 3 | **Health check** | `GET /health` answers `2xx` when ready, `401`/`403` when the credential is wrong. AMC calls it when you register or change the endpoint or auth. | [Register your agent](https://www.agentmissioncontrol.ai/developers/register-your-agent/) | | 4 | **Invocation** | `POST /runs` accepts `{ orchestratorWorkflowId, versionId, inputs, idempotencyKey }`, starts the work in the background, and returns a run id at once. | [Start and cancel a run](https://www.agentmissioncontrol.ai/developers/byo-start-a-run/) | | 5 | **Input contract** | Declare the run parameters at registration. Read them from the flat `inputs` string map. | [Declare run parameters](https://www.agentmissioncontrol.ai/developers/byo-input-contract/) | | 6 | **Run status** | `GET /runs/{id}/status` returns the run status (`running`, `human_intervention`, `completed`, `failed`, `cancelled`) and every step. A `completed` run can add `outcome: "partial"` to report that part of the work wasn't done. | [Run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/) | | 7 | **Steps** | Each step has a stable `stepId`, a status, timestamps, and content readable at `GET /runs/{id}/steps/{stepId}/content`. | [Run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/) | | 8 | **Token and cost reporting** | Per-step `inputTokens`, `outputTokens`, `cachedInputTokens` and `model`; run totals at `GET /runs/{id}/tokens`, with `costUsd` if you price it yourself. | [Report tokens and cost](https://www.agentmissioncontrol.ai/developers/byo-tokens-and-cost/) | | 9 | **Human in the loop** | To ask a person, report run status `human_intervention` with the question on the waiting step, and accept the answer at `POST /runs/{id}/messages`. | [Ask a person mid-run](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/) | | 10 | **Failure** | A failed run ends with status `failed` and a `failureReason` people can act on. Spend accrued before the failure is still reported. | [Errors and timeouts](https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/) | | 11 | **Cancel** | `POST /runs/{id}/cancel` stops the run and reports `cancelled`. | [Start and cancel a run](https://www.agentmissioncontrol.ai/developers/byo-start-a-run/) | | 12 | **Live events** (recommended) | `GET /runs/{id}/events` streams typed events as SSE for people watching the run. | [Run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/) | ## Rules that cause silent errors - **Use camelCase JSON keys exactly as documented.** AMC reads them case-insensitively, but a misspelled key reads as empty, with no error. - **Report the input/output split and the model.** With only `tokenCount` and no `costUsd`, ROI falls back to a flat per-token estimate. A step with no `model`, or a model AMC has no rate for, prices at AMC's default rate. - **Don't report a status AMC doesn't know.** An unknown run status is treated as `waiting`, which the run page shows as **Intervention**. - **Keep run state after the run ends.** AMC reads step lists and step content from your agent whenever someone opens the run. > **Note:** The fastest way to meet items 4 to 12 in Python is [the amc_sdk package](https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/), which serves the whole contract around your run function. ## Related - [Bring your own agent](https://www.agentmissioncontrol.ai/developers/byo-overview/) - [Register your agent: the technical reference](https://www.agentmissioncontrol.ai/developers/register-your-agent/) - [Run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/) - [The amc_sdk Python package](https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/) --- Source: https://www.agentmissioncontrol.ai/developers/integration-contract/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Register your agent: the technical reference Agent Mission Control developer docs > Overview > Every registration field, how AMC derives the resource id, what the health check does, and how Foundry import works. **Before you start:** A deployed, reachable agent. Tenant Admin, Department Manager, or Agent Manager (agents only). Registration adopts an agent that is already deployed. AMC stores metadata about it, checks it is reachable, and never pushes code or a definition to it. For the step-by-step UI walkthrough, see [Register agents and workflows](https://www.agentmissioncontrol.ai/guide/register-agents/). This page covers what each field does underneath. ## Fields | UI label | API field | What it does | | --- | --- | --- | | **Name** | `name` | Required. The display name people see on the **Agents** page and on runs. | | **Kind** | `kind` | `workflow` or `agent`. Immutable. An Agent Manager can register only `agent`. | | **Adapter** | `adapter` | `foundry` (**Azure AI Foundry**) or `flight-director` (**Flight Director / custom**). Immutable. | | **Department** | `departmentId` | Required in the form. Scopes who can see and run it. Must be a department you can write to. | | **This resource uses the AMC SDK** | `usesAmcSdk` | Custom adapter only. Lets you register without path segments. | | **Endpoint URL** | `endpoint` | Required. The base URL. For Foundry, the agent URL (it contains `/agents/`). For custom, the URL AMC appends `/health`, `/runs` and so on to. | | **Path segments** | `pathSegments` | Custom adapter only, required unless you use the AMC SDK. Joined with `/` to form the resource id. They don't change the URLs AMC calls. | | **Authentication** | `authMode` | `same-tenant-mi` (**Managed Identity (same tenant)**) or `key` (**API key**). | | **Customer tenant id** | `customerTenantId` | Optional, managed identity only. Stored with the registration. | | **Resource scope** | `resourceScope` | Managed identity only. The audience AMC requests a token for. Required for custom endpoints (an `api:///.default` scope). | | **API key** | `key` | Key mode only. Write-only: stored in Key Vault, never returned, presented as the `api-key` header. | | **Mark this resource as sensitive** | `isSensitive` | Hides runs from people without a grant to the resource. | | **Allow this resource's artifacts to start runs** | `artifactsMayStartRuns` | Off by default. Lets HTML artifacts this resource produces start runs, as the viewer, after the viewer confirms. | | **Artifact retention (days)** | `artifactRetentionDays` | Optional. Blank uses the tenant default. | | **Input contract** | `inputContract` | Optional. The parameters a run accepts. See [Declare run parameters](https://www.agentmissioncontrol.ai/developers/byo-input-contract/). | | **Declared steps** | `declaredSteps` | Optional. The step plan, in order: `key`, `name`, `description`. Each `key` should equal the `stepId` your agent reports. | ## The resource id AMC derives a stable resource id (`orchestratorWorkflowId`) from the registration and sends it as `orchestratorWorkflowId` on every `POST /runs`. | Registration | Resource id | | --- | --- | | Foundry | The agent name: the path segment after `/agents/` in the endpoint URL. | | Custom, with path segments | The segments joined with `/`, for example `finance/invoice-summarizer`. | | Custom, no path segments (AMC SDK) | The endpoint's lowercase host and path, trailing slash removed, for example `invoices.contoso.com/amc`. | A tenant can register a resource once. The uniqueness key is the adapter, the endpoint host (with its port when it isn't the default), and the resource id. A second registration of the same resource returns `409 already_registered`. ## The health check On register, and on any update that changes the endpoint, path segments, the SDK flag, or authentication, AMC checks the resource before it saves anything. A key is checked inline first, so a rejected key is never stored. | Adapter | Check | Result | | --- | --- | --- | | Custom | `GET /health` with the registration's credential. One attempt, 5 second timeout, redirects not followed. | `2xx`: **Healthy**. `401` or `403`: **Access not granted**. Any other status (including `503`), a timeout, or a connection failure: **Unreachable**. | | Foundry | A catalog lookup of the agent in its Foundry project. Foundry endpoints have no `/health`. | Found: **Healthy**. Auth failure: **Access not granted**. Anything else: **Unreachable**. | A failed check returns `422` with `error` set to `unreachable` or `access_not_granted`, and nothing is saved. The check runs only at registration time: AMC does not re-probe on a schedule. ## Managed identity rules - **Foundry:** leave **Resource scope** blank (AMC uses `https://ai.azure.com/.default`) or set `https://cognitiveservices.azure.com/.default`. Grant AMC's identity a least-privilege role on the project first. - **Custom endpoints:** only a Tenant Admin or Super Admin can register one with managed identity (others get `403 managed_identity_requires_tenant_admin`). The endpoint must be `https`, the scope must be an `api://` application ID URI, and the installation must have approved that host and audience pair. Otherwise use an API key. See [Endpoint and authentication](https://www.agentmissioncontrol.ai/developers/byo-endpoint-and-auth/). ## Import from Foundry **Import from Foundry** registers every agent and workflow in one Foundry project at once. You supply the project endpoint (not an agent URL), one department for all of them, and the authentication AMC uses to list and later run the project. AMC lists the project's catalog, and that listing is the access check. - Each new resource is created with source **Auto-discovered**. Its kind comes from the catalog type: types ending in `-agent` become agents, the rest workflows. - Re-running is safe. Resources already registered keep their department, sensitivity and owner; only their catalog type is refreshed. Nothing is removed for agents that left the project. - Import needs workflow-level rights in the department, so an Agent Manager can't import. ## Update, deactivate, delete - Kind and adapter can't change. Changing the endpoint re-runs the health check; in key mode, a changed endpoint needs the key again. - Leaving the input contract or declared steps out of an update keeps the stored ones. - **Deactivate** archives the resource and deletes its stored key. **Delete** removes it with all its run history. Both are refused with `409 run_in_progress` while a run depends on it. ## The registration API The **Agents** page is a client of these endpoints. They need a signed-in user with the matching permission. | Method and path | Purpose | | --- | --- | | `GET /api/adapters` | The adapter catalog: each adapter's required inputs and supported auth modes. | | `POST /api/registrations` | Register. Returns `201` with the registration and `health: "healthy"`. | | `PUT /api/registrations/{id}` | Update. Re-checks health when the endpoint or auth changed. | | `POST /api/registrations/import` | Import a Foundry project. | | `POST /api/registrations/{id}/deactivate` | Deactivate. | | `POST /api/registrations/{id}/reassign-owner` | Reassign the accountable owner. | | `DELETE /api/registrations/{id}` | Delete. | `POST /api/registrations (custom adapter, key mode)` ```json { "name": "Invoice Summarizer", "kind": "workflow", "adapter": "flight-director", "departmentId": "finance", "endpoint": "https://invoices.contoso.com/amc", "usesAmcSdk": true, "authMode": "key", "key": "", "isSensitive": false, "inputContract": { "parameters": [ { "name": "invoiceId", "type": "string", "required": true, "description": "Invoice number" }, { "name": "ownerEmail", "type": "string", "required": false, "role": "assignee" } ] }, "declaredSteps": [ { "key": "fetch", "name": "Fetch invoice", "order": 1 }, { "key": "summarize", "name": "Summarize", "order": 2 } ] } ``` > **Note:** The `role` field on an input parameter is accepted by the API but not offered in the registration form. Set it through the API when you need AMC to route a run to the person named in an input. See [Declare run parameters](https://www.agentmissioncontrol.ai/developers/byo-input-contract/). ## Related - [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) - [Endpoint and authentication](https://www.agentmissioncontrol.ai/developers/byo-endpoint-and-auth/) - [Declare run parameters](https://www.agentmissioncontrol.ai/developers/byo-input-contract/) - [How agents connect to AMC](https://www.agentmissioncontrol.ai/developers/how-agents-connect/) --- Source: https://www.agentmissioncontrol.ai/developers/register-your-agent/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Deploy Agent Mission Control Agent Mission Control developer docs > Deploy AMC > Install AMC in your own Azure subscription: prepare Entra ID, deploy the managed application, finish post-deployment setup, and verify it. **Before you start:** Owner or Contributor on the target subscription or resource group, and Application Developer (or higher) in Microsoft Entra ID. > **Note:** Prefer a file? [Download the deployment guide as Markdown](https://www.agentmissioncontrol.ai/downloads/amc-deployment-guide.md) (v4, release 2026.10.16). AMC is delivered as an Azure **managed application**. Deploying it creates a dedicated, single-tenant instance inside your own subscription, and your data never leaves it. **Applies to:** release **2026.10.16** (October 2026). Release versions use the date-based format `YYYY.MM.DD`; this release carries the same code as the earlier `0.0.25` build. **Time required:** about 15 minutes of your attention, plus roughly 25 minutes of unattended deployment. > **Important:** Two required steps can only be done **after** the deployment finishes, because they need the instance's URL, which Azure generates during it. Plan to return to the Azure portal once the deployment succeeds. Nobody can sign in until you do. ## Before you begin ### Permissions | Where | What you need | Used for | | --- | --- | --- | | Target Azure subscription | **Owner**, or **Contributor** on the resource group where the instance is created | Creating the managed application | | Microsoft Entra ID | **Application Developer** or higher (or an existing registration prepared for you) | Registering the sign-in application | If your organization separates these duties, a directory administrator can do the Entra work (Steps 1 and 3). Only Step 2 needs subscription permissions. ### Resource providers The deployment fails early with `MissingSubscriptionRegistration` if any of these providers isn't registered in the target subscription. Most subscriptions already have them: `Microsoft.Solutions` · `Microsoft.App` · `Microsoft.ContainerInstance` · `Microsoft.Sql` · `Microsoft.Cache` · `Microsoft.KeyVault` · `Microsoft.ServiceBus` · `Microsoft.OperationalInsights` · `Microsoft.Insights` · `Microsoft.Storage` · `Microsoft.Network` · `Microsoft.ManagedIdentity` · `Microsoft.Security` One more provider, `Microsoft.EventGrid`, isn't used by the deployment itself but by malware scanning of uploaded files (Microsoft Defender for Storage), which creates an Event Grid system topic next to the file storage account. The deployment succeeds without it; scanning doesn't work. Register it before you deploy, and read the [known issue](#known-issue-malware-scanning-of-uploaded-files) below. `Register a provider` ```bash az provider register --namespace Microsoft.ContainerInstance ``` ### Choosing a region Pick a region that supports **Azure Container Apps**, **Azure SQL Database** and **Azure Managed Redis**. Managed Redis is the most restricted, so check it first. This release is validated in **West US 2**. > **Note:** The instance hosts no email infrastructure. Notification email goes through each tenant's own SMTP service, connected after deployment ([Step 6](#step-6-connect-email)). That service, not the region you choose here, decides where email data is stored and processed. ### What gets created Everything below lives in a **managed resource group** that Azure creates alongside the instance. You can view these resources, but the group is locked for writes: the application owns its own infrastructure. | Component | Notes | | --- | --- | | 8 container apps | `api`, `client`, `streaming`, `run-executor`, and four background workers | | Container Apps environment | VNet-injected; only `client` has a public ingress | | Azure SQL Database | Standard S1 (20 DTU), private endpoint | | Azure Managed Redis | Balanced B1, TLS 1.2, no public access | | Key Vault | Purge protection enabled, private endpoint | | Service Bus namespace | One queue and one topic | | Log Analytics workspace | Application and platform logs | | 2 storage accounts | One for the deployment's own files, one for files users and agents upload (HITL attachments, run artifacts). The second is configured for **Microsoft Defender for Storage** on-upload malware scanning; a file is released only after a clean verdict. See the known issue below | | Virtual network, 5 private endpoints, 8 managed identities | Supporting infrastructure | ### Known issue: malware scanning of uploaded files In release 2026.10.16, on-upload malware scanning may not activate on a managed instance even though the deployment reports success. The setting is written, but Defender's own follow-up (its scanner role and Event Grid topic inside the managed resource group) doesn't always complete, and the setting then reads back as disabled. On an affected instance every uploaded file stays in **Scanning…** and can't be opened or downloaded; human-in-the-loop requests, runs and everything else work normally. We've seen this on upgraded instances and haven't yet verified a fresh installation either way. If you hit it, contact the publisher with the instance's name and managed resource group. Re-enabling scanning is done on our side, and the fix for the deployment template is tracked. Registering `Microsoft.EventGrid` beforehand is a prerequisite for the fix, not a fix by itself. > **Note:** As the publisher, we receive **Owner** access to the managed resource group so we can support and upgrade the instance. We have no access to your subscription outside that group. Review the exact grant any time on the managed application's **Authorizations** blade. ## Step 1: Register the sign-in application in Microsoft Entra ID AMC signs your users in with your own Entra directory. It never stores passwords and never sees your users' Entra tokens beyond the sign-in exchange. Create the registration in advance (recommended); the inline option on the deployment form doesn't expose every setting. 1. In the Azure portal, go to **Microsoft Entra ID > App registrations > New registration**. 2. Enter a name, for example `Agent Mission Control`. 3. Under **Supported account types**, select **Accounts in this organizational directory only (single tenant)**. The portal preselects a multi-tenant option; change it. 4. Leave **Redirect URI** empty and select **Register**. You add redirect URIs in Step 3. 5. On **Overview**, copy the **Application (client) ID**. You need it in Step 2. > **Important:** AMC is a single-organization application. A multi-tenant registration invites sign-in attempts from directories you don't control. ### Create a client secret 1. Go to **Certificates & secrets > Client secrets > New client secret**. 2. Enter a description and an expiry. We recommend 12 months. 3. Select **Add**, then copy the secret **Value** immediately. Entra shows it only once. > **Important:** Put the expiry date in your calendar. When the secret expires, users can no longer sign in, and the rotation procedure isn't published yet. See [Rotating the client secret](#rotating-the-client-secret). ### Add the email optional claim AMC identifies users by their Entra object ID and displays their email address. Work accounts don't receive an `email` claim by default. 1. Go to **Token configuration > Add optional claim**. 2. Select **ID**, then **email**, then **Add**. If you skip this, users can still sign in, but their display email is derived from their user principal name, which isn't always their real address. ## Step 2: Deploy the managed application 1. In the Azure portal, open the AMC offer and select **Create**. 2. Complete the wizard using the tables below. 3. On **Review + create**, select **Create**. ### Basics | Field | Guidance | | --- | --- | | Subscription | The subscription that owns and pays for the instance | | Resource group | Where the managed application *object* lives. The resources go elsewhere | | Region | See [Choosing a region](#choosing-a-region) | | Application name | A name for this instance, for example `amc-production` | | Managed resource group | **Accept the generated name.** See the warning below | > **Important:** Never reuse a managed resource group name from a previous deployment, even a deleted one. Resource names derive from it, and a deleted Key Vault stays recoverable for up to 90 days, so a reused name collides with your earlier deployment and the new one fails. ### Identity & Tenant | Field | Required | Guidance | | --- | --- | --- | | Tenant name | Yes | Your organization's display name inside AMC, for example `Contoso, Ltd.` | | Sign-in application | Yes | Select **Select existing** and choose the registration from Step 1 | | Client secret | Yes | The secret value from Step 1 | | Super Admin Entra object ID | No | See [Choosing how to claim the Super Admin role](#choosing-how-to-claim-the-super-admin-role) | | Super Admin email | No | Display cross-check only; ignored if the object ID is blank | | Sign-in directory (tenant) ID override | No | Under **Advanced**. Leave blank unless your users sign in from a different Entra directory than the one that owns this subscription | > **Note:** The application picker is labelled **service principal** and lists every service principal in your directory, including Microsoft's first-party apps. Match on the **Application (client) ID** from Step 1. If you choose **Create new** instead, set the new registration to single tenant and create its client secret in the blade the portal opens. ### Choosing how to claim the Super Admin role Every installation needs one first administrator. Fill in the Super Admin object ID, or leave it blank: | Approach | How it works | When to use it | | --- | --- | --- | | **Nominate a user** (enter their Entra object ID) | That person becomes Super Admin automatically at first sign-in | You already know who administers AMC | | **Use a one-time setup code** (leave both fields blank) | The deployment outputs a setup code. The app asks for it at first sign-in, and whoever enters it becomes Super Admin | You don't know the administrator yet, or a different person deploys than administers | > **Important:** The setup code is a credential: anyone who can read the deployment outputs can read it. **Claim the role promptly.** The code stops working the moment the role is assigned; the stored secret is deleted and never reissued. Until then it's live, so nominate a Super Admin by object ID when you can. That issues no code at all. ### Start the deployment 1. Select **Review + create**. The portal validates your entries and reports any field it can't accept. 2. Select **Create**. Deployment takes about 25 minutes, mostly the database, the cache and the Container Apps environment. The application containers start last, after a one-time setup task applies the database schema, seeds your tenant and creates the instance's signing key. You can close the portal; the deployment continues. To follow it, open your resource group and select **Deployments**, or watch the managed application's **Overview**. When the provisioning state reaches **Succeeded**, continue with Step 3. > **Note:** A successful deployment isn't a usable instance yet: sign-in fails until you finish Step 3. That's expected. If the deployment fails, there's nothing to repair in place. Delete the managed application, fix the cause (see [Troubleshooting](#troubleshooting)), and deploy again with a **new** managed resource group name. ## Step 3: Register the redirect URIs 1. When the deployment succeeds, open the managed application and select **Settings > Parameters and Outputs**. 2. Copy the values of **clientUrl** and **entraRedirectUri** (or read them with the CLI command below). 3. Open your app registration from Step 1 and select **Authentication**. 4. Under **Platform configurations**, select **Add a platform > Web** (or edit the existing Web platform). 5. Add **both** URIs from the table below, then select **Save**. `Read the URLs with the Azure CLI` ```bash az managedapp show --resource-group --name \ --query "properties.outputs.{clientUrl:clientUrl.value, redirect:entraRedirectUri.value}" ``` | URI | Purpose | | --- | --- | | `https://client./api/auth/callback` (the `entraRedirectUri` output) | Signing in | | `https://client.` (the `clientUrl` output) | Signing out | > **Important:** Register both. Entra validates the sign-out destination against the same list, so an instance with only the first URI signs people in and then fails to sign them out. ## Step 4: First sign-in 1. Browse to the **clientUrl** value. 2. Sign in with an Entra account from your directory. - **You nominated a Super Admin:** the role is assigned automatically when that person signs in. Others can sign in too, but hold no permissions until an administrator grants them. - **You left the fields blank:** the app shows **Finish setting up this installation** and asks for the setup code. Read **superAdminBootstrapCode** from **Parameters and Outputs** (or with the command below), enter it, and select **Claim Super Admin**. `Read the setup code` ```bash az managedapp show --resource-group --name \ --query "properties.outputs.superAdminBootstrapCode.value" -o tsv ``` The role is granted immediately and the code is **destroyed**: the deployment's copy stops working and the stored secret is deleted. It can't be claimed again, including after you later remove that administrator. An installation left with no Super Admin is recovered with the publisher's help: a redeployment into the same managed resource group issues no new code, and a new deployment starts with an empty database. ## Step 5: Verify the deployment | Check | Where | Expected | | --- | --- | --- | | Deployment succeeded | Managed application > **Overview** | Provisioning state **Succeeded** | | Post-deployment tasks completed | **Parameters and Outputs** > `postdeployStatus` | `migrationApplied`, `tenantSeeded`, `signingKeyCreated` all `true`, and `usersCreated` greater than zero | | Application responds | `clientUrl` in a browser | The sign-in screen, not an error page | | Sign-in works | Sign in with an Entra account | You reach the application | | Administrator established | Your profile inside AMC | Your account holds **Super Admin** | | Release version | **Settings > System** | The release you installed (for example `2026.10.16`); a Super Admin also sees the update section there | ## Step 6: Connect email AMC sends no email until a tenant connects its own SMTP service in **Settings > Notifications**. A Tenant Admin configures their own tenant; a Super Admin can configure any tenant by signing in to it. Until then, that tenant's notifications are logged as `skipped` and its admins see an email-off banner. Everything else works without email. - **Presets:** Exchange Online (OAuth), and Other SMTP server (any service with a username and password, SendGrid included). A preset fills in the server, port, TLS mode and login method; you enter the secret. - **TLS is required:** STARTTLS on port 587 or implicit TLS on port 465. The server needs a public internet address. - **The secret** (password, API key or client secret) is stored in the instance's Key Vault and never shown again. - **Send test email** sends to your own address and shows the result at once. For the in-app steps, see [Set up email notifications (SMTP)](https://www.agentmissioncontrol.ai/guide/email-setup/). ## Troubleshooting | Symptom | Cause | Resolution | | --- | --- | --- | | `AADSTS50011: The redirect URI ... does not match` at sign-in | The sign-in URI is missing or mistyped on the registration | Repeat Step 3. The URI must match `entraRedirectUri` exactly, including `/api/auth/callback` | | Sign-in works, sign-out does nothing or errors | Only one redirect URI is registered | Add the `clientUrl` value as a second Web redirect URI (Step 3) | | `AADSTS7000215: Invalid client secret provided` | The secret doesn't belong to the selected application, or it expired | Confirm the secret was created on the registration whose client ID you supplied. See [Rotating the client secret](#rotating-the-client-secret) | | The setup code is rejected | Already claimed, or mistyped | A code works **exactly once, permanently**. If an administrator exists, just sign in. To recover an installation with no Super Admin, contact the publisher; a redeployment issues no new code. Watch for trailing spaces from copy-paste | | Deployment fails within the first minute | A resource provider isn't registered, or the region lacks a required service | See [Resource providers](#resource-providers) and [Choosing a region](#choosing-a-region) | | Deployment fails with a Key Vault name conflict | The managed resource group name was reused | Redeploy with a new managed resource group name | | Deployment fails on quota | No capacity for Container Apps, SQL or Redis in the region | Request a quota increase, or choose another region | | Uploaded files stay in **Scanning…** | Malware scanning on the file storage account didn't activate. See the [known issue](#known-issue-malware-scanning-of-uploaded-files) | Make sure `Microsoft.EventGrid` is registered in the subscription, then contact your publisher; re-enabling scanning is done on the publisher's side | For a support request, include the managed application's name, its managed resource group name, the deployment's correlation ID, and the `postdeployStatus` output. ## Operate and maintain ### Connecting a workflow or agent The deployment asks nothing about your orchestrator. You connect workflows and agents **inside the application** once it's running, each with its own endpoint and credentials. Register as many as you need, from more than one orchestrator, and change them without redeploying. See [Register your agent](https://www.agentmissioncontrol.ai/developers/register-your-agent/). - **A key you provide:** stored in the instance's Key Vault and presented on every call. Nothing else is required. - **The instance's managed identity:** AMC calls the endpoint as itself, with no shared secret to distribute or rotate. Available for **Microsoft Foundry** endpoints, and it needs one grant on your side. For an endpoint of your own it's **not available on a Marketplace installation**: the installation-wide list of endpoints allowed to receive the managed-identity token is empty and isn't part of the deployment form, so the registration is refused with a message to use an API key instead. Use a key for your own endpoints. ### Granting the instance's identity access For managed-identity authentication, grant the instance's identities access to the resource after you register it. The instance runs under **six identities and all six need the grant**: different parts of AMC call your endpoint (starting, executing and streaming runs, polling status, and starting budget-released and scheduled runs). Granting only one leaves scheduled or executed runs failing with a permission error on an otherwise healthy instance. Collect all six principal IDs from **Parameters and Outputs**. For a **Microsoft Foundry** endpoint, assign the built-in role **Foundry User** (`53ca6127-db72-4b80-b1b0-d745d6d5456d`) to each principal ID at the scope of the Foundry **account**, not the project. > **Important:** A project-only grant isn't enough. AMC reads the project's agent catalog to confirm the resource exists and that it has access, and a project-only grant refuses that read. The symptom is a registration that fails validation with a permissions error even though the endpoint and workflow name are correct. An endpoint of your own can't use the managed identity on a Marketplace installation (see above), so there's nothing to grant for it. ### Rotating the client secret > **Important:** **Not yet verified.** This procedure is still being confirmed and will be published here. Until then, set a reminder well before the expiry you chose in Step 1 and contact us ahead of it. Don't let the secret lapse: users can't sign in once it does. ### Deleting an instance Deleting the managed application deletes its managed resource group and everything in it, including the database. Export anything you need first. The Key Vault stays recoverable for up to 90 days and its name derives from the managed resource group name, so a replacement deployment must use a new name. ## Upgrading an instance An instance upgrades **in place**: the release's deployment template re-runs into the existing managed resource group. Nothing is recreated, the database and managed identities are kept, and the application stays reachable throughout (a proven run takes 5 to 17 minutes). You don't redeploy the managed application or touch `Microsoft.Solutions`. ### Email after an upgrade After upgrading from a release without per-tenant SMTP, email stays off for every tenant until a Tenant Admin or Super Admin configures it in **Settings > Notifications** ([Step 6](#step-6-connect-email)). Nothing else is affected. ### The promotion gate A release reaches instances only after the publisher **promotes** it: a reviewed, human-approved step that moves the release pointer every instance's update check reads. Until then an instance reports no update. Promotion is per release, so once promoted, every instance is offered the same release. ### What the Update button does In **Settings > System**, a Super Admin sees the promoted release and can select **Request update**. AMC sends a signed request to the publisher, which verifies it came from your instance and queues it. The request moves from `Requested` to `InProgress` when the publisher's upgrade executor picks it up, and ends as `Applied ` or `Failed `. The executor runs in the publisher's own subscription under a managed identity, and acts only on an instance's own verified request for a release a human has already promoted. ### What the executor refuses, and why The executor stops and records a one-word reason rather than guess. A refusal never changes the instance, every outcome shows in **Settings > System**, and every start is logged on the publisher side. | Reason | What it means | What happens next | | --- | --- | --- | | `not_verified` | The request's signature didn't verify | It stays in the publisher's triage queue; contact your publisher | | `not_promoted` | The requested version is no longer the promoted one | Ask your publisher; a later promotion supersedes it | | `already_at_or_above` | The instance already runs that version or newer | Nothing to do | | `instance_forbidden` | The publisher's executor isn't exempt from the deny assignment on your managed resource group (Marketplace: the offer's authorizations don't include the publisher's operations group) | The publisher reviews the authorizations and upgrades by hand on request until then | | `instance_not_found` | The managed resource group the instance reported no longer exists | Contact the publisher | | `whatif_unexpected` | The upgrade would add or remove an Azure resource | The publisher reviews the change and runs it with explicit approval | | `deployment_failed`, `health_gate_failed` | The deployment didn't finish, or finished but missed a health check | The instance keeps running and the publisher investigates. There's **no automatic rollback** | ### If you need the publisher to act Tell your publisher the instance identifier (the `instanceId` deployment output) and the version you expect. The publisher can start the same upgrade by hand, review the preview, and run it with any tolerance the release needs. ### Rolling back A rollback is the publisher running the same procedure with the **previous** version, forced past the already-newer check. Images and configuration converge back; the database schema doesn't. That's why every release's schema changes are **expand/contract** (add first, remove one release later): a release never drops or renames anything the previous release still uses. A release that deliberately drops something says so in its release notes and is never rolled back across the drop; the way forward is the next release. ## Reference ### Deployment outputs | Output | Description | | --- | --- | | `clientUrl` | The instance's address, and its sign-out redirect URI | | `entraRedirectUri` | The sign-in redirect URI to register in Entra | | `postdeployStatus` | Post-deployment results: `usersCreated`, `migrationApplied`, `tenantSeeded`, `signingKeyCreated`. Contains no secrets | | `superAdminBootstrapCode` | The one-time setup code. Present only when the Super Admin fields were left blank | | `instanceId` | The instance's identifier. It isn't shown inside the application; read it here (or from the suffix of the managed resource group's resource names) and quote it when you ask the publisher to act on the instance | The deployment also emits six managed-identity principal IDs: `apiPrincipalId`, `streamingPrincipalId`, `statusSyncWorkerPrincipalId`, `budgetWorkerPrincipalId`, `schedulerWorkerPrincipalId` and `runExecutorPrincipalId`. You need them only if you connect a workflow that authenticates AMC by managed identity. See [Connecting a workflow or agent](#connecting-a-workflow-or-agent). ### Related content - [Register an application with the Microsoft identity platform](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) - [Optional claims in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/optional-claims) - [Azure managed applications overview](https://learn.microsoft.com/azure/azure-resource-manager/managed-applications/overview) ## Related - [How agents connect to AMC](https://www.agentmissioncontrol.ai/developers/how-agents-connect/) - [Register your agent: the technical reference](https://www.agentmissioncontrol.ai/developers/register-your-agent/) --- Source: https://www.agentmissioncontrol.ai/developers/deploy-amc/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Bring your own agent Agent Mission Control developer docs > Bring your own agent > Connect any agent or workflow to AMC by serving a small HTTP contract that AMC calls to start, watch, answer and cancel runs. **Before you start:** An agent or workflow you can put behind an HTTP endpoint. Read [How agents connect](/developers/how-agents-connect/) first. Bring your own agent means AMC drives an HTTP service you own. The service can wrap anything: a LangGraph graph, a .NET worker, a script, or another vendor's agent. You register it with the **Flight Director / custom** adapter (`flight-director`), and AMC calls a fixed set of paths under its endpoint URL. ## The endpoints AMC calls | Method and path | AMC calls it when | Returns | | --- | --- | --- | | `GET /health` | You register the agent, or change its endpoint or auth. | Any `2xx` body | | `POST /runs` | A run starts (manual, schedule, webhook). | A run: `orchestratorRunId`, `status`, `startedAt` | | `GET /runs/{id}/status` | Every status-sync poll, and whenever someone opens the run. | Run status with every step | | `GET /runs/{id}/tokens` | Every status-sync poll. | Token totals and `costUsd` | | `GET /runs/{id}/steps/{stepId}/content` | Someone opens a step. | The step's output and activity log | | `POST /runs/{id}/cancel` | Someone cancels the run. | The run, normally with status `cancelled` | | `POST /runs/{id}/messages` | Someone answers your agent's question. | Run status after the answer | | `GET /runs/{id}/events` | Someone is watching the run live. | A Server-Sent Events stream | All eight are needed for the full experience. `/messages` matters only if your agent asks people questions, and `/events` only powers the live view: AMC records every run from its poll whether or not the stream works. > **Note:** AMC does not call `GET /workflows`, `GET /workflows/{id}`, `GET /agent-types` or `GET /runs/{id}/content` today. The amc_sdk package serves them, but you don't need them, and AMC doesn't read run parameters or steps from them: you declare those when you register. ## Wire conventions - JSON request and response bodies, with the camelCase keys shown in these docs. Extra keys are ignored. A misspelled key reads as empty, with no error. - Timestamps are ISO 8601 with an offset, for example `2026-10-17T09:30:00+00:00`. - Any non-`2xx` response is a failure. AMC retries each JSON call once (not `/health` or `/events`), on a transport fault or a non-`2xx` status, before giving up. Make `POST /runs` safe to receive twice. - Run ids are yours. Make them unique and URL-safe; AMC escapes them into paths. ## Build it 1. Implement the endpoints. In Python, [the amc_sdk package](https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/) serves all of them around one run function. In any other language, start from the [minimal endpoint](https://www.agentmissioncontrol.ai/developers/byo-test-locally/). 2. Decide how AMC authenticates: an API key, or managed identity on an approved https host. See [Endpoint and authentication](https://www.agentmissioncontrol.ai/developers/byo-endpoint-and-auth/). 3. Report progress the way AMC reads it: [run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/), [tokens and cost](https://www.agentmissioncontrol.ai/developers/byo-tokens-and-cost/), and [questions for people](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/). 4. Handle the failure cases in [Errors and timeouts](https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/). 5. [Test it locally](https://www.agentmissioncontrol.ai/developers/byo-test-locally/), deploy it, then register it: choose the **Flight Director / custom** adapter, enter the **Endpoint URL**, declare the [input contract](https://www.agentmissioncontrol.ai/developers/byo-input-contract/) and steps. See [Register your agent](https://www.agentmissioncontrol.ai/developers/register-your-agent/). ## Related - [The amc_sdk Python package](https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/) - [Endpoint and authentication](https://www.agentmissioncontrol.ai/developers/byo-endpoint-and-auth/) - [Start and cancel a run](https://www.agentmissioncontrol.ai/developers/byo-start-a-run/) - [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-overview/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Endpoint and authentication Agent Mission Control developer docs > Bring your own agent > How AMC builds request URLs from your endpoint, the two credentials it can present, and what multi-tenancy means for your service. **Before you start:** A deployed endpoint. Managed identity also needs a Tenant Admin and an app registration in Microsoft Entra ID. ## The endpoint URL The **Endpoint URL** is a base. AMC adds a trailing slash if it's missing and resolves every path relative to it, so an endpoint of `https://invoices.contoso.com/amc` gets `https://invoices.contoso.com/amc/health`, `https://invoices.contoso.com/amc/runs`, and so on. - It must be an absolute `http` or `https` URL. Use `https` anywhere but local testing; managed identity requires it. - Path segments don't change these URLs. They only form the resource id AMC sends as `orchestratorWorkflowId`. See [the resource id](https://www.agentmissioncontrol.ai/developers/register-your-agent/). - The health check does not follow redirects. Serve `/health` directly. ## API key In key mode AMC presents the key in an `api-key` header on every call, including `/health`. It never sends the key as `Authorization: Bearer`, and never sends both headers. `What your endpoint receives` ```http POST /amc/runs HTTP/1.1 Host: invoices.contoso.com api-key: Content-Type: application/json; charset=utf-8 ``` `Check the key (FastAPI)` ```python import os from fastapi import Depends, FastAPI, Header, HTTPException API_KEY = os.environ["AGENT_API_KEY"] def check_key(api_key: str | None = Header(default=None, alias="api-key")) -> None: if api_key != API_KEY: raise HTTPException(status_code=401, detail="unauthorized") app = FastAPI(dependencies=[Depends(check_key)]) ``` - Return `401` or `403` for a bad key. At registration that shows as **Access not granted**, which tells the person the endpoint is up and the key is wrong. - AMC stores the key in Key Vault, never returns it, and deletes it when the registration is deactivated. - To rotate, update the registration with the new key. AMC checks the new key against `/health` before it replaces the stored one, so accept both keys while you switch. ## Managed identity In managed-identity mode AMC requests a Microsoft Entra token from its own managed identity for the **Resource scope** you register, and presents it as `Authorization: Bearer `. Your service validates it like any Entra access token issued for your app. 1. Create an app registration for your endpoint and give it an application ID URI, for example `api://invoice-summarizer`. 2. Have your AMC administrator approve your host and that audience for the installation (below). 3. Register as a Tenant Admin (or Super Admin) with **Managed Identity (same tenant)** and **Resource scope** set to `api://invoice-summarizer/.default`. 4. In your service, accept tokens for that audience from AMC's identity, and reject everything else with `401`. AMC's identity is shared by the whole installation, so it refuses to send a token anywhere it wasn't told to. A custom-endpoint registration with managed identity must pass every rule, or registration fails before anything is stored: | Rule | If it fails | | --- | --- | | The caller is a Tenant Admin or Super Admin. | `403` with reason `managed_identity_requires_tenant_admin`. | | The scope is an application ID URI (`api://...`). Azure resource scopes and a blank scope are refused. | `400`: AMC does not send tokens for Azure resource scopes to self-hosted endpoints. | | The endpoint is `https`. | `400`: managed identity is only sent to https endpoints. | | The host, or a parent domain of it, is approved for the installation, and the scope is one of that host's approved audiences. | `400`: managed identity isn't set up for that host, or the audience isn't approved for it. | The approvals live in one installation setting, `Registration:SelfHostedManagedIdentityTargets`, in the form `host=audience[,audience...][;host=...]`. A host entry admits that host and its subdomains, never look-alikes. Empty, the default, means managed identity is off for custom endpoints. `Registration:SelfHostedManagedIdentityTargets` ```text agents.contoso.com=api://invoice-summarizer/.default;tools.contoso.com=api://tool-a/.default,api://tool-b/.default ``` ## Tenancy - AMC is multi-tenant. Each AMC tenant registers your endpoint separately, and each registration has its own credential and its own runs. - AMC sends no tenant identifier on calls to your endpoint. A managed-identity token is the same for every tenant on an installation, so it can't tell them apart. - If one endpoint serves several AMC tenants, give each registration its own API key and map key to tenant on your side. Never let one tenant's run id resolve for another. - Your agent's own outbound credentials (to your model provider, to the systems it reads) are yours to manage. AMC never sees them. > **Important:** If an AMC background worker can't resolve the registration's credential, it polls your endpoint without one rather than poll a different endpoint. An endpoint that enforces auth returns `401`, which AMC records as a poll failure on the right endpoint. Never answer an unauthenticated poll with run data. ## Related - [Bring your own agent](https://www.agentmissioncontrol.ai/developers/byo-overview/) - [Register your agent: the technical reference](https://www.agentmissioncontrol.ai/developers/register-your-agent/) - [Errors and timeouts](https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-endpoint-and-auth/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Start and cancel a run Agent Mission Control developer docs > Bring your own agent > The exact request AMC sends to start a run, what to return, where inputs come from, and how cancel works. **Before you start:** An endpoint that serves `POST /runs`. See [Bring your own agent](/developers/byo-overview/). ## The start request `POST /runs` ```json { "orchestratorWorkflowId": "invoices.contoso.com/amc", "versionId": null, "inputs": { "invoiceId": "INV-1042", "ownerEmail": "sam@contoso.com" }, "idempotencyKey": null } ``` | Field | Meaning | | --- | --- | | `orchestratorWorkflowId` | The resource id AMC derived at registration (path segments, or host and path). Use it if one endpoint serves several workflows; otherwise ignore it. | | `versionId` | A version id when the caller picked one, else `null`. Triggers always send `null`. | | `inputs` | A flat map of string to string. Every value is a string: numbers and booleans arrive as text, and webhook objects or arrays arrive as JSON text. See [Declare run parameters](https://www.agentmissioncontrol.ai/developers/byo-input-contract/). | | `idempotencyKey` | Set on scheduled fires so a retried fire doesn't start a second run. If you see a key you've already started a run for, return that run instead of starting another. | ## The response `200 OK` ```json { "orchestratorRunId": "run_5f2c0e8a41b94c55", "orchestratorWorkflowId": "invoices.contoso.com/amc", "status": "running", "startedAt": "2026-10-17T09:30:00.120000+00:00", "finishedAt": null } ``` - Return as soon as the run is accepted, and do the work in the background. Don't hold the request open for the length of the run. - `orchestratorRunId` is the id AMC uses on every later call. Keep it unique for the life of the endpoint. - `startedAt` becomes the run's start time in AMC. - Any non-terminal `status` (`running`, `pending`, `queued`) is stored as `running`, and the first poll corrects it. A terminal status (`completed`, `failed`, `cancelled`) creates the run already finished, and AMC never polls it again, so it records no tokens, cost, outcome or failure reason for that run. Return a non-terminal status and let the first poll report the ending, even for an agent that finishes synchronously. - A non-`2xx` response, or a body AMC can't parse, fails the start: the caller gets `502` and no run is created. ## Where inputs come from | Trigger | What arrives in `inputs` | | --- | --- | | **Run now** | The values the person entered. Blank fields are left out. | | Webhook, input contract declared | Every top-level field of the JSON body, as text, whether or not you declared it. Only the reserved fields `runName` and `assigneeId` are taken by AMC and never forwarded. | | Webhook, no input contract | One input, `payload`, holding the raw body. Reserved fields are stripped from it. | | Schedule | No inputs, unless the workflow has default inputs set. A resource with required parameters can't be scheduled without them. | Before any of these reaches you, AMC validates the inputs against your input contract. A start that fails validation never calls your endpoint. ## Cancel When someone selects **Cancel run**, AMC sends `POST /runs/{id}/cancel` with an empty JSON body (`{}`) and reads a run back. | You return | AMC does | | --- | --- | | `status: "cancelled"` (or `canceled`) | Ends the run as cancelled, reads its final tokens, withdraws any open intervention. | | `completed` or `failed` | Treats the cancel as a no-op: the run had already ended, and the next poll records how. | | A non-terminal status | Records the request and keeps polling until you report the ending. Use this for a cancel that takes time. | - A run waiting on a person must still be cancellable. - Return `404` for a run id you don't know. - `cancelled` means a person asked for it. When your agent stops a run itself, report `failed` with a reason. ## Related - [Declare run parameters](https://www.agentmissioncontrol.ai/developers/byo-input-contract/) - [Run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/) - [Errors and timeouts](https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-start-a-run/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Declare run parameters Agent Mission Control developer docs > Bring your own agent > Declare the inputs a run accepts so AMC can validate every start, render the Run now form, and route runs to the right person. **Before you start:** A registration you can edit: Tenant Admin, Department Manager, or Agent Manager (agents only). The input contract is the list of parameters a run accepts. You declare it on the registration, under **Input contract**, or as `inputContract` in the registration API. AMC uses it in three places: to validate every start, to build the **Run now** form, and to map webhook fields onto inputs. `inputContract` ```json { "parameters": [ { "name": "invoiceId", "type": "string", "required": true, "format": "INV-####", "description": "Invoice number" }, { "name": "amountLimit", "type": "number", "required": false }, { "name": "region", "type": "enum", "required": true, "enumValues": ["emea", "amer", "apac"] }, { "name": "ownerEmail", "type": "string", "required": false, "role": "assignee", "description": "Who approves the summary" } ], "metadata": { "team": "finance-ops" } } ``` | Field | Meaning | | --- | --- | | `name` | Required. The key the value arrives under in `inputs`. | | `type` | `string`, `number`, `boolean`, `date` or `enum`. Any other label is treated as `string`. | | `required` | A start without a non-blank value is refused. | | `format` | A hint. **Run now** shows it as the field's placeholder. | | `description` | Shown under the field in **Run now**, and as the placeholder when there is no `format`. | | `default` | Documentation only. AMC does not fill it in, so apply it in your agent when the input is absent. | | `enumValues` | For `enum`: the allowed values. **Run now** shows a picker. | | `role` | What the parameter means to AMC. `assignee` is the only role AMC understands; other values are ignored. | | `metadata` | Optional non-secret key/value pairs stored with the contract. | ## Validation AMC validates the inputs before it calls your endpoint. Workflow default inputs are all-or-nothing: they apply only when the start supplies no inputs at all, and supplying any input switches every default off. A blank value counts as absent. | Type | Accepted | | --- | --- | | `number` | A decimal number in invariant format, such as `1200` or `1200.50`. | | `boolean` | `true` or `false`, any case. | | `date` | An ISO 8601 date or date-time, such as `2026-10-17`. | | `enum` | One of `enumValues`, compared without case. With no `enumValues`, any value. | | `string` | Anything. | `400 when inputs fail validation` ```json { "title": "Invalid request", "status": 400, "detail": "One or more run inputs are invalid.", "error": "run_input_validation_failed", "errors": [ { "parameter": "invoiceId", "code": "required_missing", "message": "Parameter 'invoiceId' is required." }, { "parameter": "region", "code": "enum_mismatch", "message": "Parameter 'region' must be one of: emea, amer, apac." } ] } ``` The error codes are `required_missing`, `type_mismatch` and `enum_mismatch`. **Run now** marks the failing fields from this list. ## No parameters Leave the contract empty for a resource that gathers its own inputs, such as one that always works on yesterday's data. It can then be scheduled freely. A resource with any required parameter needs those inputs at every start, so a schedule can't run it unless it supplies them. ## Route runs to a person Mark the parameter that names the person a run is about with `"role": "assignee"`. Its value must be an email. At run start AMC resolves it to a user in your tenant and makes them the run's assignee, so the run's interventions go to them. A missing value or an unknown email falls back to the workflow's normal routing, and never fails the run. > **Note:** The registration form doesn't offer `role`. Set it through the registration API (`POST` or `PUT /api/registrations`). Your agent can also declare the assignee itself mid-run. See [Ask a person mid-run](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/). ## Webhook fields With a contract declared, a webhook body's top-level fields map onto inputs by name, so callers can send named values. `Webhook body` ```json { "invoiceId": "INV-1042", "amountLimit": 5000, "region": "emea", "runName": "invoice-INV-1042" } ``` `inputs your agent receives` ```json { "invoiceId": "INV-1042", "amountLimit": "5000", "region": "emea" } ``` `runName` and `assigneeId` are reserved: AMC uses them to name the run and set its assignee, and never forwards them. > **Important:** AMC reads the contract only from the registration. Even if your endpoint publishes one (amc_sdk serves it at `GET /workflows/{id}`), enter the same contract when you register. ## Related - [Start and cancel a run](https://www.agentmissioncontrol.ai/developers/byo-start-a-run/) - [Register your agent: the technical reference](https://www.agentmissioncontrol.ai/developers/register-your-agent/) - [Ask a person mid-run](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-input-contract/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Run status and steps Agent Mission Control developer docs > Bring your own agent > The status document AMC polls, every run and step status it understands, step content, and the live event stream. **Before you start:** An endpoint that serves `POST /runs`. See [Start and cancel a run](/developers/byo-start-a-run/). AMC's status sync calls `GET /runs/{id}/status` on every live run, every 60 seconds by default. It is the record of the run. AMC also calls it whenever someone opens the run, to draw the step list, so keep finished runs readable. `GET /runs/{id}/status` ```json { "orchestratorRunId": "run_5f2c0e8a41b94c55", "status": "running", "currentStep": 2, "totalSteps": 3, "steps": [ { "stepId": "fetch", "name": "Fetch invoice", "status": "completed", "agentId": null, "agentName": "Fetcher", "startedAt": "2026-10-17T09:30:00.300000+00:00", "completedAt": "2026-10-17T09:30:04.900000+00:00", "tokenCount": 0 }, { "stepId": "summarize", "name": "Summarize", "status": "running", "agentId": null, "agentName": "Summarizer", "startedAt": "2026-10-17T09:30:05.000000+00:00", "completedAt": null, "tokenCount": 4810, "inputTokens": 4200, "outputTokens": 610, "cachedInputTokens": 3000, "model": "" }, { "stepId": "approve", "name": "Approve summary", "status": "pending", "tokenCount": 0 } ], "runName": "invoice-INV-1042" } ``` ## Run fields | Field | Meaning | | --- | --- | | `orchestratorRunId` | Your run id. | | `status` | The run status. See the table below. | | `currentStep` | How many steps have started (the 1-based position of the latest one). | | `totalSteps` | How many steps the run has. | | `steps` | Every step, in order, including ones not started yet. | | `runName` | Optional. Renames this run in AMC. Sticky: leaving it out or sending blank keeps the last name. Ignored once the run has ended. | | `assigneeRef` | Optional. The email of the person this run is about. See [Ask a person mid-run](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/). | | `failureReason` | Optional. Why the run failed, in words a person can act on. See [Errors and timeouts](https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/). With `outcome: "partial"`, what wasn't done. | | `outcome` | Optional. `"partial"` on a `completed` run marks it **Completed with issues**. See [Completed with issues](#completed-with-issues). | For `runName`, `assigneeRef` and `failureReason`, an absent or null value means "no statement". It never clears what AMC already recorded. ## Run status values | Status | AMC records | People see | | --- | --- | --- | | `running` | Running | **Running** | | `human_intervention` | Waiting, and opens an intervention for the waiting step | **Intervention**, plus an item in the Review queue | | `waiting` or `paused` | Waiting, with no intervention | **Intervention**, with nothing to answer | | `completed` | Completed, outcome `success` (or `partial` with `outcome: "partial"`) | **Completed** (or **Completed with issues**) | | `failed` | Failed, outcome `failure` | **Failed**, with your reason | | `cancelled` (or `canceled`) | Cancelled | **Cancelled** | | anything else | Waiting, and keeps polling | **Intervention** | > **Important:** Report `running` while your agent waits on anything other than a person (a queue, an API, a timer). `waiting`, `paused` and unknown values all show as **Intervention**, which sends people looking for a question that isn't there. Once AMC records `completed`, `failed` or `cancelled`, the run is final: it stops polling, and later reports don't change it. ## Completed with issues When a run produced a usable result but part of the work wasn't done, report `completed` with `outcome: "partial"` and a run-level `failureReason` that says what wasn't done. The run's status stays `completed`; its outcome is `partial` instead of `success`. `GET /runs/{id}/status, completed with issues` ```json { "orchestratorRunId": "run_5f2c0e8a41b94c55", "status": "completed", "currentStep": 2, "totalSteps": 2, "outcome": "partial", "failureReason": "Slack post failed; the report was saved but nobody was notified.", "steps": [ { "stepId": "report", "name": "Save report", "status": "completed", "tokenCount": 0 }, { "stepId": "notify", "name": "Notify channel", "status": "failed", "tokenCount": 0, "failureReason": "Slack returned 503" } ] } ``` - `partial` is the only value AMC reads. It matches after trimming, without regard to case. Any other value, or no `outcome`, is no statement, and the run is a clean success. - AMC honors it only with `status: "completed"`. On a `failed` or `cancelled` run it is ignored. - Send it on the same report as `completed`. Once the run is final, later reports don't change it. - AMC never derives a partial outcome from failed steps. A `completed` run with a `failed` step and no `outcome` is a clean success. Whether a failed step matters is your agent's call. - AMC stores up to 2000 characters of `failureReason`. Without a run-level reason, it uses the first failed step's `failureReason`. - People see **Completed with issues** with the reason, and the completion email's subject starts with **Completed with issues**. The run counts as neither a pass nor a failure. An admin can mark it complete or failed. ## Step fields | Field | Meaning | | --- | --- | | `stepId` | Required, stable, unique within the run. AMC addresses step content and replies by it. | | `name` | Required. The label on the run page. | | `status` | `pending`, `running`, `waiting` (on a person), `completed`, `failed` or `cancelled`. | | `agentId`, `agentName` | Optional. Which agent ran the step. | | `startedAt`, `completedAt` | Optional timestamps. | | `tokenCount` | Total tokens for the step. | | `inputTokens`, `outputTokens`, `cachedInputTokens`, `model`, `modelUsage` | What AMC prices the step from. See [Report tokens and cost](https://www.agentmissioncontrol.ai/developers/byo-tokens-and-cost/). | | `delivered` | On a `waiting` step, the question for the person. Other steps serve their output from the content endpoint. | | `failureReason` | On a `failed` step, why it failed. | ## Declared steps If you registered declared steps, AMC shows the full plan as pending from the start of the run. It matches each declared step to a reported one by `key` equal to `stepId` (then by name, ignoring case). Reported steps that match nothing stay in the order they ran, alongside the matched ones. When the run ends, declared steps that never ran are dropped. ## Step content When someone opens a step, AMC calls `GET /runs/{id}/steps/{stepId}/content`. AMC doesn't store it, so serve it for as long as people may look at the run. `GET /runs/{id}/steps/summarize/content` ```json { "stepId": "summarize", "action": null, "delivered": "Invoice INV-1042: 14 line items, three cost centers, total within limit.", "reasoning": null, "activityLog": [ { "time": "2026-10-17T09:30:05.200000+00:00", "type": "thinking", "tool": null, "text": "Grouping line items by cost center", "result": null }, { "time": "2026-10-17T09:30:06.800000+00:00", "type": "tool", "tool": "get_invoice", "text": "{\"id\": \"INV-1042\"}", "result": "{\"total\": 1200}" } ] } ``` `delivered` is the step's headline output. Each `activityLog` row is one detail line under it: `type` is `thinking`, `tool` or `result`. AMC also accepts a bare string as a row and shows it as a tool line. ## Live events `GET /runs/{id}/events` returns `text/event-stream`. Replay the run's events from the start, then follow new ones, and close the stream when the run ends. Each event is one `data:` line holding a JSON object. `GET /runs/{id}/events` ```text data: {"eventType": "step.status", "orchestratorRunId": "run_5f2c0e8a41b94c55", "stepId": "fetch", "payloadFragment": "{\"status\": \"running\"}", "at": "2026-10-17T09:30:00.300000+00:00"} data: {"eventType": "step.status", "orchestratorRunId": "run_5f2c0e8a41b94c55", "stepId": "fetch", "payloadFragment": "{\"status\": \"completed\"}", "at": "2026-10-17T09:30:04.900000+00:00"} data: {"eventType": "run.completed", "orchestratorRunId": "run_5f2c0e8a41b94c55", "stepId": null, "payloadFragment": "{\"summary\": \"...\"}", "at": "2026-10-17T09:31:10.000000+00:00"} ``` `payloadFragment` is a string that itself holds JSON. AMC parses it by event type and drops events it can't read: | `eventType` | `stepId` | `payloadFragment` | Live view | | --- | --- | --- | --- | | `step.status` | Required | `{"status": "running"}` or `{"status": "completed"}`. A running step may add `"activity": {"type", "tool", "text", "result"}`. | Advances or completes the step, adds an activity line | | `token.delta` | Optional | `{"totalTokens": n, "agentTokens": n, "hostTokens": n}` | Updates the token count | | `step.paused` | Required | Not read | Shows the step is waiting on a person | | `run.renamed` | None | `{"name": "invoice-INV-1042"}` | Renames the run in place | | `run.status` | None | `{"status": "failed", "reason": "..."}` or `{"status": "cancelled"}` | Ends the view as failed or cancelled | | `run.completed` | None | Not read | Ends the view as completed | Events are display only. Anything you send on the stream must also appear in `/status`, because the poll is what AMC records. ## Related - [Report tokens and cost](https://www.agentmissioncontrol.ai/developers/byo-tokens-and-cost/) - [Ask a person mid-run](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/) - [Errors and timeouts](https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-run-status/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Report tokens and cost Agent Mission Control developer docs > Bring your own agent > Report tokens per step with the model that produced them, so AMC prices each run correctly for ROI and budgets. **Before you start:** An endpoint that serves `/status`. See [Run status and steps](/developers/byo-run-status/). AMC reads `GET /runs/{id}/tokens` and `GET /runs/{id}/status` on every poll, so report running totals while the run is live: department budget spend counts in-flight runs. `GET /runs/{id}/tokens` ```json { "orchestratorRunId": "run_5f2c0e8a41b94c55", "agentTokens": 4810, "hostTokens": 0, "totalTokens": 4810, "inputTokens": 4200, "outputTokens": 610, "costUsd": 0 } ``` | Field | Meaning | | --- | --- | | `agentTokens` | Tokens your agent's steps used. | | `hostTokens` | Tokens a separate host-side model call used, if you have one. Otherwise `0`. | | `totalTokens` | `agentTokens + hostTokens`. | | `inputTokens`, `outputTokens` | The run's prompt and completion split. | | `costUsd` | The run's cost in US dollars if you price it yourself, else `0`. | ## Per-step fields | Step field | Meaning | | --- | --- | | `inputTokens` | Prompt tokens, including cached ones. | | `cachedInputTokens` | The cached part of `inputTokens`, priced at the cheaper cached rate. | | `outputTokens` | Completion tokens. | | `model` | The model id that produced the step. AMC looks it up in its rate table, ignoring case. | | `modelUsage` | When one step used several models: `[{ "model", "inputTokens", "outputTokens", "cachedInputTokens" }]`. | > **Important:** `inputTokens` must include the cached tokens. Report only the uncached remainder and a warm cache makes the run look almost free. ## How AMC prices a run 1. If `/tokens` reports `costUsd` greater than zero, that is the run's cost. 2. Otherwise AMC prices each step: by `modelUsage` when present (each model at its own rate, summed), else by `model` and the step's tokens. A missing or unknown model uses AMC's default rate. 3. If that comes to zero (no split reported), ROI estimates cost from `totalTokens` at a flat per-token rate. `Per-step price` ```text uncached = inputTokens - cachedInputTokens cost = uncached / 1,000,000 x input rate + cachedInputTokens / 1,000,000 x cached input rate + outputTokens / 1,000,000 x output rate ``` Report `costUsd` when your provider gives you an exact figure (for example, the Claude Agent SDK's total cost). It wins over any recompute. Keep reporting tokens and `model` anyway: they drive the per-step and per-model breakdown people see. ## Failed and cancelled runs Spend before a failure is real. Keep the tokens on the step that failed and in `/tokens`, or the failed run prices at zero. On cancel, AMC reads `/tokens` one last time, so report what the run spent up to the cancel. `A step that used two models` ```json { "stepId": "research", "name": "Research", "status": "completed", "tokenCount": 51200, "inputTokens": 48000, "outputTokens": 3200, "cachedInputTokens": 30000, "model": "", "modelUsage": [ { "model": "", "inputTokens": 40000, "outputTokens": 2500, "cachedInputTokens": 28000 }, { "model": "", "inputTokens": 8000, "outputTokens": 700, "cachedInputTokens": 2000 } ] } ``` ## Related - [Run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/) - [The amc_sdk Python package](https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/) - [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-tokens-and-cost/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Ask a person mid-run Agent Mission Control developer docs > Bring your own agent > Pause a run on a question, have AMC route it to the right person, and resume when their answer arrives. **Before you start:** An endpoint that serves `/status`. See [Run status and steps](/developers/byo-run-status/). Your agent asks a person a question by parking a step and saying so in its status. AMC opens an **intervention**, routes it, tracks its SLA, and posts the answer back to your endpoint. ## The flow 1. Your agent parks a step: step `status` becomes `waiting`, the question goes in that step's `delivered`, and the run `status` becomes `human_intervention`. 2. On its next poll AMC finds the waiting step and opens an intervention for it, with your question as the request. It appears in the Review queue, the bell, and email. 3. A person replies in AMC. AMC calls `POST /runs/{id}/messages` with the answer. 4. Your agent resumes from the parked step, sets the run back to `running`, and returns the new status. `GET /runs/{id}/status while waiting` ```json { "orchestratorRunId": "run_5f2c0e8a41b94c55", "status": "human_intervention", "currentStep": 3, "totalSteps": 3, "steps": [ { "stepId": "fetch", "name": "Fetch invoice", "status": "completed", "tokenCount": 0 }, { "stepId": "summarize", "name": "Summarize", "status": "completed", "tokenCount": 4810 }, { "stepId": "approve", "name": "Approve summary", "status": "waiting", "startedAt": "2026-10-17T09:31:00.000000+00:00", "tokenCount": 0, "delivered": "The total is above the EUR 5,000 limit. Approve the summary for INV-1042?" } ], "assigneeRef": "sam@contoso.com" } ``` > **Important:** The run status must be `human_intervention`. AMC opens an intervention only for that value. A run reported as `waiting` shows as **Intervention** but gives nobody anything to answer. - Put the question in `delivered` on the waiting step. Without it the intervention opens with only the step name. - Park one step at a time. If several report `waiting`, AMC picks the one that started first. - Waiting holds the run, not your service: keep serving other runs. - Add a `step.paused` event to the live stream if you have one, so a watching browser shows the pause at once. ## The answer `POST /runs/{id}/messages` ```json { "orchestratorRunId": "run_5f2c0e8a41b94c55", "stepId": "approve", "message": "Approved. Use the March exchange rate.\n\n[Attached file: rates.pdf (file, 82 KB)]", "attachments": [ { "kind": "file", "filename": "rates.pdf", "mediaType": "application/pdf", "sizeBytes": 84213, "foundryFileId": null } ] } ``` - `message` is the person's reply. When they attach files, AMC appends one `[Attached file: name (kind, size)]` line per file and lists them in `attachments`. Without files, `attachments` is left out. - Your agent never receives a file or a link to one. Attachments stay inside AMC, scanned for malware; people download them there. - Return the run status after the answer, as JSON (`200`). The run should read `running`, or terminal if the answer finished it. - Return `409` when the run isn't waiting, or `stepId` isn't the waiting step. Return `404` for an unknown run. AMC calls your endpoint before it records the reply. If you return an error, nothing is recorded, the intervention stays open, and the person can send again. `Park and resume (FastAPI, in-memory run store)` ```python import asyncio from fastapi.responses import JSONResponse from pydantic import BaseModel class SendMessage(BaseModel): orchestratorRunId: str | None = None stepId: str | None = None message: str = "" async def ask_human(run: dict, step_id: str, question: str) -> str: run["answer"] = asyncio.get_running_loop().create_future() run["waitingStep"] = step_id set_step(run, step_id, "waiting", startedAt=now(), delivered=question) run["status"] = "human_intervention" emit(run, "step.paused", step_id) try: return await run["answer"] finally: run["answer"] = None run["waitingStep"] = None run["status"] = "running" set_step(run, step_id, "running") @app.post("/runs/{run_id}/messages") async def send_message(run_id: str, body: SendMessage): run = get_run(run_id) answer = run.get("answer") if answer is None or answer.done(): return JSONResponse(status_code=409, content={"error": "run_not_waiting"}) if body.stepId and body.stepId != run["waitingStep"]: return JSONResponse(status_code=409, content={"error": "step_not_waiting"}) answer.set_result(body.message) await asyncio.sleep(0) # let the run resume before reporting its status return await run_status(run_id) ``` The helpers (`set_step`, `emit`, `get_run`, `run_status`, `now`) are the ones in the [minimal endpoint](https://www.agentmissioncontrol.ai/developers/byo-test-locally/). In the run, call `answer = await ask_human(run, "approve", "Approve the summary?")`, then complete the step. ## Who gets the question An intervention goes to the run's assignee. The run gets one from: - the **Assignee** picked in **Run now**, or `assigneeId` in a webhook body; - an input whose parameter is marked `"role": "assignee"` (see [Declare run parameters](https://www.agentmissioncontrol.ai/developers/byo-input-contract/)); - your agent, by reporting `assigneeRef` (an email) on `/status`. Use this when a workflow runs once per person and asks each about their own data. With no run assignee, the question goes to the workflow's **Default assignee**, and failing that to any manager of its department. AMC resolves `assigneeRef` to a user in the tenant, and drops it (with an audit entry) when that person isn't in the workflow's department or can't see a sensitive workflow. An omitted `assigneeRef` never unassigns a run. ## When nobody answers AMC re-notifies and escalates on the intervention's SLA (see [Set intervention SLAs and upload limits](https://www.agentmissioncontrol.ai/guide/intervention-settings/)). Your run keeps waiting; give it your own deadline if it must not wait forever. If the run ends while waiting, for example because someone cancels it, AMC withdraws the intervention. The person's side is in [Respond to a human intervention](https://www.agentmissioncontrol.ai/guide/respond-to-interventions/). ## Related - [Run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/) - [Declare run parameters](https://www.agentmissioncontrol.ai/developers/byo-input-contract/) - [The amc_sdk Python package](https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Errors and timeouts Agent Mission Control developer docs > Bring your own agent > How to end a run as failed with a useful reason, and what AMC does when your endpoint is slow, down, or wrong. **Before you start:** An endpoint that serves `/status`. See [Run status and steps](/developers/byo-run-status/). ## Fail a run When your agent can't finish, report run `status: "failed"` with a `failureReason`, mark the step that broke `failed` with its own `failureReason`, and keep the tokens it spent. `GET /runs/{id}/status after a failure` ```json { "orchestratorRunId": "run_5f2c0e8a41b94c55", "status": "failed", "currentStep": 2, "totalSteps": 3, "failureReason": "The invoice service returned 404 for INV-1042.", "steps": [ { "stepId": "fetch", "name": "Fetch invoice", "status": "completed", "tokenCount": 0 }, { "stepId": "summarize", "name": "Summarize", "status": "failed", "tokenCount": 900, "inputTokens": 820, "outputTokens": 80, "model": "", "failureReason": "LookupError: invoice INV-1042 not found" }, { "stepId": "approve", "name": "Approve summary", "status": "pending", "tokenCount": 0 } ] } ``` - AMC stores the run-level `failureReason` when you send one. Without it, the first failed step's `failureReason` is used. - Write the reason for the person reading it: what failed and, if you know, what to do. Bound its length, and keep confidential task data out of it. - If the installation has failure-cause suggestions turned on, AMC classifies the reason and shows a suggested cause next to it. - Report `failed` for your own aborts, timeouts and limits. `cancelled` means a person cancelled the run. - When the run produced a usable result but part of the work wasn't done, report `completed` with `outcome: "partial"` instead. See [Completed with issues](https://www.agentmissioncontrol.ai/developers/byo-run-status/#completed-with-issues). ## When AMC can't reach you | Call | On failure | | --- | --- | | `GET /health` at registration | One attempt, 5 second timeout. The registration is refused as **Unreachable** or **Access not granted**. | | `POST /runs` | One retry (on a transport fault or a non-`2xx` status), then the caller gets `502` and no run is created. If your first attempt started work, stop it: AMC never learned its run id. | | Status poll (`/status`, `/tokens`) | The run keeps its last recorded state and is retried after 30 s, 2 min, 8 min, 30 min, then every 2 h. After 5 failures in a row AMC records an **Orchestrator unreachable** audit entry and sends a notification. A successful poll resets the count. | | `POST /runs/{id}/messages` | The reply isn't recorded and the intervention stays open, so the person can retry. | | `POST /runs/{id}/cancel` | The cancel fails with `502` and the run carries on. | The poll cadence, retry schedule and alert threshold are AMC defaults an operator can tune. ## Timeouts on your side - Answer every call quickly. `POST /runs` must return once the run is accepted, not when it finishes. - AMC has no deadline for a custom-adapter run that keeps reporting `running`. Enforce your own run time limit, and when it trips, end the run as `failed` with a reason that says so. - A waiting run can wait for days. Decide whether a question should expire, and fail the run with a reason if it does. ## Restarts AMC keeps polling a run until it ends. If your service restarts and forgets a run, every poll and every cancel gets `404`, the run stays live in AMC, and the unreachable notification above follows. If runs must survive a restart or a redeploy, store run state outside the process. At the least, persist the run ids you issued, and answer `/status` for a run you lost with `failed` and a reason such as "The agent restarted and lost this run." ## Status traps - An unknown run status is treated as waiting and shows as **Intervention**. Stick to the documented values. - Once a run is recorded as finished, AMC ignores later reports for it. Don't report `completed` before your last step's tokens are in. - A malformed JSON body on a poll counts as a poll failure, with the same retry schedule as an outage. ## Related - [Run status and steps](https://www.agentmissioncontrol.ai/developers/byo-run-status/) - [Start and cancel a run](https://www.agentmissioncontrol.ai/developers/byo-start-a-run/) - [Test your endpoint locally](https://www.agentmissioncontrol.ai/developers/byo-test-locally/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Test your endpoint locally Agent Mission Control developer docs > Bring your own agent > A complete minimal endpoint, and the calls to make against it in the order AMC makes them. **Before you start:** Python 3.10 or later, and `pip install fastapi uvicorn`. This service implements the whole run contract in about 150 lines, with an in-memory store and two steps. Use it to see the wire format working, or as the starting shape for a service in any language. `app.py` ```python import asyncio import json import os import uuid from datetime import datetime, timezone from fastapi import Depends, FastAPI, Header, HTTPException from fastapi.responses import JSONResponse, StreamingResponse from pydantic import BaseModel, Field API_KEY = os.environ.get("AGENT_API_KEY", "") STEPS = [("fetch", "Fetch invoice"), ("summarize", "Summarize")] def now() -> str: return datetime.now(timezone.utc).isoformat() def check_key(api_key: str | None = Header(default=None, alias="api-key")) -> None: if API_KEY and api_key != API_KEY: raise HTTPException(status_code=401, detail="unauthorized") app = FastAPI(dependencies=[Depends(check_key)]) runs: dict[str, dict] = {} class StartRun(BaseModel): orchestratorWorkflowId: str | None = None versionId: str | None = None inputs: dict[str, str] = Field(default_factory=dict) idempotencyKey: str | None = None def emit(run: dict, event_type: str, step_id: str | None = None, fragment: dict | None = None) -> None: run["events"].append({ "eventType": event_type, "orchestratorRunId": run["id"], "stepId": step_id, "payloadFragment": json.dumps(fragment) if fragment is not None else None, "at": now(), }) def set_step(run: dict, step_id: str, status: str, **fields) -> None: step = next(s for s in run["steps"] if s["stepId"] == step_id) step["status"] = status step.update(fields) emit(run, "step.status", step_id, {"status": status}) async def work(run: dict) -> None: try: invoice = run["inputs"].get("invoiceId", "unknown") set_step(run, "fetch", "running", startedAt=now()) await asyncio.sleep(1) # your real work goes here set_step(run, "fetch", "completed", completedAt=now(), delivered=f"Fetched {invoice}") set_step(run, "summarize", "running", startedAt=now()) await asyncio.sleep(1) set_step(run, "summarize", "completed", completedAt=now(), delivered="Summary text", tokenCount=1200, inputTokens=1000, outputTokens=200, model="") run["status"] = "completed" emit(run, "run.completed", fragment={"summary": "Summary text"}) except asyncio.CancelledError: run["status"] = "cancelled" emit(run, "run.status", fragment={"status": "cancelled"}) raise except Exception as exc: run["status"] = "failed" run["failureReason"] = f"{type(exc).__name__}: {exc}" emit(run, "run.status", fragment={"status": "failed", "reason": run["failureReason"]}) finally: run["finishedAt"] = now() def run_dto(run: dict) -> dict: return {"orchestratorRunId": run["id"], "orchestratorWorkflowId": run["workflowId"], "status": run["status"], "startedAt": run["startedAt"], "finishedAt": run["finishedAt"]} def get_run(run_id: str) -> dict: if run_id not in runs: raise HTTPException(status_code=404, detail="run not found") return runs[run_id] @app.get("/health") async def health(): return {"status": "healthy"} @app.post("/runs") async def start_run(body: StartRun): run_id = "run_" + uuid.uuid4().hex run = runs[run_id] = { "id": run_id, "workflowId": body.orchestratorWorkflowId or "invoice-summarizer", "status": "running", "startedAt": now(), "finishedAt": None, "inputs": body.inputs, "failureReason": None, "events": [], "steps": [{"stepId": sid, "name": name, "status": "pending", "tokenCount": 0} for sid, name in STEPS], } run["task"] = asyncio.create_task(work(run)) return run_dto(run) @app.post("/runs/{run_id}/cancel") async def cancel_run(run_id: str): run = get_run(run_id) if run["status"] in ("running", "human_intervention"): run["task"].cancel() await asyncio.sleep(0) return run_dto(run) @app.get("/runs/{run_id}/status") async def run_status(run_id: str): run = get_run(run_id) started = [s for s in run["steps"] if s["status"] != "pending"] body = {"orchestratorRunId": run_id, "status": run["status"], "currentStep": len(started), "totalSteps": len(run["steps"]), "steps": [{k: v for k, v in s.items() if k != "delivered" or s["status"] == "waiting"} for s in run["steps"]]} if run["failureReason"]: body["failureReason"] = run["failureReason"] return body @app.get("/runs/{run_id}/tokens") async def run_tokens(run_id: str): run = get_run(run_id) total = sum(s.get("tokenCount", 0) for s in run["steps"]) return {"orchestratorRunId": run_id, "agentTokens": total, "hostTokens": 0, "totalTokens": total, "inputTokens": sum(s.get("inputTokens", 0) for s in run["steps"]), "outputTokens": sum(s.get("outputTokens", 0) for s in run["steps"]), "costUsd": 0} @app.get("/runs/{run_id}/steps/{step_id}/content") async def step_content(run_id: str, step_id: str): step = next((s for s in get_run(run_id)["steps"] if s["stepId"] == step_id), None) if step is None: return JSONResponse(status_code=404, content={"error": "step not found"}) return {"stepId": step_id, "action": None, "delivered": step.get("delivered"), "reasoning": None, "activityLog": []} @app.get("/runs/{run_id}/events") async def run_events(run_id: str): run = get_run(run_id) async def stream(): sent = 0 while True: while sent < len(run["events"]): yield f"data: {json.dumps(run['events'][sent])}\n\n" sent += 1 if run["finishedAt"] and sent >= len(run["events"]): return await asyncio.sleep(0.2) return StreamingResponse(stream(), media_type="text/event-stream") ``` To add questions for people, add the `ask_human` helper and the `/messages` route from [Ask a person mid-run](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/). ## Drive it the way AMC does `Terminal 1` ```bash AGENT_API_KEY=dev-key uvicorn app:app --port 8080 ``` `Terminal 2` ```bash H='api-key: dev-key' curl -s -H "$H" localhost:8080/health # registration check curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/health # 401 without the key RUN=$(curl -s -H "$H" -H 'Content-Type: application/json' \ -d '{"orchestratorWorkflowId":"invoice-summarizer","versionId":null,"inputs":{"invoiceId":"INV-1042"},"idempotencyKey":null}' \ localhost:8080/runs | python3 -c 'import sys, json; print(json.load(sys.stdin)["orchestratorRunId"])') curl -s -N -H "$H" localhost:8080/runs/$RUN/events # live view, closes when the run ends curl -s -H "$H" localhost:8080/runs/$RUN/status # the poll curl -s -H "$H" localhost:8080/runs/$RUN/tokens # the poll curl -s -H "$H" localhost:8080/runs/$RUN/steps/summarize/content ``` ## Check before you register - `/health` returns `2xx` with the right credential and `401` without it. - `POST /runs` returns in well under a second, with status `running`. - `/status` shows every step from the start, and ends `completed` with the last step's tokens in. - Every step that used a model reports `inputTokens`, `outputTokens` and `model`. - A forced failure ends `failed` with a readable `failureReason`. - Cancel works while the run is running and while it is waiting on a person. - Finished runs stay readable at `/status` and `/steps/{stepId}/content`. ## Register a test endpoint - AMC must be able to reach the endpoint from its own network. For an AMC in the cloud, expose your local port over an https tunnel or deploy to a test host. - Use **API key** authentication. Key mode accepts `http` endpoints; managed identity needs `https` and an approved host. - Choose **Flight Director / custom**, check **This resource uses the AMC SDK** (or enter a path segment such as `invoice-summarizer`), and enter the endpoint and key. See [Register your agent](https://www.agentmissioncontrol.ai/developers/register-your-agent/). ## Related - [Bring your own agent](https://www.agentmissioncontrol.ai/developers/byo-overview/) - [The amc_sdk Python package](https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/) - [Errors and timeouts](https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-test-locally/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # The amc_sdk Python package Agent Mission Control developer docs > Bring your own agent > Serve AMC's whole run contract from one Python run function: steps, tokens, cost, renames, assignees and questions for people. **Before you start:** Python 3.10 or later, and the `amc_sdk` package that ships with AMC. `amc_sdk` is AMC's library for exposing an endpoint AMC can drive. You write a run function and a small config; `build_app` returns a FastAPI app that serves every endpoint, with the run store, run and step state, typed events, camelCase DTOs and the SSE loop built in. > **Note:** `amc_sdk` is not Anthropic's Claude Agent SDK (`claude-agent-sdk`). That library builds an agent; this one exposes an agent to AMC. They compose: build with one, expose with the other. ## Install `amc_sdk` ships with AMC and isn't on PyPI. Install it from the copy your AMC provider gives you. It depends on `fastapi` and `pydantic`; the `serve` extra adds `uvicorn`. ```bash pip install "./amc_sdk[serve]" ``` ## A complete agent `app.py` ```python import os from fastapi import HTTPException, Request from amc_sdk import Agent, AgentType, HealthReport, InputParam, RunContext, StepDef, build_app API_KEY = os.environ.get("AGENT_API_KEY", "") async def verify_caller(request: Request) -> None: if API_KEY and request.headers.get("api-key") != API_KEY: raise HTTPException(status_code=401, detail="unauthorized") def health() -> HealthReport: if not os.environ.get("MODEL_API_KEY"): return HealthReport(ready=False, detail="MODEL_API_KEY is not set") return HealthReport(ready=True) async def summarize(invoice_id: str) -> tuple[str, dict]: # Call your model here. Return the text and the usage it reported. return f"Summary of {invoice_id}", {"input": 4200, "output": 610, "cached": 3000} agent = Agent( workflow_id="invoice-summarizer", name="Invoice Summarizer", description="Summarizes one invoice.", type="workflow", steps=[ StepDef("fetch", "Fetch invoice"), StepDef("summarize", "Summarize"), ], agent_types=[AgentType("invoice-summarizer", "Invoice Summarizer", ["text"])], input_parameters=[ InputParam("invoiceId", type="string", required=True, description="Invoice number"), InputParam("ownerEmail", type="string", role="assignee", description="Who approves it"), ], health=health, ) @agent.run async def run(ctx: RunContext) -> None: invoice_id = ctx.inputs["invoiceId"] ctx.run_renamed(f"invoice-{invoice_id}") if ctx.inputs.get("ownerEmail"): ctx.set_assignee(ctx.inputs["ownerEmail"]) ctx.step_started("fetch") ctx.tool_activity("fetch", tool="get_invoice", text=f'{{"id": "{invoice_id}"}}', result='{"total": 1200}') ctx.step_completed("fetch", token_count=0, delivered=f"Fetched {invoice_id}") ctx.step_started("summarize", agent_name="Summarizer") ctx.thinking("summarize", "Grouping line items by cost center") summary, usage = await summarize(invoice_id) ctx.step_completed( "summarize", token_count=usage["input"] + usage["output"], delivered=summary, input_tokens=usage["input"], output_tokens=usage["output"], cached_input_tokens=usage["cached"], model="", ) ctx.set_output({"invoiceId": invoice_id, "summary": summary}) app = build_app(agent, verify_caller=verify_caller, max_concurrency=4) ``` ```bash AGENT_API_KEY=dev-key MODEL_API_KEY= uvicorn app:app --host 0.0.0.0 --port 8080 ``` ## The run function The handler is `async def (ctx: RunContext) -> None`, set with `@agent.run`. The SDK runs it in the background for each `POST /runs` and owns the run-level status: - **Returns normally:** the run is `completed`, and `run.completed` carries `ctx.output`. - **Raises:** the run is `failed`. Every step still pending, running or waiting is marked failed with the error text (`ExceptionType: message`), which AMC shows as the failure reason. - **Cancelled** (`POST /runs/{id}/cancel`): the run is `cancelled`, and unfinished steps are marked failed with `cancelled`. - **Returns normally after `ctx.mark_partial(reason)`:** the run is still `completed`, and `/status` publishes `outcome: "partial"` with `reason` as `failureReason`. AMC shows **Completed with issues**. `ctx.inputs` is the flat string map from `POST /runs`. The SDK doesn't parse or validate it; AMC has already validated it against your registered contract. ## RunContext | Call | What it does | | --- | --- | | `step_started(step_id, agent_name=None)` | Marks the step `running` and stamps `startedAt`. | | `step_completed(step_id, token_count, ...)` | Marks it `completed`. Keyword arguments: `delivered`, `input_tokens`, `output_tokens`, `cached_input_tokens`, `model`, `model_usage`, `agent_id`. | | `step_failed(step_id, detail=None)` | Marks one step `failed` with `detail` as its reason, while the run continues. | | `thinking(step_id, text)` | Adds a `thinking` row to the step's activity log. | | `tool_activity(step_id, tool, text="", result=None)` | Adds a `tool` row with the tool name, its input and its result. | | `activity(step_id, text, type="tool", tool=None, result=None)` | Adds any activity row. Identical rows aren't repeated. | | `token_delta(step_id, text)` | Emits a `token.delta` event. | | `await ask_human(step_id, question)` | Parks the step on a question and returns the answer posted to `/runs/{id}/messages`. See the limitation below. | | `set_assignee(email)` | Declares who the run is about. Published as `assigneeRef` on the next poll. `None` does nothing. | | `run_renamed(name)` | Renames this run. Published as `runName` and as a `run.renamed` event. Blank does nothing. | | `mark_partial(reason)` | Declares that the run finished with issues; `reason` says what wasn't done. It doesn't end the run. Once the handler returns, `/status` publishes `outcome: "partial"` and `failureReason`. A blank `reason` raises `ValueError`. The reason is trimmed and capped at 2000 characters, and the last call wins. Raising still fails the run, and on a failed or cancelled run the call is ignored. AMC never derives a partial outcome from failed steps. | | `set_cost(cost_usd)` | The run's own cost. AMC prefers it over its own pricing. | | `set_step_tokens(step_id, token_count, ...)` | Sets a step's tokens directly, for the failure path. | | `set_output(dict)` | The run's output, sent with `run.completed`. | | `emit(event_type, step_id=None, fragment=None)` | Appends a raw event. Rarely needed. | ## Report spend on the failure path A run that raises before `step_completed` reports zero tokens unless you record them first: ```python try: summary, usage = await summarize(invoice_id) except Exception: ctx.set_step_tokens("summarize", token_count=spent_in + spent_out, input_tokens=spent_in, output_tokens=spent_out, model="", cached_input_tokens=spent_cached) ctx.set_cost(spent_usd) raise # the run fails, with the error as its reason ``` ## Config objects | Object | Fields | | --- | --- | | `Agent` | `workflow_id`, `name`, `description`, `type` (`"workflow"` by default), `steps`, `version` (`"1"`), `agent_types`, `input_parameters`, `run_metadata`, `health`. | | `StepDef` | `step_id`, `name`, `description=None`. One per step, in order. `step_id` is what AMC keys the step by. | | `InputParam` | `name`, `type="string"`, `required=False`, `format`, `description`, `default`, `enum_values`, `role`. | | `AgentType` | `key`, `display_name`, `capabilities`. | | `HealthReport` | `ready=True`, `detail=None`, `status=None`. | `build_app` needs a handler and at least one `StepDef`. Every declared step is listed on `/status` from the start of each run, as `pending`. ## Health With no `health` callable, `/health` returns `{"status": "healthy", "service": ""}`. With one, `ready=False` returns `503` with `status: "not_configured"` and your `detail`. AMC's registration check treats any non-`2xx` as **Unreachable**, so check your own configuration (model keys, required files) here: a misconfigured agent can't be registered. ## build_app options | Option | Default | Purpose | | --- | --- | --- | | `verify_caller` | Allow everyone | A FastAPI dependency run before every request. Raise `HTTPException` to reject. Supply one: the default is not a security control. | | `on_startup` | None | `callable(agent)`, sync or async, run at startup. | | `max_concurrency` | Unbounded | Cap on runs executing at once in this process. Extra runs wait their turn while reported as `running`. | | `max_runs` | `200` | How many runs to keep in memory. The oldest finished runs are evicted first; running runs never are. | | `sse_poll_seconds` | `0.1` | How often the SSE stream checks for new events. | | `cors_allow_origins` | `["*"]` | CORS origins. | | `title` | The agent's name | The OpenAPI title. | ## Limitations in 0.1.0 - **Questions don't open interventions yet.** `ask_human` reports the run as `waiting`, and AMC opens an intervention only for `human_intervention`. The run shows as **Intervention** with nothing to answer. Until this changes, an agent that must ask people should serve the contract directly, as in [Ask a person mid-run](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/). - **The live view shows completion and renames only.** The SDK's step, token and failure events carry plain-text fragments, which AMC's live view can't read and drops. The poll still records everything, so the run page catches up within one poll. - **Runs live in memory.** A restart loses them: AMC's polls and cancels for those runs then fail, and the runs stay live in AMC (see [Restarts](https://www.agentmissioncontrol.ai/developers/byo-errors-and-timeouts/)). Keep `max_runs` large enough that finished runs stay readable for as long as people review them. - **Declarations stay on your side.** The SDK publishes `input_parameters` and `steps` at `GET /workflows/{id}`, but AMC doesn't read them. Enter the same input contract and declared steps when you register. ## Test with FastAPI's TestClient `test_app.py` ```python import time from fastapi.testclient import TestClient from app import app def wait_for(client, run_id, status): for _ in range(100): body = client.get(f"/runs/{run_id}/status").json() if body["status"] == status: return body time.sleep(0.02) raise AssertionError(body) def test_run_completes(): with TestClient(app) as client: run = client.post("/runs", json={"inputs": {"invoiceId": "INV-1042"}}).json() status = wait_for(client, run["orchestratorRunId"], "completed") assert status["runName"] == "invoice-INV-1042" tokens = client.get(f"/runs/{run['orchestratorRunId']}/tokens").json() assert tokens["inputTokens"] == 4200 def test_missing_input_fails_the_run(): with TestClient(app) as client: run = client.post("/runs", json={"inputs": {}}).json() status = wait_for(client, run["orchestratorRunId"], "failed") assert "KeyError" in status["steps"][0]["failureReason"] ``` Use `TestClient` as a context manager. Without it, the client's event loop closes between requests and cancels the background run. ## Related - [Bring your own agent](https://www.agentmissioncontrol.ai/developers/byo-overview/) - [Ask a person mid-run](https://www.agentmissioncontrol.ai/developers/byo-human-in-the-loop/) - [Report tokens and cost](https://www.agentmissioncontrol.ai/developers/byo-tokens-and-cost/) - [Test your endpoint locally](https://www.agentmissioncontrol.ai/developers/byo-test-locally/) --- Source: https://www.agentmissioncontrol.ai/developers/byo-amc-sdk/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Run a Claude skill as a Microsoft Foundry hosted agent Agent Mission Control developer docs > Claude skill on Microsoft Foundry > How a Claude skill runs unattended inside Foundry, with Agent Framework as the host and claude-agent-sdk as the brain, and reports to AMC. **Before you start:** A Claude skill folder (with SKILL.md) and a Microsoft Foundry project. Read [How agents connect](/developers/how-agents-connect/) first. This path turns a Claude skill into a **Foundry hosted agent**. Foundry owns the endpoint, the sessions, the sandbox, scale-to-zero and Entra auth on the data plane. Inside the container, Microsoft Agent Framework (MAF) speaks the Foundry Responses protocol, and `claude-agent-sdk` runs the skill. AMC streams the run and shows it as steps, with tokens and cost per step. The `foundry-hybrid-agent-creator` skill scaffolds the agent from templates. The templates already contain fixes for the failure modes agents of this shape hit in production, so a new agent starts with them instead of rediscovering them. ## Why two layers - **Not pure MAF:** MAF's chat client reasons over a chat API. It can't load a `SKILL.md`, follow its method, run its scripts through Bash, or delegate through Task. Only the Claude Code CLI bundled in `claude-agent-sdk` can. - **Not pure claude-agent-sdk:** that gives you a self-hosted HTTP service where you own the endpoint and the auth. See [Claude skill, self-hosted](https://www.agentmissioncontrol.ai/developers/choose-a-build-path/). The hybrid exists for when the agent must be a first-class Foundry resource: invocable over the Responses protocol, deployed as immutable versions, and visible to the platform and to AMC. ## Architecture `Process tree inside the container` ```text python3 main.py container entrypoint; Foundry starts this └── ResponsesHostServer :8088 agent-framework-foundry-hosting (MAF): HTTP + session only └── InvoiceSummarizerAgent(BaseAgent) │ clientless shell: parse, Starting step, stream, envelope, │ trailing markers, failed-run verdict, HITL plumbing ├── steps.StepReporter maps tool calls to phase steps; copies produced items out └── runner.run_turn() claude_agent_sdk.query(): the ONLY thing that reasons └── claude (bundled CLI) executes SKILL.md via the Skill tool ├── Bash -> the skill's own scripts (gated by the PreToolUse hook) ├── mcp__ -> the skill's integrations, as the instance identity └── Task -> sub-agents, only if you enable them ``` | Layer | Owns | Must never | | --- | --- | --- | | `main.py` (MAF hosting) | HTTP on `:8088`, `/readiness`, sessions | Add a `/health` route; reason | | `agent.py` (MAF shell) | Request parsing with defaults, refusal before spend, one turn at a time, the `Starting …` step, streaming, the envelope, trailing markers, the failed-run verdict, HITL plumbing | Hold a chat client, call a model, grow business logic | | `runner.py` (brain wiring) | The prompt, `ClaudeAgentOptions`, the tool-policy hook, the `query()` loop, the gates, host post-processing (publishing as plain I/O) | Make judgments the skill owns | | `steps.py` (host observer) | Mapping tool calls to phases, copying produced items out of the skill's data files, per-phase token attribution | Ask a model anything | | The skill | The method | Be patched to fit the host | ## The rule that holds it together **MAF never reasons.** The `BaseAgent` takes no chat client. The host may compute facts about the run (which tool was called, what a file contains, whether a regex matched) and enforce policy in code. It may not ask a model a question or compose content. Uploading the output after the turn is I/O, not reasoning, so the host does it. Having the model retype a large file through its output tokens is slow, costly and fails. Claude models are reached through Foundry: `CLAUDE_CODE_USE_FOUNDRY=1` and the agent's Entra **instance identity**. There is no API key in the sandbox. ## What AMC sees - A `Starting …` step the moment the request is accepted, before any model call. - One step per skill phase, each with its own tokens and `model`. - One step per produced item, copied from the skill's own data files. - Host steps for publishing, red when refused. - Trailing per-model markers with the tokens no step claimed, cost and run totals. - A failed run ends `response.failed`, so AMC shows it as failed. - A run that completed with issues sends one `run.outcome` marker and ends `response.completed`, so AMC shows it as **Completed with issues**. These are this path's way of meeting the [integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/). The rest of this section walks through each piece. ## In this section | Article | Covers | | --- | --- | | [Prerequisites and project setup](https://www.agentmissioncontrol.ai/developers/foundry-prerequisites-and-setup/) | Tools, Azure resources, the decisions to make first, vendoring the skill | | [Build the agent from the templates](https://www.agentmissioncontrol.ai/developers/foundry-build-the-agent/) | Every file, the request contract, the prompt, tool policy, gates, publishing | | [Invocation modes and session lifetime](https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/) | Streaming, background, the 100 s cap, the session idle timeout | | [Run steps, token markers and cost](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/) | Phases, item steps, residual tokens, the failure marker, `RunFailed` | | [Human-in-the-loop](https://www.agentmissioncontrol.ai/developers/foundry-human-in-the-loop/) | Pausing for a person and resuming with the answer | | [Choose models and spend ceilings](https://www.agentmissioncontrol.ai/developers/foundry-model-selection/) | Orchestrator tier, sub-agents, deployment pins, ceilings | | [Deploy to Foundry](https://www.agentmissioncontrol.ai/developers/foundry-deploy/) | Build, `deploy.py`, identities and RBAC, version payload gotchas | | [Verify and register in AMC](https://www.agentmissioncontrol.ai/developers/foundry-verify-and-register/) | Probes in order, first real run, pricing, registration | | [Troubleshooting](https://www.agentmissioncontrol.ai/developers/foundry-troubleshooting/) | Symptoms, causes and fixes, and where the logs are | ## Related - [Prerequisites and project setup](https://www.agentmissioncontrol.ai/developers/foundry-prerequisites-and-setup/) - [Build the agent from the templates](https://www.agentmissioncontrol.ai/developers/foundry-build-the-agent/) - [How agents connect to AMC](https://www.agentmissioncontrol.ai/developers/how-agents-connect/) - [Choose a build path](https://www.agentmissioncontrol.ai/developers/choose-a-build-path/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-hybrid-overview/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Prerequisites and project setup Agent Mission Control developer docs > Claude skill on Microsoft Foundry > What to install, which Azure resources and roles you need, the decisions to settle before writing code, and how to vendor the skill safely. **Before you start:** An Azure subscription with an AI Foundry project and Claude model deployments. Python 3.12 and the Azure CLI. ## What you need - **A Claude skill folder** with a `SKILL.md`, plus whatever it references: `scripts/`, `references/`, `config/`, assets. - **A Microsoft Foundry project** and the Foundry resource (account) it belongs to, with Claude deployments for all three tiers (Opus, Sonnet, Haiku). - **An Azure Container Registry** the project can pull from. - **The Azure CLI**, signed in with `az login`. The agent itself uses no API keys: you deploy as yourself and the sandbox authenticates as its own identity. - **Docker** with `linux/amd64` builds, or `az acr build` to build server-side. The hosting platform is x86_64 only. - **Python 3.12** locally to run `deploy.py` and `probe.py`, with `azure-ai-projects>=2.6.0`, `azure-identity` and `python-dotenv`. - **The `foundry-hybrid-agent-creator` skill** in your Claude Code project's `.claude/skills/`, if you want Claude to scaffold the agent for you. ## Roles | Who | Role | Scope | For | | --- | --- | --- | --- | | You, deploying | `Foundry Project Manager` | Project | Create the agent and its versions | | You, deploying | ACR push rights (or use `az acr build`) | Registry | Push the image | | The account and project system managed identities | Classic **AcrPull** | Registry | Pull the image | | The agent's **instance identity** | `Foundry User` | Foundry **account** | Call the model | | The agent's **instance identity** | App role or caller allowlist | Each outbound server | The skill's integrations | The instance identity only exists after the first deploy. [Deploy to Foundry](https://www.agentmissioncontrol.ai/developers/foundry-deploy/) covers when and how to grant it. ## Settle these before any code The creator skill runs a five-round interview. Every answer maps to a template value, so nothing is left to a guess. Decide these yourself if you build by hand. | Round | Decide | Lands in | | --- | --- | --- | | 1. The task | The request fields, each with a default (a scheduler sends `{}`). What the skill produces and where it lands. What the skill must never do here. Whether the task data is confidential. | Request parsing, `DENY_RULES`, `EMIT_STEP_ACTIVITY_LOG`, `OUTPUT_GLOBS` | | 2. Models and spend | Orchestrator tier, whether sub-agents are allowed, the three deployment names, expected spend of one run. | `CLAUDE_AGENT_MODEL`, `disallowed_tools`, `ANTHROPIC_DEFAULT_*_MODEL`, ceilings | | 3. What the run shows | Which method stages appear as steps, which produced items appear one by one, which consumer watches. | `PHASE_TITLES`, `PHASE_RULES`, `ARTIFACT_WATCHES`, `STEP_TOKENS` | | 4. Invocation and outputs | Scheduled, watched live or on demand. Expected wall clock. Who publishes the output. Outbound integrations. Hosts the skill's scripts must reach. Timezone. | `RUN_TIMEOUT_SECONDS`, session idle timeout, `_postprocess`, MCP servers, `BASH_ALLOWED_HOSTS` | | 5. Human-in-the-loop | Does a run ever need a person mid-run? Is the pause mandatory or conditional? | `HITL`, `HITL_QUESTIONS` | The port is always **8088**. There are no API keys to collect. ## Project layout For an `invoice-summarizer` skill, the scaffolded agent directory looks like this. The `.tpl` suffix is dropped when the templates are filled in. `invoice-summarizer-agent/` ```text invoice-summarizer-agent/ main.py entrypoint: ResponsesHostServer(create_agent()).run() agent.py clientless BaseAgent: the shell runner.py one skill turn: prompt, options, policy hook, query() loop, gates, publish steps.py phase rules, artifact watches, per-phase tokens, host steps probe.py preflight / stream / background / failed probes deploy.py immutable version from a container image, routes 100% to it requirements.txt exact pins Dockerfile python:3.12-slim + tzdata + ca-certificates, skill baked in, non-root .env.example endpoint, image, pins, tier, ceilings, timeouts, reporting switches .dockerignore README.md request contract, what a run reports, build / deploy / invoke / logs external/ invoice-summarizer/ the SANITIZED copy of the skill (see below) ``` ## Vendor the skill, never copy it raw The skill is baked into the image unchanged at `/app/.claude/skills//`. Copy it with a script, not a raw `COPY` of the author's folder. - **Copy everything the method references:** `SKILL.md`, `config/`, `scripts/`, `references/`, assets, and anything `SKILL.md` links to. An allowlist that drops `references/` ships a skill that passes the existence check and runs without its method. - **Exclude credential and state paths by name, wherever they sit:** `state/`, `.env*`, `credentials/`, `secrets/`, `*.token`, `*.pem`, `*.key`, plus the skill's own local state or credential folders. A live token has been found in a skill's `state/` folder. - **Fail loudly** when an excluded path is present in the copy. The Dockerfile checks again, recursively, at build time. - **Never patch the source skill.** Host-imposed changes go in the prompt as numbered items. ## Check the templates offline `Parse every Python file` ```bash python -c "import ast; [ast.parse(open(f, encoding='utf-8').read()) for f in ['main.py','agent.py','runner.py','steps.py','deploy.py','probe.py']]" ``` Then unit-test `steps.py` with a fake message sequence. Every declared phase appears, item steps appear when the data file changes, and for every model the sum of step tokens equals the attributed tokens and stays within the totals. Include the idle-gap pair described in [Run steps, token markers and cost](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/). ## Related - [Run a Claude skill as a Microsoft Foundry hosted agent](https://www.agentmissioncontrol.ai/developers/foundry-hybrid-overview/) - [Build the agent from the templates](https://www.agentmissioncontrol.ai/developers/foundry-build-the-agent/) - [Choose models and spend ceilings](https://www.agentmissioncontrol.ai/developers/foundry-model-selection/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-prerequisites-and-setup/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Build the agent from the templates Agent Mission Control developer docs > Claude skill on Microsoft Foundry > What each generated file does, and the code you fill in: request fields, the prompt, tool policy, gates, output discovery and publishing. **Before you start:** The decisions in [Prerequisites and project setup](/developers/foundry-prerequisites-and-setup/). The examples use an `invoice-summarizer` skill: it reads a day's invoices, writes `summary-.html`, and checks its totals with its own `scripts/verify_totals.py`. Substitute your skill's names. ## Placeholders Templates carry `{{NAME}}` placeholders. Simple ones are substituted; block-level markers are where you write code. | Placeholder | Meaning | Example | | --- | --- | --- | | `AGENT_NAME` | Hosted-agent name, kebab case | `invoice-summarizer-agent` | | `AGENT_DISPLAY_NAME` | Used in the `Starting …` step title | `the Invoice Summarizer` | | `AGENT_CLASS` | Shell class name | `InvoiceSummarizerAgent` | | `SKILL_NAME` | The skill's front-matter `name:` | `invoice-summarizer` | | `SKILL_SOURCE_PATH` | The sanitized skill copy in the build context | `external/invoice-summarizer` | | `ORCHESTRATOR_TIER` | `opus`, `sonnet` or `haiku` | `sonnet` | | `PRIMARY_FIELD` | The field a bare-string request maps onto | `target_date` | | `REQUEST_FIELD_NAMES` | Quoted, comma-separated request fields | `"target_date", "dry_run"` | | `HITL_ENABLED` | `True` or `False` | `False` | | `ACTIVITY_LOG` | `true` for public or your own data, `false` for confidential data | `true` | | `OUTPUT_GLOBS` | The skill's own output names; nothing else is reported or published | `"summary-*.html"` | | `BASH_ALLOWED_HOSTS` | Hosts the skill's scripts may reach from the shell; empty means none | (empty) | | `HITL_QUESTIONS` | Full-match regexes of questions the agent may ask | `r"Which ledger: .{1,80}\?"` | > **Important:** Request field names may not be `title`, `delivered`, `model` or `agent_version`. The `Starting …` step already carries those, and `readiness()` refuses every request, preflight included, until the clash is renamed. A version is immutable, so recovery is a rebuild. ## main.py The entrypoint. It configures logging, logs readiness once, and serves the clientless agent. It has no chat client and no `.as_agent()`. `main.py (core)` ```python from agent_framework_foundry_hosting import ResponsesHostServer from agent import create_agent, readiness def main() -> None: ready, detail = readiness() logger.info("preflight: %s (%s)", "READY" if ready else "NOT READY", detail) if not ready: logger.error("starting anyway: every request will be refused with this reason") ResponsesHostServer(create_agent()).run() ``` - A NOT-ready container still starts. An exiting container shows as "provisioning failed" with no reason; a started one refuses each request with the reason. - Readiness is `/readiness`, provided by the protocol library. Do not add `/health`. - Do not pass `ResponsesServerOptions(resilient_background=True)`. It is workflow-only, and `ResponsesHostServer` raises `RuntimeError` when it is given a non-workflow agent, so the container would fail to start. ## agent.py: the shell A `BaseAgent` subclass whose `run()` delegates to `runner.run_turn()`. It owns everything a network-facing entrypoint on a long-lived session must own. ### Request parsing The request is a JSON object whose fields are all optional with server-side defaults. Unknown fields are refused, not ignored: a caller passing `max_usd` or `output_folder` believes it is taking effect. No budget, path or output location is ever accepted from the wire. A bare string maps onto the primary field for playground use. Requests over 4096 bytes are refused. `mode` is `run` (default) or `preflight`. You fill in the field block. Validate each field with its default, bound prompt-bound strings (200 characters or fewer) and restrict their characters, then derive the run directories. `agent.py: _parse_request, the field block` ```python # target_date: default today in the agent's timezone raw_date = payload.get("target_date") or datetime.now(ZoneInfo("Europe/London")).date().isoformat() if not isinstance(raw_date, str) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}", raw_date): raise BadRequest("target_date must be YYYY-MM-DD") dry_run = payload.get("dry_run", False) if not isinstance(dry_run, bool): raise BadRequest("dry_run must be true or false") base = WORK_ROOT / "runs" / raw_date request: dict[str, Any] = { "mode": mode, "target_date": raw_date, "dry_run": dry_run, "run_dir": str(base / "run"), "out_dir": str(base / "out"), } ``` This snippet is illustrative. The template leaves this block for you, with the rules above in its comments; it only fixes that `run_dir` and `out_dir` are derived here and must be plain paths below the work tree. ### Refusal before spend Everything is validated before the first model call. `readiness()` runs on every request and checks the ceilings, that `SKILL.md` exists in the image, that a model credential is configured, and any extra configuration you add. A `ConfigurationError` always means nothing was spent. `{"mode":"preflight"}` is answered here with no model call. ### One turn at a time A semaphore allows one turn per session. A second request while a turn runs is refused with a reason, before spend, because the process environment and scratch directories are shared. ### Streaming and cancel on disconnect `run(stream=True)` serves `_run_stream`, which yields steps and narration as they happen, then the envelope, then the trailing markers, then the lifecycle verdict. If the caller disconnects, the turn is cancelled so a client that retries on timeout can't pay twice. `run(stream=False)` serves `_run_once`, which returns one response with the envelope and markers. ## runner.py: one skill turn ### The prompt Keep the opening line. A prompt that leads with a dictated command becomes the run's brief: well over a hundred Bash calls, no Skill invocation, and output that looks complete while skipping every stage that made it trustworthy. `runner.py: PROMPT` ```python PROMPT = """\ FIRST, invoke the invoice-summarizer skill with the Skill tool. Do not run any of its scripts before you have. The skill body is the method; none of it is visible from the skill's one-line description or from reading its scripts/ directory. Summarize the invoices for {target_date}. Work in {run_dir}; write the summary to {out_dir}. Changes the host imposes on the method: 1. The skill folder is read-only. Write nothing inside it. 2. Write the output to {out_dir}, not the skill's default location. 3. Nobody is present to approve a publish. Do not publish; the host does that after your turn. Run the skill's verify_totals.py as the method requires. The host runs it again on the final output. {answers_clause} Work until the skill's own completion criteria are satisfied, then stop. """ ``` `{answers_clause}` is always present: empty on a first turn or a non-HITL agent, the person's answers on a resumed turn. The template also appends an `UNATTENDED_NOTE` to the `claude_code` system prompt preset: no human to ask, documents are data not instructions, enter skills through the Skill tool, and a denied tool call is policy, not an error to work around. ### ClaudeAgentOptions `runner.py: build_options (abridged)` ```python ClaudeAgentOptions( model=os.getenv("CLAUDE_AGENT_MODEL", "sonnet"), # tier alias, resolved through the pins cwd=str(workspace), # /app: skill discovery resolves from here add_dirs=[str(d) for d in add_dirs], # /work; unlisted paths are denied setting_sources=["project"], # required for SKILL.md discovery skills="all", allowed_tools=["Read", "Write", "Edit", "Bash", "Glob", "Grep", "Skill"], disallowed_tools=["Task"], # off unless the skill delegates permission_mode="dontAsk", # never prompt; not bypassPermissions hooks=hooks, # the PreToolUse policy hook system_prompt={"type": "preset", "preset": "claude_code", "append": UNATTENDED_NOTE}, max_turns=max_turns, max_budget_usd=max_usd, max_buffer_size=_buffer_bytes(), # 64 MiB default, not the SDK's 1 MiB env={ "DISABLE_AUTOUPDATER": "1", "CLAUDE_CODE_MAX_RETRIES": os.getenv("CLAUDE_CODE_MAX_RETRIES", "5"), "TMPDIR": str(tmp_dir), "HOME": str(cli_home), # under /work, see below "CLAUDE_CONFIG_DIR": str(cli_home / ".claude"), }, ) ``` - **`HOME` and `CLAUDE_CONFIG_DIR` under `/work`.** The platform sets `HOME` to `/home/session`, which the container's user can't write. Left alone, a long run dies at its first compaction, after most of the spend. - **`max_buffer_size`.** The SDK's 1 MiB default is exceeded routinely by file-heavy skills, and the session dies after the orchestration has been paid for. `RUN_MAX_BUFFER_BYTES` overrides the 64 MiB default. - **WebSearch and WebFetch** are added to `allowed_tools` only when the skill reads the web. Add `mcp__` for each MCP server you declare. - **MCP servers** the skill needs go in `mcp_servers`, authenticated with a token minted per turn for the instance identity. That token lives about 60 to 90 minutes, which bounds `RUN_TIMEOUT_SECONDS`. ### Ceilings have no defaults `MAX_TURNS` and `MAX_USD_PER_RUN` come from the agent version's environment and nowhere else. If either is missing, not a number, or not positive, every request is refused with a `ConfigurationError`. A request can never set them. ### Tool policy in code A PreToolUse hook enforces what the skill must never do in this deployment. It denies with a reason the model can read, counts the denial, and explicitly allows every Bash call it doesn't deny. Without that explicit allow, the CLI's own rules refuse harmless compound commands, and one denial can make the model abandon Bash and hand-write the deliverable. `runner.py: DENY_RULES` ```python # (tool-name regex, input field or "*", value regex, reason shown to the model) DENY_RULES = [ (re.compile(r"^Bash$"), "command", re.compile(r"\bsend_reminders\.py\b"), "this deployment only summarizes; it never emails customers"), ] ``` Rules check paths, hosts and network clients, never document bodies. A body check once denied a publish because the document legitimately mentioned the forbidden host. Field `*` means the JSON of the whole input minus body fields. Before `DENY_RULES` the hook always applies a positive Bash boundary: - **Network:** shell network clients (`curl`, `wget`, `nc`, `ssh`, Python `urllib`, `requests` or `httpx`, `pip install`, `git clone`, and others) may reach only `BASH_ALLOWED_HOSTS`. Empty means no shell networking; the web goes through the WebFetch and WebSearch tools. - **Filesystem:** absolute paths must start with an allowed prefix such as `/app/`, `/work/`, `/tmp/`, `/usr/`, `/bin/`, `/etc/ssl/` or `/usr/share/zoneinfo/`. Paths like `/etc/passwd`, `/proc` (including `/proc/self/environ`) and `/home/session` are out of bounds. - **Gate scripts** may not be written, copied, moved or linked anywhere. The pinned run and output directories may not be removed or moved. > **Note:** A regex over shell text is defense in depth, not the boundary. The enforcement boundary is the container (non-root user, read-only app tree, one writable tree scrubbed per run, no credentials on disk), the CLI's `add_dirs`, and the checks on what leaves: output globs, host-run gates, and reads through pinned directory descriptors. ### The work tree Before each turn the runner scrubs the CLI home, temp and every previous run's directories under `/work`, then opens the run and output directories component by component with `O_NOFOLLOW` and holds them as directory descriptors. Every later read of a file the brain wrote goes through those descriptors, so a symlink swapped in afterwards is never followed, and a replaced directory fails the run. ### Gates: the host runs the skill's own verifiers After the turn, the host runs the skill's verifiers itself, on the files it is about to publish, and reads its own subprocess's exit status and output. Whether the brain ran them is recorded as an observation only. No gate pass, no publish. With `GATE_RUNS` empty the runner refuses to publish at all. `runner.py: GATE_RUNS` ```python GATE_RUNS = { "verify": { "script": "scripts/verify_totals.py", # relative, inside the vendored skill "args": [], "place_in_run_dir": True, # the script loads inputs from its own directory "success": re.compile(r"^OK: totals reconcile", re.M), # the script's OWN success line }, } ``` Optional keys: `inputs` (a name passed to your `_gate_inputs` for a gate that runs once per produced item) and `input_arg` (the flag that carries each item's file). Limits: 45 s per script, 150 s for all gates together, 40 items for a per-item gate. When the deliverable is generated from an artifact, also regenerate it in a scratch directory and compare bytes with what you are about to publish. That catches any edit made after the gate. An agent that publishes through an API has no file to regenerate and gets the gates and the pins only; say so in its README. ### Output discovery and publishing `runner.py` ```python OUTPUT_GLOBS = ["summary-*.html"] # only these are reported and published ``` `_discover_output` lists the pinned output directory and keeps regular files with one hard link that match `OUTPUT_GLOBS`, newest first. An empty `OUTPUT_GLOBS` matches nothing, so a paid run reports having produced nothing with no error anywhere. `_postprocess` is where the host publishes, as plain I/O. As shipped it reports **Dry run: nothing published** on a dry run, refuses with a red **Publishing the output** step when a gate didn't pass, and otherwise reports **Output ready**. Add your upload (for example eTag read-then-update, no partial publish), read every file with `steps.read_regular_file(name, out_root)`, set `result["published"]`, and set `result["is_error"]` and `result["error"]` on failure. ## steps.py Maps tool calls to phase steps and copies produced items out of the skill's data files. Configuration is covered in [Run steps, token markers and cost](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/). ## requirements.txt `requirements.txt` ```text agent-framework-core==1.11.0 agent-framework-foundry-hosting==1.0.0a260709 claude-agent-sdk==0.2.138 azure-identity==1.19.0 # only when the skill calls MCP servers as the instance identity # + the Python packages the skill's scripts import ``` - **Pin exactly.** An immutable image whose harness drifts no longer identifies the stack that produced an output. - **`agent-framework-foundry-hosting` is an alpha.** It is deliberately not moved to the responses-2.x line, whose only gain (`resilient_background`) is workflow-only. - **`claude-agent-sdk==0.2.138` bundles the Claude Code CLI** as a native binary inside the wheel and prefers it over `PATH`, so the image needs no Node.js. - **The skill's script dependencies** go in the same interpreter. If the skill ships its own `requirements.txt`, resolve both together, ideally into one hash-locked set generated on Linux. ## Dockerfile `Dockerfile (abridged)` ```dockerfile FROM python:3.12-slim WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends tzdata ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY invoice-summarizer-agent/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY external/invoice-summarizer /app/.claude/skills/invoice-summarizer/ # ...build fails if any symlink, state/, credentials/, secrets/, .env*, *.token, *.pem, # *.key, token*.json or *credential* file is inside the vendored skill COPY invoice-summarizer-agent/main.py invoice-summarizer-agent/agent.py \ invoice-summarizer-agent/runner.py invoice-summarizer-agent/steps.py /app/ RUN test -f /app/.claude/skills/invoice-summarizer/SKILL.md RUN useradd --create-home --uid 10001 agent \ && mkdir -p /work && chown agent:agent /work \ && chown -R root:root /app && chmod -R a-w /app USER agent ENV HOME=/home/agent ENV AGENT_NAME=invoice-summarizer-agent ENV AGENT_WORKSPACE=/app ENV AGENT_WORK_ROOT=/work ENV PYTHONUNBUFFERED=1 EXPOSE 8088 CMD ["python", "main.py"] ``` - `tzdata` is not in `python:3.12-slim`. Any skill that computes a date in a timezone fails on its first date call without it. - Binary dependencies of the skill's scripts are installed here, so a missing binary fails the build, not the first paid run. - `/app` is read-only to the agent's user; `/work` is the one writable tree. - `AGENT_*` names live in the Dockerfile because the platform reserves them on the version's environment. ## Related - [Run steps, token markers and cost reporting](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/) - [Invocation modes and session lifetime](https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/) - [Deploy to Foundry](https://www.agentmissioncontrol.ai/developers/foundry-deploy/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-build-the-agent/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Invocation modes and session lifetime Agent Mission Control developer docs > Claude skill on Microsoft Foundry > The three ways to call a hosted agent, the session idle timeout that kills long runs, and how to size the run timeout. **Before you start:** An agent built from the templates. See [Build the agent from the templates](/developers/foundry-build-the-agent/). ## The endpoint The endpoint is agent-scoped and isn't in the agent record. `deploy.py` prints it: ```text /agents//endpoint/protocols/openai/responses?api-version=v1 ``` The project-level `openai/v1/responses` path returns 400. Callers authenticate with an Entra token for `https://ai.azure.com`. ## Three calls | Call | Served by | Limit | Use for | | --- | --- | --- | --- | | `POST {"input": …}` (no stream, no background) | `_run_once` | About **100 s**, platform-capped. A real run never finishes. | `{"mode":"preflight"}` and local tests | | `POST {"input": …, "stream": true}` | `_run_stream` | Session idle timeout and `RUN_TIMEOUT_SECONDS` | A watching consumer: AMC, its scheduler, a person. Steps arrive live; the run ends `completed` or `failed`. | | `POST {"input": …, "background": true}`, then `GET …/responses/{id}` | `_run_once` | Same | A raw scheduler that can't hold a stream. Read the envelope's `status`. | ### What each mode means - **A cut stream cancels the turn.** So a streaming client's own timeout must exceed `RUN_TIMEOUT_SECONDS`, and a probe that closes early has still spent the minutes it watched. - **Background runs don't depend on the client,** but the stored response holds only the final items: no `Starting …` step and no phase steps. Its status reads `completed` even when the envelope says `failed`. See [Run steps, token markers and cost](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/). - **Stale background responses can't be cancelled** through the endpoint once the container is gone; they stay `in_progress`. The shell logs `Responses resilience: DISABLED` at start, which is the platform saying so. - **One turn per session.** A second request during a turn is refused before spend. ## The session idle timeout This is the one setting that silently kills every long run until you set it. `session_configuration.idle_timeout_seconds` on the agent version tells the platform to suspend the sandbox after that many seconds without an **inbound request**. The default is **900 s**; the range is **120 to 3600**. A running turn, an open stream with keepalives, a background response in progress, and completed output items do not count as activity. Without the setting, a run dies at about 15 minutes: the stream is cut, a background response sticks at `in_progress`, there's no envelope, and the spend is lost. It's easy to misread as gateway flakiness or the model. - `deploy.py` sets it from `SESSION_IDLE_TIMEOUT_SECONDS`, default 3600. Set the maximum for any run over about 10 minutes. - A run that can exceed 60 minutes also needs its caller to `GET /responses/{id}` within the window. A GET is an inbound request. Polling every 60 s is good hygiene either way. - Between HITL turns the same timer applies: an unanswered question past the timeout lands the resumed turn in a fresh sandbox with an empty `/work`. ## RUN_TIMEOUT_SECONDS A different limit: the wall clock for one turn. When it expires the runner terminates the CLI and the envelope reports `timed_out`. The spend up to that point is real. - It must be at least 300 s below the session idle timeout. `deploy.py` refuses otherwise. The idle timer starts at the inbound request and the runner still needs time to end the turn and write the envelope. - With the 3600 s maximum, the default is 3300. - Size it at 2× or more of a measured run's wall clock. - If the skill calls MCP servers, the per-turn token lives about 60 to 90 minutes. Keep the timeout inside that. ## What a scheduler sends `Scheduler body` ```json {"input": "{}", "background": true} ``` Every request field is optional with a server-side default, so `{}` is a full run. A raw scheduler then polls by id and alerts on a `failed` envelope status or a missing run. AMC streams instead and treats `response.failed` as a failed run. See [how AMC calls this agent](https://www.agentmissioncontrol.ai/developers/foundry-verify-and-register/). ## Sizing Illustrative only: a research-and-write skill on Sonnet took on the order of 60 to 90 turns and 25 to 35 minutes. On Opus the same skill took more turns and roughly twice the cost. Measure your own skill with one dry run, then set ceilings at about 1.5× and `RUN_TIMEOUT_SECONDS` at 2× or more of the wall clock. ## Related - [Run steps, token markers and cost reporting](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/) - [Deploy to Foundry](https://www.agentmissioncontrol.ai/developers/foundry-deploy/) - [Choose models and spend ceilings](https://www.agentmissioncontrol.ai/developers/foundry-model-selection/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Run steps, token markers and cost reporting Agent Mission Control developer docs > Claude skill on Microsoft Foundry > How the host turns the brain's tool calls into AMC steps, reports tokens and cost exactly once, and makes a failed run end as failed. **Before you start:** The step and run-status rules in the [integration contract](/developers/integration-contract/). Foundry reports no usage for hosted agents: the run-level `usage` block arrives all zeros, and nothing shows between `response.created` and the end. So the shell emits progress and spend as **informational `function_call` items** with `call_id` equal to `name`. AMC treats exactly that shape as a step. ## Steps in order | Step | Emitted by | When | Carries | | --- | --- | --- | --- | | `Starting ` | `agent._serve` | When the request is accepted, before any model call | Request fields, agent version, model, ceilings | | One per **phase** (`Loading the skill`, …) | `steps.StepReporter` | When the phase has been idle for two assistant messages, or at turn end | Tool-call count, elapsed time, up to 8 descriptors, `model` and that phase's tokens | | One per **produced item** (`Item: `) | `steps.StepReporter` | When a tool result lands and a watched file changed | The entry's own fields, copied | | **Host** steps (`Publishing the output`, `Dry run: nothing published`, `Output ready`) | `runner._postprocess` | After the turn | Records; `failure_reason` when refused | | Trailing `<agent>:<model>` markers, then the failure marker | `agent._markers` | Last | Residual tokens, cost, run totals | Titles are stable across runs and unique within one; a repeat becomes `Title (2)`. Run-specific facts go in `delivered`, never in the title. ## Phases A phase is a fact about a tool call, never a model's opinion. `PHASE_RULES` maps `(tool, field, regex)` to a phase; the first match wins, so order rules from specific to generic. Calls that match nothing fold into the most recent phase. `steps.py: phase map for invoice-summarizer` ```python PHASE_TITLES = { "loading": "Loading the skill", "inputs": "Reading invoices", "produce": "Writing the summary", "verify": "Verifying totals", } PHASE_RULES = [ # keep the two loading rules from the template ("Skill", "skill", re.compile(r"^(?:.*[:/])?invoice-summarizer$"), "loading", "Skill invoice-summarizer"), ("Read", "file_path", re.compile(r"/\.claude/skills/invoice-summarizer/"), "loading", "read {basename}"), ("Read", "file_path", re.compile(r"/invoices/"), "inputs", "read {basename}"), ("Bash", "command", re.compile(r"verify_totals\.py"), "verify", "{basename}"), ("Write", "file_path", re.compile(r"\.(json|md|html)$"), "produce", "write {basename}"), ] ``` - Fields: `url` (host and path, lower-cased), `query`, `command`, `file_path`, or `*` for the whole input as JSON. - Descriptor templates can use `{value}`, `{compact_url}` and `{basename}`. - Skill entry is matched on the skill **name**, allowing a `plugin:` or directory prefix. Any installed skill can be loaded by the Skill tool, so "a Skill call happened" isn't evidence this method was entered. - Give each gate (verify, lint, render) its own phase. Those are the steps a reader wants to see pass. ### Idle close Closing a phase the moment another phase's call arrives fragments the list into dozens of numbered repeats, because the model spreads one message's calls over several stages. So a phase closes after `PHASE_IDLE_MESSAGES = 2` assistant messages without a call in it. Keep 2. Test it with a one-message gap. A two-message gap closes the phase either way, so it can't tell correct accounting from an off-by-one. In this example from the skill's own guidance, `lint` maps to a phase titled `Linting the body`: `Idle-gap test` ```python run(["lint", "plan", "lint"]) # no "(2)": the phase survived the single gap run(["lint", "plan", "queue", "lint"]) # "Linting the body (2)": closed after two ``` ## Item steps `ARTIFACT_WATCHES` names a data file the skill writes in its run directory, the list inside it, the key that detects new entries, and the fields to show. The brain composes; the host transcribes. `steps.py` ```python ARTIFACT_WATCHES = [ {"file": "invoices.json", "list": "items", "key": "id", "title": "vendor", "title_prefix": "Item: ", "body": ["invoice_no", "date", "amount"]}, ] ``` `file` must be a single file name directly in the run directory. Optional `revision_field` and `revision_prefix` report a field's revisions as their own steps, marked `(revised)`. Leave the list empty when the skill produces one opaque output. ## Tokens, exactly once Each phase step carries the tokens accrued since the previous close, for the model with the most new output, and marks them **attributed**. The trailing marker for each model then carries only `totals − attributed`, never negative: WebFetch's Haiku summaries, the closing narration, anything no step claimed. Summing every step plus the trailing markers gives the run total once. - **`input_tokens` is cache-inclusive:** uncached + cache read + cache creation. `cached_input_tokens` is the cache-read subset. AMC prices `input − cached` at the input rate and `cached` at the cached rate. Report only the uncached remainder and a warm-cache run is under-priced by orders of magnitude. - **Every step names its `model`,** even at zero tokens. A step without one is priced at AMC's generic default rate: wrong money, no error. - **Never put run totals back into `input_tokens`** on the trailing markers while steps carry tokens. AMC would bill the run twice. - **`cost_usd` follows the same rule.** AMC prefers a reported cost over pricing tokens, so the trailing marker carries the model's exact `cost_usd` only when no step claimed that model's tokens. Otherwise the figure rides as `run_model_cost_usd`, which AMC ignores. - **`STEP_TOKENS=off`** moves every token onto the trailing markers. Use it for a consumer that reads only the stored background response, where mid-run steps aren't persisted. ### The trailing marker `One per model` ```json { "type": "function_call", "call_id": "invoice-summarizer-agent:<sonnet-deployment>", "name": "invoice-summarizer-agent:<sonnet-deployment>", "arguments": { "delivered": "run complete: 9 steps reported, $1.84, 41 turns, 12 min", "input_tokens": 1820, "output_tokens": 412, "total_tokens": 2232, "cached_input_tokens": 1500, "model": "<sonnet-deployment>", "run_model_cost_usd": 1.79, "run_cost_usd": 1.84, "run_input_tokens": 912340, "run_output_tokens": 38110, "run_cached_input_tokens": 851200, "run_turns": 41, "run_duration_s": 734, "steps_reported": 9 } } ``` Values are illustrative. One marker per model, each with its own `call_id`: markers that share a `call_id` merge into one item with invalid JSON arguments. Never emit an aggregate marker alongside the per-model ones. Token figures come from the SDK's `ResultMessage.model_usage`, which is per deployment, covers the whole tree (sub-agents and WebFetch's summarizer included), and is camelCase (`inputTokens`, `cacheReadInputTokens`, `costUSD`). Per-step tokens come from `AssistantMessage.usage`, which repeats on every content-block frame of a message: key it by message id and keep the last. ### activity_log With `EMIT_STEP_ACTIVITY_LOG=true`, the run's transcript (thinking, tool calls and results, bounded but not redacted) rides on exactly one marker: the model with the most output tokens. AMC renders it as collapsible detail. Never on every marker, never on the failure marker. ## A failed run must end as failed AMC derives run status from the Responses lifecycle frame: `response.completed` is completed, `response.failed` or an `error` event is failed. A marker with `failure_reason` fails only that step. An agent whose publish is refused, but whose turn ends normally, shows as a green run with one red step. So `_run_stream` yields the envelope and every marker, then raises: `agent.py: _run_stream` ```python failed = bool(error) or bool((result or {}).get("is_error")) # the FLAGS, never the text if failed: reason = str(error or (result or {}).get("error") or _NO_REASON) raise RunFailed(reason[:2000]) ``` - The hosting adapter catches it and emits `response.failed` with `error.message` set to the reason. Items already streamed stay on the response, so AMC gets the status, the reason and the spend. - **Gate on the flags, not the reason text.** An SDK error frame can arrive with an empty reason. - **Never raise before the markers,** or the spend report is lost. Raise from inside the generator's `try` so the cancel-on-disconnect `finally` still runs. - **Waiting on a person is not a failure.** A pending question completes the response normally. ### The failure marker ```json { "type": "function_call", "call_id": "invoice-summarizer-agent", "name": "invoice-summarizer-agent", "arguments": {"delivered": "", "failure_reason": "refused to publish: ... verify", "model": "<sonnet-deployment>"} } ``` Exactly one, named with the plain agent name, never with tokens or `cost_usd`. Emit it for a shell-level refusal (no usage markers exist) and for a runner-level error (usage markers accompany it). ### The non-streaming path can't do this If `_run_once` raised, the adapter would emit `response.failed` with empty output and the envelope and markers would be lost. So a background request without `stream` always stores a `completed` response, and its envelope's `status` carries the verdict. | Path | Used by | Run verdict | | --- | --- | --- | | Streaming (`stream: true`) | AMC, a person watching | The lifecycle frame, and the envelope | | Background without `stream` | A raw scheduler polling by id | The envelope's `status` only | ## Completed with issues A failed step doesn't fail the run, and AMC never works out from failed steps that a run was only partly done. Only an explicit `run.outcome` marker counts. So the shell has three endings: | Ending | When | Envelope `status` | On the stream | AMC shows | | --- | --- | --- | --- | --- | | ok | Every step ran, or every failure was classed `ignore` | `ok` | `response.completed` | **Completed** | | Completed with issues | A step classed `partial` failed, and nothing fatal did | `partial`, with `output.partial_reasons` | One `run.outcome` marker, then `response.completed` | **Completed with issues**, with the reason | | failed | Anything fatal: a ceiling, a refused publish, a bad request, a host step that isn't classed | `failed` | The failure marker, then `RunFailed` and `response.failed` | **Failed** | ### STEP_FAILURE_POLICY `runner.STEP_FAILURE_POLICY` maps a host step's title to what its failure means for the run. You fill it in when you build the agent: `runner.py` ```python STEP_FAILURE_POLICY: dict[str, str] = { "Notifying the channel": "partial", "Refreshing the archive index": "ignore", } ``` | Value | The run | | --- | --- | | `fatal` | Fails. This is the default: a title that isn't listed, or any value other than `partial` or `ignore`, is fatal. | | `partial` | Completes with issues. The step's title and reason are added to the run's reasons. | | `ignore` | Stays clean. The step still shows red. | - A failing host action reports through `runner.host_step_failed(result, reporter, title, delivered, reason)`, which shows the red step and applies the policy. - `runner.note_partial(result, reason)` records a shortfall the host can compute from the skill's own output, such as a count in its data file. - Publishing the deliverable is always fatal, so **Publishing the output** is never listed. - The shell joins the reasons into one marker, capped at 2000 characters. It sends none on a failed run or a paused one. A failure after it still wins. - The non-streaming path carries the same verdict in the envelope (`status: "partial"`), and the marker is in its output. ### The run.outcome marker ```python Content.from_function_call( call_id="run.outcome", name="run.outcome", arguments={"outcome": "partial", "reason": "Notifying the channel: Slack returned 503"}, informational_only=True, ) ``` - Reserved name `run.outcome`, with `call_id` equal to `name`. AMC reads it from the item's `done` frame and never lists it as a step. It carries no tokens, no `model` and no `delivered`. - `outcome` must be `"partial"`, the only value AMC acts on. Any other value is ignored and keeps an earlier `partial`. - `reason` says what wasn't done, in words the user can act on, up to 2000 characters. AMC shows it as the run's reason. - Send it any time before `response.completed`. The last `partial` wins. A marker after the run has ended is ignored. - Failed wins. If the run ends `response.failed`, AMC ignores the marker. ## The envelope The last text item, same shape on every path: ```json {"status": "ok" | "partial" | "failed" | "awaiting_human", "error": null, "request": {"mode": "run", "target_date": "2026-10-16", "dry_run": false}, "output": {"output_path", "output_files", "output_bytes", "gate_evidence", "skills_bypassed", "published", "preflight", "steps", "pending_request", "hitl_paused", "partial_reasons"}, "metrics": {"turns", "cost_usd", "input_tokens", "output_tokens", "duration_ms", "model_usage", "step_tokens_attributed", "recovered_errors", "timed_out"}} ``` - `gate_evidence` carries `<gate>_ok` from the host's own run of each gate, plus `<gate>_run_by_brain` as an observation. A failing gate adds `<gate>_output`. - `skills_bypassed` non-null means the skill was never entered and the output came from improvisation. It is reported, not failed. ## Confidential mode `EMIT_STEP_ACTIVITY_LOG=false` is the single switch for confidential task data. It withholds the transcript, emits phase steps with counts, timings and tokens but no descriptors, turns item steps into `Item: item 3` with no content, echoes request field names only, masks file and publish-record names in the envelope with `***`, keeps the model's narration in the container, and replaces model-authored error text and request-derived refusal text with a fixed sentence (the detail stays in the container log). ## Rename the run An agent can rename its own run in AMC with a marker named `run.renamed` whose `call_id` is also `run.renamed`, carrying `{"name": "…"}`. Or add `run_name` to any step's arguments, such as the `Starting …` step. The last non-empty name wins, a blank name is ignored, names are trimmed and capped at 80 characters, and a rename after the run has ended is ignored. ## Related - [Invocation modes and session lifetime](https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/) - [Human-in-the-loop for a Foundry hybrid agent](https://www.agentmissioncontrol.ai/developers/foundry-human-in-the-loop/) - [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Human-in-the-loop for a Foundry hybrid agent Agent Mission Control developer docs > Claude skill on Microsoft Foundry > Pause a run for a person's decision, ask through AMC, and resume the run with the answer. **Before you start:** `HITL_ENABLED` set to `True` and a consumer that can answer, such as AMC's run page. Most scheduled skills never need a person. Leave `HITL = False` unless the skill has a decision a person must make (which of several targets, approval before publish) and a consumer can answer it. The brain runs unattended, so the pause is host-driven: the brain writes a question file and stops, the shell asks the question in the shape AMC recognises and completes the response, and the next turn applies the answer. ## The flow 1. The brain writes `<run_dir>/hitl_request.json` with `request_id`, a one-sentence `question`, and `options` (a list) if the answer is a choice. It saves its intermediate work in the run directory and stops. 2. After the turn, the runner reads the file through the pinned run directory. The envelope's status becomes `awaiting_human`. 3. The shell emits steps and markers for the work so far, then a `request_info` item, then completes the response normally. 4. AMC shows the question. The answer comes back as a new request with `previous_response_id` set to the paused response and a `function_call_output` whose `call_id` is the request id. 5. The shell detects the resume, recovers the original request, keeps the run directory, and builds `{answers_clause}` into the prompt so the brain continues. ## The question item ```json { "type": "function_call", "name": "request_info", "call_id": "ledger-choice.7f3a2c91b0de", "arguments": { "request_id": "ledger-choice.7f3a2c91b0de", "request_event": {"type": "request_info", "data": {"question": "Which ledger: EU or US?"}}, "options": ["EU", "US"], "request": {"mode": "run", "target_date": "2026-10-16", "dry_run": false}, "resume_state": {"mode": "run", "target_date": "2026-10-16", "dry_run": false} } } ``` - **`call_id` is not equal to `name`.** That keeps it out of the step list and lets AMC key the pending request on `call_id`. AMC recognises `request_info` by its name alone, so the question still shows if `call_id == name`, but keep them different so the pending request has its own key. - **The id is unique per pause:** the brain's id as the stem, if well formed (otherwise `req`), plus a `.` and 12 random hex characters the host always appends. - **`resume_state`** carries the original request so the resumed turn never runs on defaults. It is omitted in confidential mode. ## The answer `Resume request` ```json { "previous_response_id": "<paused-response-id>", "input": [{"type": "function_call_output", "call_id": "ledger-choice.7f3a2c91b0de", "output": "EU"}], "stream": true } ``` An answer to a `call_id` this conversation never asked is ignored. An unrelated function result doesn't count as a resume. ## What to add - **`HITL_NOTE`** appended to the unattended note: when a decision genuinely needs a person, and only then, write the file, save intermediates, and stop; the answer arrives at the start of the next turn; never ask a question the skill's own rules can answer. - **`HITL_QUESTIONS`:** full-match regexes of the questions the agent may ask. In confidential mode a question that doesn't match, or an option over 40 characters or containing a URL or path, is replaced with a fixed prompt. - **Tell the brain to re-derive missing intermediates.** A session suspended by the idle timeout between question and answer gives the resumed turn an empty `/work`. Put anything small and essential in the question's arguments too. ## Design for these - **The question file fails closed.** Present but unreadable, malformed, not an object, or without question text makes the run failed. Only a missing file means no pause. - **A turn that never pauses ends green.** If the brain simply doesn't write the file, the run ends `ok` having asked nobody. `output.hitl_paused` shows it. If the pause is a mandatory approval gate, enforce it in `_postprocess`: a first turn with `hitl_paused` false and nothing published is a failed run. - **Cost and ceilings are per turn.** A resumed turn reloads the skill and pays for its context again, so a two-turn run can spend up to twice `MAX_USD_PER_RUN`. - **A background caller** sees `request_info` in the stored response and status `awaiting_human`, and resumes the same way. ## Related - [Run steps, token markers and cost reporting](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/) - [Invocation modes and session lifetime](https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/) - [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-human-in-the-loop/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Choose models and spend ceilings Agent Mission Control developer docs > Claude skill on Microsoft Foundry > Pick the orchestrator tier, decide on sub-agents, pin all three deployments, and size the ceilings from a measured run. **Before you start:** Claude deployments in your Foundry resource. Run `az cognitiveservices account deployment list` to see their names. ## Three separate decisions | Decision | Where it lands | Default if you have no view | | --- | --- | --- | | **Orchestrator tier:** the model that runs the skill | `CLAUDE_AGENT_MODEL` on the version: `opus`, `sonnet` or `haiku`, resolved through the pins | What the skill runs on locally. A skill that pins no model runs on the author's session model, usually Sonnet. | | **Sub-agents:** may the brain delegate with Task | `disallowed_tools` in `runner.build_options` | **Off.** Enable only when `SKILL.md` itself delegates. | | **Deployment pins** | `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Pin all three to deployments that exist, even if only one tier is used. | - **Opus** for judgment-heavy work, **Sonnet** for method-following work at about half the cost, **Haiku** only for trivial skills. - **Task stays off for a skill that never delegates.** With the tool available, such a skill can still fan out into parallel sub-agents and exhaust the sandbox, and the run dies with no envelope. - **An unpinned tier** falls back to a deployment name that may not exist, and the run fails mid-way, after spend. - **Haiku appears in usage even when nothing chose it:** WebFetch summarizes fetched pages with a small model. Its tokens are real and show on their own marker. `.env` ```bash ANTHROPIC_DEFAULT_OPUS_MODEL=<opus-deployment-name> ANTHROPIC_DEFAULT_SONNET_MODEL=<sonnet-deployment-name> ANTHROPIC_DEFAULT_HAIKU_MODEL=<haiku-deployment-name> CLAUDE_AGENT_MODEL=sonnet ``` ## Relative cost Illustrative, from a research-and-write skill. Measure your own. | Orchestrator | Relative cost | Notes | | --- | --- | --- | | Sonnet | 1× | Tens of turns, 25 to 35 minutes | | Opus | About 2× | More turns than Sonnet, not fewer | | Opus, improvising without the skill | About 4× | The prompt led with a command and the method never loaded | ## Ceilings - `MAX_TURNS` and `MAX_USD_PER_RUN` at about 1.5× a measured run. For a Sonnet run of about 90 turns, that's `MAX_TURNS=160` and a dollar ceiling about twice the measured cost. - `RUN_TIMEOUT_SECONDS` at 2× or more of the wall clock, and at least 300 s below the session idle timeout. - Session idle timeout at the 3600 maximum. - A ceiling below one full run makes "partial" the designed steady state. - Re-measure after changing the orchestrator tier. ## Prices in AMC AMC prices each step by its `model` and tokens from the token prices you configure. A model with no price is priced at a generic default rate, which is wrong for Claude. Add a price for **every** deployment name the agent can report, including Haiku when WebFetch is allowed. When a marker carries the SDK's own `cost_usd`, AMC uses it. ## Related - [Run steps, token markers and cost reporting](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/) - [Invocation modes and session lifetime](https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/) - [Verify the agent and register it in AMC](https://www.agentmissioncontrol.ai/developers/foundry-verify-and-register/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-model-selection/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Deploy to Foundry Agent Mission Control developer docs > Claude skill on Microsoft Foundry > Build the image, create an immutable agent version with deploy.py, grant the instance identity, and avoid the known deploy gotchas. **Before you start:** A built agent, a container registry the project can pull from, and the `Foundry Project Manager` role on the project. ## 1. Build the image ```bash docker build --platform linux/amd64 -f invoice-summarizer-agent/Dockerfile \ -t invoice-summarizer-agent:$(git rev-parse --short HEAD) . # or build server-side, with no local push: az acr build --registry <registry> --platform linux/amd64 \ --image invoice-summarizer-agent:<git-sha> \ -f invoice-summarizer-agent/Dockerfile --no-logs . ``` - `linux/amd64` is required: the platform is x86_64 only. - **Tag by git SHA. Never `:latest`, never a reused tag.** The platform caches by tag, so a tag rebuilt after a version points at it is a fix that never shipped. `deploy.py` refuses `:latest`. ## 2. Configure .env `.env` ```bash FOUNDRY_PROJECT_ENDPOINT=https://<account>.services.ai.azure.com/api/projects/<project> ANTHROPIC_FOUNDRY_RESOURCE=<account> AGENT_IMAGE=<registry>.azurecr.io/invoice-summarizer-agent:<git-sha> ANTHROPIC_DEFAULT_OPUS_MODEL=<opus-deployment-name> ANTHROPIC_DEFAULT_SONNET_MODEL=<sonnet-deployment-name> ANTHROPIC_DEFAULT_HAIKU_MODEL=<haiku-deployment-name> CLAUDE_AGENT_MODEL=sonnet MAX_TURNS=<from a measured run> MAX_USD_PER_RUN=<from a measured run> RUN_TIMEOUT_SECONDS=3300 SESSION_IDLE_TIMEOUT_SECONDS=3600 STEP_TOKENS=on EMIT_STEP_ACTIVITY_LOG=true HOSTED_CPU=2 HOSTED_MEMORY=4Gi # only used to print a ready-to-paste role assignment AZURE_SUBSCRIPTION_ID=<subscription-id> AZURE_RESOURCE_GROUP=<resource-group> ``` There are no secrets in this file. `ANTHROPIC_FOUNDRY_RESOURCE` is the resource name only. Sandbox tiers are 0.5/1Gi, 1/2Gi and 2/4Gi; start on 2/4Gi. Guessing low doesn't give a slow run, it gives a container that dies at turn start with no envelope. ## 3. Run deploy.py ```bash pip install "azure-ai-projects>=2.6.0" azure-identity python-dotenv az login python deploy.py ``` `deploy.py` doesn't build or push. It creates a version from the image, waits for it to become `active`, routes 100% of the endpoint to it, and prints both agent principals and the endpoint URL. `deploy.py: the version definition` ```python HostedAgentDefinition( cpu=CPU, memory=MEMORY, container_configuration=ContainerConfiguration(image=image), environment_variables=agent_environment(foundry_resource), protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")], session_configuration=SessionConfiguration( idle_timeout_seconds=timedelta(seconds=SESSION_IDLE_TIMEOUT_SECONDS)), ) ``` The version's environment sets `CLAUDE_CODE_USE_FOUNDRY=1`, `ANTHROPIC_FOUNDRY_RESOURCE`, the three pins, `CLAUDE_AGENT_MODEL`, `MAX_TURNS`, `MAX_USD_PER_RUN` (required, no defaults), `RUN_TIMEOUT_SECONDS`, `CLAUDE_CODE_MAX_RETRIES` (default 5), `STEP_TOKENS`, `EMIT_STEP_ACTIVITY_LOG`, and any integration settings you add. All of it is immutable per version, so "what limit was this run held to" is answerable from the deployment record. ### Version payload gotchas - **Protocol `responses` version `2.0.0`, not `1.0.0`.** `1.0.0` gives an opaque `internal server error` on every request while the container looks healthy. - **Session idle timeout on every version.** Without it every run longer than 15 minutes dies. It needs `SessionConfiguration`, and the field is a `timedelta`, not an integer. - **No `AGENT_*` or `FOUNDRY_*` names** in `environment_variables`: the platform rejects the whole version as reserved. Container defaults for those names go in the Dockerfile. The platform injects `FOUNDRY_AGENT_VERSION`, `FOUNDRY_AGENT_NAME` and `FOUNDRY_PROJECT_ENDPOINT`, which you can read. - **Rollback is a routing change**, not a rebuild. Versions are immutable and retained; point the version selector at a previous one. ## 4. Grant the identities A hosted agent carries two principals: a blueprint and an **instance identity**. The sandbox authenticates as the instance identity. Granting the blueprint looks right in the portal and produces `401 Principal does not have access to API/Operation`. The instance identity is stable across versions, so you grant it once. `Model access (printed by deploy.py)` ```bash az role assignment create \ --assignee-object-id <instance-identity-principal-id> \ --assignee-principal-type ServicePrincipal \ --role "Foundry User" \ --scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.CognitiveServices/accounts/<account>" ``` - **Account scope, not project.** Claude Code calls the account's `/anthropic/v1/messages` endpoint itself, and account-level endpoints aren't proxied by the project. - **Use `--assignee-object-id` with `--assignee-principal-type ServicePrincipal`.** The plain `--assignee` form does a Graph lookup a non-privileged account can't perform on these principals. - **Image pull uses the account and project managed identities,** not the agent's. `image_pull_failed`, `UnauthorizedAcrPull` or `InvalidAcrPullCredentials` mean they lack classic **AcrPull** on the registry. `Container Registry Repository Reader` is inert on a registry in legacy permissions mode. The registry also needs `azureADAuthenticationAsArmPolicy` enabled. - **Every outbound server** the skill calls must allow the same instance identity: an app role and, if the server keeps one, its caller allowlist. ### Outbound integrations An unattended agent that must write to a Microsoft 365 document library or any Graph-backed store needs an app-to-app MCP server the brain calls. Foundry's built-in document-library tool is read-only grounding and refuses managed identities. When that API is app-only, no person can hold a token for it, so `{"mode":"preflight"}` is the only test. Adding a delegated scope so a person can probe it removes the server's security property. ## Runtime notes - `CLAUDE_CODE_MAX_RETRIES` is bounded at 5. Foundry returns no rate-limit headers, and the CLI's default backoff makes a hard failure look like a hang. - There is no per-request caller authorization on the data plane. Any principal with project data-plane access can invoke a run and spend up to `MAX_USD_PER_RUN`. Don't route a production agent to callers beyond the deploying team, and say so in its README. - Set an Azure budget and action group on the subscription. It's the only bound that survives restarts. ## Windows workstations - The Azure CLI can crash with `UnicodeEncodeError: 'charmap'` while streaming output. Set `$env:PYTHONIOENCODING = "utf-8"` and use `az acr build --no-logs`. The server-side operation usually succeeded, so check state before retrying. - `docker push` through Docker Desktop's proxy can die partway. Use `az acr build`. - Git Bash rewrites a leading `/subscriptions/...` argument into a Windows path. Run scope-bearing `az` commands from PowerShell or set `MSYS_NO_PATHCONV=1`. JMESPath filters break in PowerShell; run those from Bash. ## Related - [Verify the agent and register it in AMC](https://www.agentmissioncontrol.ai/developers/foundry-verify-and-register/) - [Troubleshoot a Foundry hybrid agent](https://www.agentmissioncontrol.ai/developers/foundry-troubleshooting/) - [Invocation modes and session lifetime](https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-deploy/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Verify the agent and register it in AMC Agent Mission Control developer docs > Claude skill on Microsoft Foundry > Prove the deployed agent with free probes first, run it once for real, then add token prices and register it in AMC. **Before you start:** A deployed, routed agent version with the instance identity granted. See [Deploy to Foundry](/developers/foundry-deploy/). `probe.py` calls the deployed endpoint as your signed-in `az` user. Run the probes in this order: each is cheaper than the next. ## 1. Preflight (free) ```bash python probe.py preflight ``` Sends `{"mode":"preflight"}`: no model call, no skill. It must return envelope status `ok` before anything is spent. The detail starts with `filesystem ok: N checks`, which proves the host's file layer on this image (directories pin by descriptor, symlinks are refused, the scrub empties what it says), followed by one round trip per outbound dependency you added, as the agent's own identity. ## 2. Failed-run lifecycle (free) ```bash python probe.py failed ``` Sends a request the shell refuses before spend (an unknown field) on both paths. On the stream it must end `response.failed`; the stored background response reads `completed` with envelope status `failed`. ## 3. A few minutes of streaming ```bash python probe.py stream 300 ``` A real run for 300 seconds, printing each finished step with its model and tokens. Closing the connection cancels the turn, so this costs a small fraction of a full run. A cold container can take a minute or more to answer; the probe waits up to 180 s to connect. ## 4. The first full run ```bash python probe.py background ``` Starts a background run, polls every 60 s (each GET also keeps the session alive), and prints the stored envelope. Once the agent is registered (step 6) you can start it from AMC instead. Then read the envelope: - `status` is `ok`. - `gate_evidence` has `<gate>_ok` true for every gate. - `skills_bypassed` is `null`. - `step_tokens_attributed` is at most `model_usage` for every model. - Then set the ceilings at 1.5× what you measured and deploy a new version with them. ## 5. Add token prices in AMC Add a price for every model deployment name the agent can report, Haiku included when WebFetch is allowed. A missing price is billed at a generic default rate with no error. See [Choose models and spend ceilings](https://www.agentmissioncontrol.ai/developers/foundry-model-selection/). ## 6. Register in AMC Registration itself is covered in [Register your agent](https://www.agentmissioncontrol.ai/developers/register-your-agent/). What is specific to a Foundry hosted agent: | Field | Value for this agent | | --- | --- | | **Kind** | Agent | | **Adapter** | **Azure AI Foundry** | | **Endpoint URL** | The agent URL `deploy.py` printed: `<your-foundry-project-endpoint>/agents/<agent-name>/endpoint/protocols/openai/responses`. AMC derives the project endpoint (everything before `/agents/`) and the agent name from it, and adds `api-version` itself. | | Auth | **Managed Identity (same tenant)** or **API key** | | **Resource scope** | Leave blank for the default `https://ai.azure.com/.default`. The only other accepted value is `https://cognitiveservices.azure.com/.default`. | - By default the endpoint host must be a Microsoft Foundry host (ending `.services.ai.azure.com`, `.ai.azure.com` or `.cognitiveservices.azure.com`). AMC won't send its token anywhere else. - To register every agent in a project at once, use **Import a Foundry project** with the **Foundry project endpoint**. AMC lists the project's agents, registers each as an agent or workflow by its type, and never deactivates rows that are absent. ### How AMC calls this agent - AMC POSTs to the agent's Responses endpoint with `stream: true`. A plain hosted agent is not sent `background` by default, so if AMC's connection drops, the turn is cancelled like any cut stream. - The Responses `input` is a string: AMC sends the run's `prompt` input, or the first input if there is no `prompt`. The shell parses it as JSON, so a value such as `{"target_date": "2026-10-16"}` sets request fields, a bare value maps onto the primary field, and an empty value runs on every default. - A paused run (a `request_info` item followed by `response.completed`) shows as waiting on a person. AMC sends the answer as `function_call_output` with `previous_response_id`, as in [Human-in-the-loop](https://www.agentmissioncontrol.ai/developers/foundry-human-in-the-loop/). ## 7. Schedule it Schedule from AMC, which streams the run and shows a run that ends `response.failed` as failed. A raw scheduler sends `{"input":"{}","background":true}` and reads the envelope's `status`. ## Related - [Register your agent: the technical reference](https://www.agentmissioncontrol.ai/developers/register-your-agent/) - [Deploy to Foundry](https://www.agentmissioncontrol.ai/developers/foundry-deploy/) - [Troubleshoot a Foundry hybrid agent](https://www.agentmissioncontrol.ai/developers/foundry-troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-verify-and-register/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Troubleshoot a Foundry hybrid agent Agent Mission Control developer docs > Claude skill on Microsoft Foundry > Symptoms you are likely to hit, what causes each, the fix, and where to find the container logs. **Before you start:** A deployed agent and read access to the project's Application Insights workspace. ## Symptoms and fixes | Symptom | Cause | Fix | | --- | --- | --- | | Runs die at about 15 minutes: stream cut, background stuck `in_progress`, no envelope | No session idle timeout on the version (default 900 s) | Set `SESSION_IDLE_TIMEOUT_SECONDS=3600` and redeploy | | `internal server error` on every request; container looks healthy | Protocol declared as `responses` `1.0.0` | Declare `2.0.0` | | Version rejected: `reserved for platform use` | An `AGENT_*` or `FOUNDRY_*` name in `environment_variables` | Move it to the Dockerfile `ENV` | | `image_pull_failed`, `UnauthorizedAcrPull`, `InvalidAcrPullCredentials` | Account and project managed identities lack classic AcrPull | Grant **AcrPull** on the registry to both | | `401 Principal does not have access to API/Operation` | The model grant went to the blueprint, or to the project | Grant `Foundry User` to the **instance identity** at **account** scope | | Long run dies with `EACCES` then `Prompt is too long` | The CLI's config dir is under the unwritable `/home/session` | Keep `HOME` and `CLAUDE_CONFIG_DIR` under `/work` in `build_options` | | `JSON message exceeded maximum buffer size` | SDK default 1 MiB buffer | Keep the template's 64 MiB `max_buffer_size` | | Every request refused: `MAX_TURNS and MAX_USD_PER_RUN must be set` | Ceilings missing from the version | Set both and redeploy | | Every request refused: request field collides with Starting step fields | A request field named `title`, `delivered`, `model` or `agent_version` | Rename it and rebuild | | Run fails mid-way on a model that "doesn't exist" | An unpinned tier fell back to a missing deployment | Pin all three `ANTHROPIC_DEFAULT_*_MODEL` | | `ZoneInfoNotFoundError` in the container | No `tzdata` in the image | Install `tzdata` in the Dockerfile | | Failed publish but AMC shows a green run | Turn ended normally, so the stream ended `response.completed` | Raise `RunFailed` after the markers in `_run_stream` | | Run costs several times normal, over a hundred Bash calls, `skills_bypassed` set | The prompt led with a command and the skill never loaded | Restore the "FIRST, invoke the skill" opening | | Run dies with no envelope after fanning out | Task enabled for a skill that doesn't delegate | Put `Task` back in `disallowed_tools` | | MCP calls start failing 401 near the end of a long run | The per-turn token expired (about 60 to 90 minutes) | Lower `RUN_TIMEOUT_SECONDS` or put a refresh proxy in front of the server | | HITL agent ends green having asked nobody | The brain didn't write `hitl_request.json` | Enforce a mandatory pause in `_postprocess` | | Steps fragment into many `(2)`, `(3)` repeats | Phase idle close changed | Keep `PHASE_IDLE_MESSAGES = 2` | | Cost in AMC far too low | `input_tokens` reported without cached tokens | Report cache-inclusive `input_tokens` with `cached_input_tokens` as the subset | | Cost in AMC doubled | Run totals or `cost_usd` on the trailing markers while steps carry tokens | Trailing markers carry the residual only | | A probe fails with a bare socket error at start | Cold container still starting | Connect with a generous timeout (the probe uses 180 s) | > **Important:** Don't add code that kills or inspects other processes in the container. Killing same-user processes was tried: it killed the sandbox's managed-identity helper and took the container down on every real run, invisibly. ## Where the logs are Container stdout lands in the Log Analytics workspace behind the project's Application Insights. - **`AppTraces`**, filtered on `AppRoleName == '<agent-name>'`, one `AppRoleInstance` per container. Every step is also logged as `step: <title>`. - **`AppExceptions`**: `Details` holds the stack, `Properties` the response id, agent version and caller. - Lines logged at startup, before the hosting server configures the exporter, reach stdout only. That's why a readiness refusal is logged again per request. - A `ConnectionError` to `169.254.169.254 /metadata/instance` is the App Insights resource detector probing a sandbox that has no metadata service. It's noise. ```bash az monitor log-analytics query -w <workspace-customer-id> \ --analytics-query "AppTraces | where AppRoleName == 'invoice-summarizer-agent' | order by TimeGenerated desc | take 200" ``` Query the workspace rather than the App Insights `traces` blade, which has gone empty mid-investigation. KQL reserves `first` and `latest` as column names. ## Process-loss recovery A hosted run dies with the process serving it (a deploy, a replica recycle) and sticks at `in_progress`. Foundry's `resilient_background` fix is workflow-only and a hybrid agent is a single agent, so it doesn't apply. If you genuinely need process-loss recovery, model the unit as a one-node workflow; see [Microsoft Agent Framework (Python)](https://www.agentmissioncontrol.ai/developers/choose-a-build-path/). ## Related - [Deploy to Foundry](https://www.agentmissioncontrol.ai/developers/foundry-deploy/) - [Invocation modes and session lifetime](https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/) - [Run steps, token markers and cost reporting](https://www.agentmissioncontrol.ai/developers/foundry-steps-and-cost/) --- Source: https://www.agentmissioncontrol.ai/developers/foundry-troubleshooting/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Claude skill, self-hosted: overview and architecture Agent Mission Control developer docs > Claude skill, self-hosted > What the self-hosted path builds, the two HTTP surfaces it can serve, and which one AMC connects to. **Before you start:** A Claude skill folder with a `SKILL.md`, Claude Code, and Docker. The `claude-skills-agent-creator` skill turns an existing Claude skill into a containerized HTTP service. Inside the container, Anthropic's `claude-agent-sdk` loads the skill and runs it. You build the image and host the container yourself. There is no Foundry hosting on this path. The creator is itself a Claude Code skill. You hand it a skill folder (one that contains a `SKILL.md`) and answer a few questions. It copies its own templates into a new agent directory and fills in the placeholders. It does not build the image or deploy anything. ## Two host surfaces: pick one A generated agent serves exactly one of two HTTP surfaces. The creator always asks which one you want. | Surface | Endpoints | Served by | Who calls it | | --- | --- | --- | --- | | **Plain HTTP** (the creator's default, called A2A in the skill) | `GET /health`, `POST /execute` (NDJSON stream), `POST /tasks/{task_id}/cancel` | `http_agent_server.py` plus `__main__.py` | An orchestrator that speaks the `/execute` NDJSON contract | | **AMC variant** | `GET /health`, `GET /workflows`, `POST /runs`, `GET /runs/{id}/status`, `GET /runs/{id}/events` (SSE), and the rest of the AMC contract | `app.py` built on `amc_sdk.build_app` | Agent Mission Control | > **Important:** AMC drives self-hosted endpoints through the AMC contract that `amc_sdk` serves. It does not call `/execute`. To connect a self-hosted skill to AMC, generate the **AMC variant**. See [Serve the agent to AMC with amc_sdk](https://www.agentmissioncontrol.ai/developers/self-hosted-amc-sdk/). ## Architecture `Request path (AMC variant)` ```text AMC ──HTTP + JSON + SSE──▶ app.py (amc_sdk.build_app, port 8080) │ ▼ claude_skills_agent.py (the SDK core) │ query(prompt, options) ▼ claude-agent-sdk ──▶ bundled Claude Code CLI ──▶ Claude │ (Anthropic API or Microsoft Foundry) ▼ /app/.claude/skills/<skill-name>/SKILL.md (baked into the image) ``` The core is the same for both surfaces. It runs `claude-agent-sdk` with `cwd=/app` and `setting_sources=["project"]`, so the SDK discovers the skill at `/app/.claude/skills/<skill-name>/`. Only the host layer around it changes: `http_agent_server.py` for plain HTTP, or `amc_sdk` for AMC. - **Brain:** `claude-agent-sdk>=0.2.138`. The Claude Code CLI ships inside the wheel as a native binary, so the image needs no Node.js. - **Credentials:** Anthropic API direct, or Claude on Microsoft Foundry. Both come in at runtime as environment variables, never baked into the image. - **Skill:** copied unchanged into the image. When the skill expects a particular filesystem, the Dockerfile reshapes the filesystem instead of patching the skill. - **Permissions:** `permission_mode="dontAsk"` denies any tool outside the allowlist instead of prompting, and the container runs as a non-root user. ## How this differs from the Foundry-hosted path The same skill can also run inside Microsoft Foundry, hosted by Microsoft Agent Framework. Both paths use `claude-agent-sdk` as the brain and turn an existing skill into a deployed agent. They differ only in where the agent runs: here it runs in a container you operate and expose over HTTP. For the Foundry variant, see [Claude skill on Microsoft Foundry](https://www.agentmissioncontrol.ai/developers/foundry-hybrid-overview/). ## What the creator asks you | Input | Choices | | --- | --- | | **Source skill path** | Absolute path to the skill folder. If the skill also ships as a plugin, copy from the bare folder, not the `.plugin` wrapper. | | **Credentials backend** | Foundry (the creator's default) or Anthropic-direct. | | **Trigger mode** | Single-purpose (always run the skill) or multi-purpose (hint the skill and let Claude decide). See [The agent core and prompt strategies](https://www.agentmissioncontrol.ai/developers/self-hosted-agent-core/). | | **Host surface** | Plain HTTP (`/execute`) or the AMC variant. Always asked. | It does not ask for a port. A plain-HTTP agent takes the next free port from the port convention; the AMC variant always uses `8080`. ## The generation pipeline 1. **Analyze the skill.** Read `SKILL.md` and `scripts/`: the frontmatter `name:` and `description:`, hardcoded paths (`grep -r "/sessions/" scripts/`), third-party Python imports, binaries the scripts call, credential needs, and where outputs go. 2. **Decide naming.** Directory, class, package, and image names derive from the skill's `name:`. See [Project layout and placeholders](https://www.agentmissioncontrol.ai/developers/self-hosted-project-layout/). 3. **Generate the scaffold.** Copy verbatim files unchanged. Fill each `.tpl` file and write it without the `.tpl` suffix. 4. **Patch the skill-specific blocks:** the prompt strategy, Dockerfile prep, tool labels, and outputs handling. 5. **Verify** that the generated Python parses, and optionally run `tests/test_run.py` if credentials are available. 6. **Record the port** for a plain-HTTP agent. ## Rules the generated agent keeps - Never modify `http_agent_server.py`. It is the shared transport contract. - Never bake secrets into the image. `.env` is excluded by `.dockerignore`. - Never patch the source skill. Match the filesystem to what it expects. - Never ship `tests/` in the image. `.dockerignore` excludes it. For how agents connect to AMC in general, read [How agents connect](https://www.agentmissioncontrol.ai/developers/how-agents-connect/) and [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/). To compare build paths, see [Choose a build path](https://www.agentmissioncontrol.ai/developers/choose-a-build-path/). ## Related - [Project layout and placeholders](https://www.agentmissioncontrol.ai/developers/self-hosted-project-layout/) - [Serve the agent to AMC with amc_sdk](https://www.agentmissioncontrol.ai/developers/self-hosted-amc-sdk/) - [Run a Claude skill as a Microsoft Foundry hosted agent](https://www.agentmissioncontrol.ai/developers/foundry-hybrid-overview/) - [Choose a build path](https://www.agentmissioncontrol.ai/developers/choose-a-build-path/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-overview/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Project layout and placeholders Agent Mission Control developer docs > Claude skill, self-hosted > Every file the creator generates, what each one does, and how each placeholder is filled. **Before you start:** Read the [overview](/developers/self-hosted-overview/) first. The creator writes a new directory, by default `azurefoundry_<short>_skills/`, next to the source skill in your repository. The skill folder stays where it is; the Dockerfile copies it into the image at build time. That is why every build runs from the repository root. `Generated layout (plain-HTTP surface)` ```text <repo-root>/ ├── skills/report-generator/ # the source skill (unchanged) │ ├── SKILL.md │ └── scripts/ └── azurefoundry_report_generator_skills/ ├── __main__.py # entry point: health probe, run_turn, uvicorn ├── claude_skills_agent.py # the SDK core: options, prompt, events, tokens ├── claude_skills_agent_executor.py # shared agent instance + per-context session map ├── http_agent_server.py # transport: /health, /execute, /tasks/{id}/cancel ├── Dockerfile ├── .dockerignore ├── .env.example ├── pyproject.toml # what the image installs from ├── requirements.txt # kept in step with pyproject.toml ├── README.md └── tests/ # local diagnostics, never shipped in the image ├── README.md ├── test_run.py ├── test_foundry_direct.py ├── test_anthropic_sdk_foundry.py ├── inspect_foundry_client.py └── debug_sdk.py ``` For the AMC variant, keep `claude_skills_agent.py` and the build files, skip `http_agent_server.py`, `claude_skills_agent_executor.py`, and the HTTP parts of `__main__.py`, and add `app.py`. See [Serve the agent to AMC with amc_sdk](https://www.agentmissioncontrol.ai/developers/self-hosted-amc-sdk/). ## What each file does | File | Kind | Job | | --- | --- | --- | | `claude_skills_agent.py` | Templated | Builds `ClaudeAgentOptions`, wraps the user message in the prompt strategy, runs `query()`, translates SDK messages into wire events, and reads tokens and cost off `ResultMessage`. | | `claude_skills_agent_executor.py` | Templated | Owns one shared agent instance behind a lock, initializes it at startup, and maps each `context_id` to a session id. | | `__main__.py` | Templated | Loads `.env`, defines the health probe, maps agent strings to NDJSON events in `_run_turn`, and starts uvicorn. | | `http_agent_server.py` | Verbatim | The plain-HTTP transport: request validation, NDJSON streaming, concurrency cap, and cancellation. | | `Dockerfile` | Templated | `python:3.12-slim` image with the skill baked in at `/app/.claude/skills/<skill-name>/`, running as a non-root user. | | `.dockerignore` | Verbatim | Keeps `.env`, `tests/`, `.git/`, virtualenvs, and caches out of the image. | | `pyproject.toml` | Templated | The dependency list the image installs (`uv pip install --system -e .`). | | `requirements.txt` | Templated | The same dependencies for local installs. The image never reads it. | | `.env.example` | Templated | Every environment variable the agent reads, with defaults. Copy to `.env`. | | `README.md` | Templated | Build, local smoke test, and troubleshooting for this agent. | | `tests/test_run.py` | Templated | Phased local smoke test: env, SDK auth, skill discovery, adapter wiring. | | `tests/*.py` (other four), `tests/README.md` | Verbatim | Credential and SDK diagnostics. See [Test the agent locally](https://www.agentmissioncontrol.ai/developers/self-hosted-testing/). | > **Note:** A template is not valid Python until it is filled. `class {{AGENT_CLASS}}:` is a syntax error until `{{AGENT_CLASS}}` becomes a name. ## Simple placeholders These are plain search-and-replace across every `.tpl` file. The examples are for a skill whose frontmatter says `name: report-generator`. | Placeholder | What it is | Example | | --- | --- | --- | | `{{AGENT_DIR}}` | New agent directory | `azurefoundry_report_generator_skills` | | `{{AGENT_CLASS}}` | Agent class name | `ReportGeneratorSkillsAgent` | | `{{EXECUTOR_CLASS}}` | Executor class name | `ReportGeneratorSkillsExecutor` | | `{{CREATE_EXECUTOR_FN}}` | Executor factory function | `create_report_generator_executor` | | `{{INIT_FN}}` | Startup-init function | `initialize_report_generator_agents_at_startup` | | `{{SKILL_NAME}}` | The skill's frontmatter `name:`, exactly | `report-generator` | | `{{SKILL_SOURCE_PATH}}` | Skill folder, relative to the repo root | `skills/report-generator` | | `{{AGENT_NAME_HUMAN}}` | Display name | `Report Generator Agent` | | `{{AGENT_DESCRIPTION}}` | One-line description | `Builds a weekly report with the report-generator skill.` | | `{{PORT}}` | TCP port for the plain-HTTP server | `9024` | | `{{PACKAGE_NAME}}` | `pyproject.toml` name | `claude-skills-report-generator-agent` | | `{{PACKAGE_DESCRIPTION}}` | `pyproject.toml` description | `Report generator agent powered by claude-agent-sdk` | | `{{AGENT_DOCKER_TAG}}` | Image tag prefix and container app name | `report-generator-skills-agent` | | `{{SKILL_DISPLAY_NAME}}` | Skill entry name in the README's registry snippet | `Report Generator` | | `{{SKILL_DESCRIPTION_SHORT}}` | Skill entry description | `Weekly report rendered as HTML.` | | `{{SKILL_ID_SNAKE}}` | Skill entry id | `report_generator` | | `{{SKILL_TAGS}}` | Comma-separated tags | `reports, claude, skill` | ## Naming rules | Identifier | Rule | Example | | --- | --- | --- | | Skill `name:` | kebab-case, used in full wherever `{{SKILL_NAME}}` appears | `report-generator` | | `{{AGENT_DIR}}` | `azurefoundry_` + snake_case + `_skills` | `azurefoundry_report_generator_skills` | | `{{AGENT_CLASS}}` | PascalCase, no underscores, suffix `SkillsAgent` | `ReportGeneratorSkillsAgent` | | `{{PACKAGE_NAME}}` | `claude-skills-` + kebab-case + `-agent` | `claude-skills-report-generator-agent` | | `{{AGENT_DOCKER_TAG}}` | short kebab-case + `-skills-agent` | `report-generator-skills-agent` | | Container app name | `<short>-skills-agent-uami` | `report-generator-skills-agent-uami` | If the derived names get unwieldy, strip filler words from the agent-side identifiers only. `{{SKILL_NAME}}` always keeps the skill's full name, because the SDK matches it. ## Block markers Block markers are replaced with code rather than a single value. Each sits next to a comment in the template that explains the choice. | Marker | File | Replace with | | --- | --- | --- | | `{{OPTIONAL_OUTPUTS_DIR_CONST}}` | `claude_skills_agent.py` | `OUTPUTS_DIR = os.getenv("REPORT_OUTPUTS_DIR", "/outputs")` if the skill writes files, else remove | | `{{OPTIONAL_MAKEDIRS}}` | `claude_skills_agent.py`, in `create_agent()` | `os.makedirs(OUTPUTS_DIR, exist_ok=True)`, or remove | | `{{SKILL_ENV_VARS}}` | `claude_skills_agent.py`, in `ClaudeAgentOptions(env=...)` | Env vars the skill's scripts read at runtime, or nothing | | `{{PROMPT_STRATEGY_BODY}}` | `claude_skills_agent.py`, `_build_prompt()` | Pattern A or B from the prompt strategies | | `{{TOOL_SCRIPT_LABELS}}` | `claude_skills_agent.py`, `_describe_tool()` | A map of `scripts/*.py` filenames to short progress labels | | `{{OPTIONAL_OUTPUTS_HEALTH_CHECK}}` | `__main__.py`, `_health()` | An outputs-dir writability check, or remove | | `{{OPTIONAL_OUTPUTS_MISSING_CHECK}}` | `__main__.py`, `_health()` | The matching `missing.append(...)` line, or remove | | `{{DOCKERFILE_SKILL_PREP_BLOCK}}` | `Dockerfile` | One or more filesystem prep patterns, concatenated | | `{{OPTIONAL_OUTPUTS_ENV}}` | `Dockerfile` | `ENV REPORT_OUTPUTS_DIR=/outputs`, or remove | | `{{SKILL_EXTRA_DEPS}}` | `pyproject.toml` and `requirements.txt` | Only the packages the skill's scripts import, one per line | | `{{AGENT_TURN_DESCRIPTION}}` | `README.md` | What one run does, end to end | | `{{PREREQUISITES_BLOCK}}` | `README.md` | Volume mounts, network access, external accounts | | `{{ADDITIONAL_VOLUMES_BLOCK}}` | `README.md`, in the `docker run` example | `-v "${PWD}/outputs:/outputs"` and similar, or empty | A few more markers appear in the templates: `{{NONROOT_EXTRA_DIRS}}` (extra directories the non-root user must own, in the Dockerfile), `{{OUTPUTS_ENV_VAR}}` and `{{OPTIONAL_OUTPUTS_ENV_VAR}}` (the outputs variable's name, in the agent and `.env.example`), `{{OPTIONAL_OUTPUTS_ENV_SETUP}}` (pointing that variable at a temp dir in `tests/test_run.py`), and `{{PROMPT_DOCSTRING}}` (the `_build_prompt()` docstring). > **Note:** If the skill produces no files, remove every outputs-related marker together. ## Related - [The agent core and prompt strategies](https://www.agentmissioncontrol.ai/developers/self-hosted-agent-core/) - [Build the Docker image](https://www.agentmissioncontrol.ai/developers/self-hosted-docker/) - [Claude skill, self-hosted: overview and architecture](https://www.agentmissioncontrol.ai/developers/self-hosted-overview/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-project-layout/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # The agent core and prompt strategies Agent Mission Control developer docs > Claude skill, self-hosted > How `claude_skills_agent.py` configures `claude-agent-sdk`, steers each message at the skill, and reads tokens and cost. **Before you start:** A generated agent directory. See [Project layout and placeholders](/developers/self-hosted-project-layout/). `claude_skills_agent.py` is the host-agnostic core. Both surfaces use it unchanged. It holds four module constants, all overridable at deploy time without a rebuild: | Constant | Env var | Default | | --- | --- | --- | | `SKILL_PARENT_DIR` | `CLAUDE_AGENT_CWD` | `/app` | | `DEFAULT_MODEL` | `CLAUDE_AGENT_MODEL` | `claude-sonnet-4-6` | | `MAX_TURNS` | `CLAUDE_AGENT_MAX_TURNS` | `50` | | `SKILL_NAME` | none (filled from `{{SKILL_NAME}}`) | the skill's `name:` | `create_agent()` runs once at startup. It confirms `claude-agent-sdk` imports, raises if neither `ANTHROPIC_API_KEY` nor `CLAUDE_CODE_USE_FOUNDRY=1` is set, and logs a warning if `SKILL.md` is missing from `<cwd>/.claude/skills/<skill-name>/`. In that case the agent still starts, but skill loading fails. ## The SDK options `claude_skills_agent.py (inside run_conversation_stream)` ```python base_tools = ["Bash", "Read", "Write", "Glob", "Grep", "Skill"] kwargs: Dict[str, Any] = dict( model=DEFAULT_MODEL, cwd=SKILL_PARENT_DIR, # /app, where .claude/skills/ lives tools=base_tools, # the tools DEFINED in the request allowed_tools=base_tools, # the tools PERMITTED to run setting_sources=["project"], # load skills from <cwd>/.claude/skills/ system_prompt={"type": "preset", "preset": "claude_code"}, permission_mode="dontAsk", # deny outside the allowlist, never prompt max_turns=MAX_TURNS, env={ "DISABLE_AUTOUPDATER": "1", "CLAUDE_CODE_MAX_RETRIES": os.getenv("CLAUDE_CODE_MAX_RETRIES", "5"), "REPORT_OUTPUTS_DIR": OUTPUTS_DIR, # from {{SKILL_ENV_VARS}} }, ) budget = (os.getenv("CLAUDE_AGENT_MAX_BUDGET_USD") or "").strip() if budget: kwargs["max_budget_usd"] = float(budget) effort = (os.getenv("CLAUDE_AGENT_EFFORT") or "").strip().lower() if effort in ("low", "medium", "high", "xhigh", "max"): kwargs["effort"] = effort buffer_bytes = (os.getenv("CLAUDE_AGENT_MAX_BUFFER_BYTES") or "").strip() if buffer_bytes: kwargs["max_buffer_size"] = int(buffer_bytes) options = ClaudeAgentOptions(**kwargs) ``` | Option | Why it is set this way | | --- | --- | | `tools=` | Set explicitly. Unset, the whole Claude Code toolset is defined in every prompt, on the order of 16k prompt tokens. `allowed_tools` alone does not prevent that. | | `allowed_tools=` | Must include `"Skill"`, or the SDK cannot route to the skill. | | `setting_sources=["project"]` | Makes the SDK discover `/app/.claude/skills/<skill-name>/`. | | `permission_mode="dontAsk"` | Denies anything outside the allowlist instead of prompting. A permission prompt in an unattended session is a hang. Do not use `"bypassPermissions"`: the bundled CLI refuses it as root. | | `DISABLE_AUTOUPDATER=1` | A self-updating CLI inside an immutable image breaks reproducibility. | | `CLAUDE_CODE_MAX_RETRIES` | Bounds retries at 5. Unset, the CLI makes about 10 attempts with doubling backoff, and a hard failure looks like a hang. | | `max_budget_usd`, `effort`, `max_buffer_size` | Optional, applied only when their env vars are set. See [Configuration and environment](https://www.agentmissioncontrol.ai/developers/self-hosted-configuration/). | ## Event translation For the plain-HTTP surface, `run_conversation_stream()` yields strings with a small wire format, which `__main__.py` maps to NDJSON events: | String | Meaning | | --- | --- | | `🤖 <first 1000 chars of the message>` | First event of every turn | | `🛠️ Remote agent executing: <label>` | One per distinct tool call, deduplicated by label | | `<final text>` | The terminal answer: every `TextBlock`, joined with a blank line | | `Error: ...` | A terminal error | - `AssistantMessage.error` is checked on every assistant message. It carries a category such as `authentication_failed`, `billing_error`, `rate_limit`, `invalid_request`, `server_error`, or `unknown`. Without this check an auth or billing failure ends the run later with the vaguer `produced no text response`. - A run that yields no text ends with `Error: agent completed but produced no text response`. - When `ResultMessage.is_error` is set, the core logs its `subtype` and `result` so the real reason is visible. - When a run uses 90% or more of `MAX_TURNS`, the core logs an `approaching cap` warning. ## Reading tokens and cost `ResultMessage` has no flat `input_tokens` or `output_tokens` attributes. Reading them with `getattr(msg, "input_tokens", 0)` returns 0 on every run, silently. Tokens live in `model_usage` (per model, camelCase, covers the whole tree including sub-agents), with `usage` (snake_case, top-level loop only) as the fallback. Input is the sum of uncached, cache-read, and cache-creation tokens: `inputTokens` alone is only the small uncached remainder. `claude_skills_agent.py (_read_usage)` ```python model_usage = getattr(msg, "model_usage", None) if isinstance(model_usage, dict) and model_usage: inp = out = 0 for entry in model_usage.values(): if not isinstance(entry, dict): continue inp += (int(entry.get("inputTokens") or 0) + int(entry.get("cacheReadInputTokens") or 0) + int(entry.get("cacheCreationInputTokens") or 0)) out += int(entry.get("outputTokens") or 0) if inp or out: return inp, out # fallback: msg.usage with input_tokens, cache_read_input_tokens, # cache_creation_input_tokens, output_tokens (excludes sub-agents) ``` The core stores the result on the instance: `last_token_usage` (`prompt_tokens`, `completion_tokens`, `total_tokens`), `last_cost_usd` from `total_cost_usd`, and `last_num_turns`. ## Progress labels `_describe_tool()` turns each tool call into a short label. Replace `{{TOOL_SCRIPT_LABELS}}` with a map from the skill's script names to labels. Unmapped Bash calls fall back to `Bash: <first line of the command>`; `Skill` calls read `Loading skill: <name>`; file tools read `<Tool>: <path or pattern>`. `claude_skills_agent.py (_describe_tool)` ```python if tool_name == "Bash": cmd = (tool_input or {}).get("command", "") or "" for script, label in ( ("fetch_sources.py", "Fetching sources"), ("render.py", "Rendering output"), ): if script in cmd: return label ``` ## Prompt strategies `_build_prompt()` decides how every incoming message reaches the SDK. Replace `{{PROMPT_STRATEGY_BODY}}` with one of two patterns, matching the trigger mode you chose. ### Pattern A: single-purpose Use it when the agent does one thing and every run should run the skill end to end. Even an empty message or a bare `go` triggers the full pipeline. `Pattern A` ```python def _build_prompt(self, user_message: str) -> str: directive = ( f"You are a single-purpose agent. Use the **{SKILL_NAME}** skill " f"to handle the user's request. Read its SKILL.md and run the " f"full pipeline. Always run the full pipeline; do not ask the " f"user to clarify unless a required argument is genuinely " f"ambiguous.\n\nUser request:\n" ) return directive + (user_message or "").strip() ``` ### Pattern B: multi-purpose Use it when the skill has several command surfaces or the request varies from run to run. Claude invokes the skill only when the request matches it, and can decline off-topic requests. `Pattern B` ```python def _build_prompt(self, user_message: str) -> str: hint = ( f"You have access to the {SKILL_NAME} skill. Use it for " f"reporting requests. Always pass --format html.\n\n" ) return hint + (user_message or "").strip() ``` | Question | Single-purpose | Multi-purpose | | --- | --- | --- | | Is there exactly one outcome this agent produces? | Yes | | | Does the user's intent vary turn to turn? | | Yes | | Does empty or minimal input still have a sensible default action? | Yes | | | Should the agent refuse off-topic requests? | | Yes | | Does the skill have many command surfaces (CRUD, reports, queries)? | | Yes | > **Note:** When in doubt, choose multi-purpose. It does not run when nobody asked, and it degrades to doing nothing instead of running the wrong thing. Whichever pattern you pick, confirm that `SKILL_NAME` matches the skill's frontmatter `name:` exactly, that `allowed_tools` includes `"Skill"`, and that `setting_sources=["project"]` is set. ## Related - [The plain-HTTP surface and executor](https://www.agentmissioncontrol.ai/developers/self-hosted-http-surface/) - [Serve the agent to AMC with amc_sdk](https://www.agentmissioncontrol.ai/developers/self-hosted-amc-sdk/) - [Configuration and environment](https://www.agentmissioncontrol.ai/developers/self-hosted-configuration/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-agent-core/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # The plain-HTTP surface and executor Agent Mission Control developer docs > Claude skill, self-hosted > The `/health`, `/execute`, and cancel endpoints, the NDJSON event stream, and how the executor and entry point wire them up. **Before you start:** Read [The agent core and prompt strategies](/developers/self-hosted-agent-core/). The creator's default surface is a plain HTTP server built on Starlette. The skill calls it A2A; the server's own docstring describes it as a plain-HTTP server that replaces A2A. It streams newline-delimited JSON. AMC does not call this surface: for AMC, use [the AMC variant](https://www.agentmissioncontrol.ai/developers/self-hosted-amc-sdk/). ## Endpoints | Method and path | Behavior | | --- | --- | | `GET /health` | Calls the health probe and returns its JSON, always with HTTP 200. Readiness is in the body's `ready` field. A probe that raises returns `status: "down"`. | | `POST /execute` | Runs one turn and streams events as `application/x-ndjson`. | | `POST /tasks/{task_id}/cancel` | Best-effort cancel. Returns `{"ok": true, "task_id": ..., "in_flight": ...}`. | ## The /execute request `POST /execute` ```json { "context_id": "ctx-1", "task_id": "task-1", "message": "Build this week's report", "files": [{ "name": "brief.pdf", "uri": "https://<your-storage>.example.com/brief.pdf", "mime": "application/pdf" }], "metadata": { "triggered_by_email": "user@contoso.com" }, "resume_token": null } ``` `context_id` and `task_id` are required; a missing one returns 400, as does a body that is not a JSON object. The other fields are optional. ## The NDJSON event stream | `kind` | Fields | Terminal | | --- | --- | --- | | `status` | `text` | No | | `file` | `name`, `uri`, `mime` | No | | `token_usage` | `prompt_tokens`, `completion_tokens`, `total_tokens` | No | | `result` | `text`, `summary` | Yes | | `error` | `message` | Yes | | `hitl_input_required` | `wait_info`, `resume_token` | Yes | | `paused` | `reason`, `resume_token` | Yes | `Example stream` ```text {"kind": "status", "text": "🤖 Build this week's report"} {"kind": "status", "text": "🛠️ Remote agent executing: Loading skill: report-generator"} {"kind": "status", "text": "🛠️ Remote agent executing: Fetching sources"} {"kind": "status", "text": "🛠️ Remote agent executing: Rendering output"} {"kind": "token_usage", "prompt_tokens": 41200, "completion_tokens": 2300, "total_tokens": 43500} {"kind": "result", "text": "Report written to /outputs/report.html ...", "summary": "Report written to /outputs/report.html ..."} ``` - The run must yield exactly one terminal event. The server drops anything after it. - If the run ends without a terminal event, the server appends `{"kind": "error", "message": "agent completed without terminal event"}`. - An exception inside the run becomes `error` with `<ExceptionType>: <message>`. - `summary` is the first 200 characters of the final text. - The template never emits `hitl_input_required` or `paused`. They exist in the contract only. ## Concurrency and cancellation A semaphore caps concurrent `/execute` streams in one process at `AGENT_MAX_CONCURRENCY` (default `3`, with a fallback to 3 if the value is not an integer). Extra requests queue instead of running in parallel without bound. A cancel is recorded against the `task_id` and checked before each event is sent. When it is seen, the stream ends with `{"kind": "error", "message": "cancelled"}`. A cancel that arrives before `/execute` starts is kept for 300 seconds, so the race is handled and the map stays bounded. ## The entry point `__main__.py` loads `.env`, initializes the shared agent at startup, and starts uvicorn on `AGENT_HOST` (default `0.0.0.0`) and `AGENT_PORT` (default `{{PORT}}`). If startup init fails, it prints a warning and starts anyway; the first request then tries to create the agent lazily. `__main__.py (health probe, outputs check filled in)` ```python async def _health() -> Dict[str, Any]: use_foundry = os.getenv("CLAUDE_CODE_USE_FOUNDRY") == "1" has_api_key = bool(os.getenv("ANTHROPIC_API_KEY")) has_foundry_resource = bool(os.getenv("ANTHROPIC_FOUNDRY_RESOURCE")) has_foundry = use_foundry and has_foundry_resource and has_api_key has_anthropic_direct = has_api_key and not use_foundry outputs_dir = os.getenv("REPORT_OUTPUTS_DIR", "/outputs") outputs_writable = os.path.isdir(outputs_dir) and os.access(outputs_dir, os.W_OK) missing: List[str] = [] if not (has_anthropic_direct or has_foundry): missing.append("ANTHROPIC_API_KEY (Anthropic-direct), or " "CLAUDE_CODE_USE_FOUNDRY=1 + ANTHROPIC_FOUNDRY_RESOURCE + ANTHROPIC_API_KEY (Foundry)") if not outputs_writable: missing.append(f"writable outputs dir at {outputs_dir}") if not missing: return {"status": "ok", "ready": True, "detail": "Fully configured"} return {"status": "not_configured", "ready": False, "detail": f"Missing: {', '.join(missing)}"} ``` `_run_turn` maps the core's strings to events: tool lines become deduplicated `status` events, lines starting with the progress emoji (🤖, 🧠, 🔍, 📝, 📤, 📥) become `status`, an `Error:` line becomes a terminal `error`, and anything else is collected as response text. After the stream it emits `token_usage` (when the core captured it) and then `result` with the last response. ## The executor `claude_skills_agent_executor.py` holds one shared agent instance at class level, guarded by an `asyncio.Lock`. `initialize_at_startup()` builds it once. `_get_or_create_thread()` maps each `context_id` to a session id. Each `/execute` call is still an independent single turn: the generated README lists single-turn behavior and ephemeral state as known limitations. > **Important:** Never edit `http_agent_server.py`. It is copied verbatim into every agent. If you need different request or response shapes, change the contract at the platform level. ## Related - [The agent core and prompt strategies](https://www.agentmissioncontrol.ai/developers/self-hosted-agent-core/) - [Test the agent locally](https://www.agentmissioncontrol.ai/developers/self-hosted-testing/) - [Serve the agent to AMC with amc_sdk](https://www.agentmissioncontrol.ai/developers/self-hosted-amc-sdk/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-http-surface/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Serve the agent to AMC with amc_sdk Agent Mission Control developer docs > Claude skill, self-hosted > Swap the plain-HTTP host for `app.py` on `amc_sdk.build_app`, map SDK events to AMC steps, and report tokens and cost AMC can price. **Before you start:** A generated agent core, and the `amc_sdk` package that ships with AMC. The AMC variant keeps the `claude-agent-sdk` core and replaces the host. `app.py` describes the agent to `amc_sdk`, drives the core inside a run handler, and reports through a `RunContext`. `amc_sdk.build_app()` serves the rest of the AMC contract. This page covers only what is specific to a Claude skill; the general contract is in [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/). > **Note:** `amc_sdk` is AMC's library for exposing an endpoint AMC can drive. It is unrelated to Anthropic's `claude-agent-sdk`, which runs the skill. The AMC variant uses both: `claude-agent-sdk` inside, `amc_sdk` as the HTTP surface. ## What build_app serves | Endpoint | Purpose | | --- | --- | | `GET /health` | Readiness. AMC probes it when you register. | | `GET /workflows`, `GET /workflows/{id}` | Definition discovery, built from your `Agent` config | | `GET /agent-types` | Agent type entries | | `POST /runs` | Starts a run. Body: `{"inputs": {...}}`, a flat string map. Returns `orchestratorRunId` and `status`. | | `POST /runs/{id}/cancel` | Best-effort cancel | | `POST /runs/{id}/messages` | A human's answer to a paused step | | `GET /runs/{id}/status` | Per-step status, tokens, and timestamps | | `GET /runs/{id}/events` | SSE stream of run events | | `GET /runs/{id}/content`, `GET /runs/{id}/steps/{stepId}/content` | Produced content | | `GET /runs/{id}/tokens` | Token usage for ROI and budgets | The run store, step state, event recorder, camelCase DTOs, and SSE loop all live in `amc_sdk`. Your `app.py` keeps only the `Agent(...)` definition, input parsing, the `@agent.run` handler, and a health probe. ## Files - Keep `claude_skills_agent.py` as generated. - Skip `http_agent_server.py`, `claude_skills_agent_executor.py`, and the HTTP parts of `__main__.py`. - Add `app.py`. ## Map SDK events to the run | claude-agent-sdk event | RunContext call | | --- | --- | | Run starts | `ctx.step_started(STEP_ID, agent_name=...)` | | `ThinkingBlock` | `ctx.thinking(STEP_ID, block.thinking)`: a `thinking` detail row | | `ToolUseBlock` plus its `ToolResultBlock` | `ctx.tool_activity(STEP_ID, tool=block.name, text=<args>, result=<result>)`: one `tool` row with name, input, and result | | `TextBlock` | `ctx.token_delta(STEP_ID, text)` | | `ResultMessage` (success) | `ctx.step_completed(STEP_ID, token_count=i+o, delivered=<final text>, input_tokens=i, output_tokens=o, model=<model id>, cached_input_tokens=<cache read>, model_usage=<optional>)` | | Final output | `ctx.set_output({...})` and `ctx.set_cost(total_cost_usd)` | `ToolResultBlock`s arrive on a `UserMessage`, not on the `AssistantMessage` that made the call. Buffer each `ToolUseBlock` by `block.id`, then emit one `ctx.tool_activity(...)` when the block whose `tool_use_id` matches lands. AMC renders these rows as collapsible sub-steps under the step's `delivered` text. Keep the capture fail-safe so it never breaks a run, and gate it behind an `EMIT_STEP_ACTIVITY_LOG` env flag so you can turn it off without a redeploy. ## Example app.py The template's core yields display strings for the plain-HTTP surface. For this variant, factor the options-building part of `run_conversation_stream()` into a method (called `build_options()` here) so `app.py` can run `query()` and see raw SDK messages. Put the buffering in the core's query loop, as the skill recommends, if you prefer; the routing to `ctx` belongs in `app.py`. `app.py` ```python import os import uvicorn from amc_sdk import Agent, AgentType, HealthReport, InputParam, RunContext, StepDef, build_app from claude_agent_sdk import query from claude_skills_agent import ( DEFAULT_MODEL, SKILL_NAME, SKILL_PARENT_DIR, ReportGeneratorSkillsAgent, ) STEP_ID = "main" EMIT_ACTIVITY = os.getenv("EMIT_STEP_ACTIVITY_LOG", "1") == "1" core = ReportGeneratorSkillsAgent() def _health() -> HealthReport: has_creds = bool(os.getenv("ANTHROPIC_API_KEY")) skill_md = os.path.join(SKILL_PARENT_DIR, ".claude", "skills", SKILL_NAME, "SKILL.md") if has_creds and os.path.isfile(skill_md): return HealthReport(ready=True, detail="Fully configured") return HealthReport(ready=False, detail="Missing Claude credentials or SKILL.md") amc_agent = Agent( workflow_id="report-generator", # catalog key, globally unique name="Report Generator", description="Builds a weekly report with the report-generator skill.", type="workflow", steps=[StepDef(STEP_ID, "Generate report")], agent_types=[AgentType("report-generator", "Report Generator", ["text", "tool-use"])], input_parameters=[InputParam("topic", required=False)], health=_health, ) def _read_model_usage(msg) -> dict[str, dict[str, int]]: """{model: {input, output, cached}}; input includes cache reads and writes.""" out: dict[str, dict[str, int]] = {} for model, e in (getattr(msg, "model_usage", None) or {}).items(): if not isinstance(e, dict): continue cached = int(e.get("cacheReadInputTokens") or 0) out[model] = { "input": int(e.get("inputTokens") or 0) + cached + int(e.get("cacheCreationInputTokens") or 0), "output": int(e.get("outputTokens") or 0), "cached": cached, } return out @amc_agent.run async def _run(ctx: RunContext) -> None: await core.create_agent() prompt = core._build_prompt(ctx.inputs.get("topic", "")) pending: dict[str, tuple[str, str]] = {} # tool_use_id -> (name, input) text: list[str] = [] usage: dict[str, dict[str, int]] = {} cost = None ctx.step_started(STEP_ID, agent_name="Report Generator") try: async for msg in query(prompt=prompt, options=core.build_options()): kind = type(msg).__name__ if kind == "ResultMessage": usage = _read_model_usage(msg) cost = getattr(msg, "total_cost_usd", None) continue content = getattr(msg, "content", None) if not isinstance(content, list): continue for block in content: b = type(block).__name__ if b == "TextBlock" and block.text.strip(): text.append(block.text.strip()) ctx.token_delta(STEP_ID, block.text) elif not EMIT_ACTIVITY: continue elif b == "ThinkingBlock": ctx.thinking(STEP_ID, block.thinking) elif b == "ToolUseBlock": pending[block.id] = (block.name, str(block.input)) elif b == "ToolResultBlock" and block.tool_use_id in pending: name, args = pending.pop(block.tool_use_id) ctx.tool_activity(STEP_ID, tool=name, text=args, result=str(block.content)) except Exception: i, o, cached = _totals(usage) ctx.set_step_tokens(STEP_ID, token_count=i + o, input_tokens=i, output_tokens=o, model=DEFAULT_MODEL, cached_input_tokens=cached) ctx.set_cost(cost) raise # run failed, error text becomes the reason i, o, cached = _totals(usage) final = "\n\n".join(text) ctx.step_completed( STEP_ID, token_count=i + o, delivered=final, input_tokens=i, output_tokens=o, model=DEFAULT_MODEL, cached_input_tokens=cached, model_usage=usage if len(usage) > 1 else None, # display-only breakdown ) ctx.set_output({"result": final}) ctx.set_cost(cost) def _totals(usage: dict[str, dict[str, int]]) -> tuple[int, int, int]: return (sum(m["input"] for m in usage.values()), sum(m["output"] for m in usage.values()), sum(m["cached"] for m in usage.values())) app = build_app(amc_agent) if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8080) ``` > **Important:** The `Agent` field for the input contract is `input_parameters`. Some skill notes write `input_params`, which `Agent` does not accept. ## Two rules that carry the money - **Report `input_tokens` and `output_tokens` separately**, not just `token_count`. AMC prices prompt and completion tokens at different per-model rates. - **Pass `model` and `cached_input_tokens`.** Read them off `ResultMessage.model_usage`: input is uncached plus `cacheReadInputTokens` plus `cacheCreationInputTokens`; cached is `cacheReadInputTokens`. A step with no `model` is priced at AMC's default rate. That is wrong money, with no error. When Task sub-agents run on a different model from the orchestrator, one step spans several models. Pass the per-model projection as `model_usage={model: {"input", "output", "cached"}}` and AMC shows it as the per-model table on the run detail. Here it is display-only: AMC prefers the exact `ctx.set_cost(total_cost_usd)` for the invoice over any per-model recompute. Omit it for a single-model run. ## The failure path Raising from the `@agent.run` handler is how a run fails. `amc_sdk` marks the run `failed`, and the error text becomes the failed step's content. AMC records it as the run's failure reason and uses it for the suggested cause it shows the operator. Before re-raising, call `ctx.set_step_tokens(...)` and `ctx.set_cost(...)`, or a failed run is priced at zero. `ctx.step_failed(STEP_ID, detail)` marks a step failed while the run continues. ## Optional: completed with issues When a step failed but the run still produced a usable result, call `ctx.mark_partial("Slack post failed; the report was saved but nobody was notified")` and return normally. The run stays `completed`, and `/status` publishes `outcome: "partial"` with the reason as `failureReason`, so AMC shows **Completed with issues**. The reason must say what wasn't done: a blank one raises `ValueError`. It is trimmed and capped at 2000 characters, and the last call wins. The call doesn't end the run. Raising still fails the run, and on a failed or cancelled run the call is ignored. AMC never derives a partial outcome from failed steps, so a `ctx.step_failed` without `mark_partial` leaves a clean completed run. ## Optional: rename the run Call `ctx.run_renamed("report-generator-weekly")` once the run's subject is known. It sets the run's display name (published as `runName` on `/status`) and emits a `run.renamed` event so a watching browser updates in place. It is sticky (a blank name never clears a set one), idempotent, and has no effect once the run is terminal. It renames only this run. ## Optional: ask a human `answer = await ctx.ask_human(STEP_ID, "Which region should the report cover?")` pauses the step and the run as `waiting`. AMC opens an intervention, and when someone answers it posts to `/runs/{id}/messages` and the call returns their text. See [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) for how AMC routes the question. ## Packaging `amc_sdk` ships with AMC and is not on PyPI. Install it from your copy (`pip install -e <path-to>/amc_sdk`); it brings `fastapi` and `pydantic`. Build the image from a context that includes the `amc_sdk/` folder. `Dockerfile (AMC variant changes)` ```dockerfile COPY amc_sdk/ ./amc_sdk/ RUN pip install ./amc_sdk EXPOSE 8080 CMD ["python", "app.py"] ``` `pyproject.toml` names the agent's modules under `[tool.setuptools] py-modules` (`__main__`, `claude_skills_agent`, `claude_skills_agent_executor`, `http_agent_server`). Keep that list in step with the files you actually keep. There is no Foundry deploy step. AMC reaches the container at the endpoint you register. See [Deploy and register in AMC](https://www.agentmissioncontrol.ai/developers/self-hosted-deploy-and-register/). ## Inbound auth `build_app(agent, verify_caller=...)` takes a FastAPI dependency that runs before every request. The default, `amc_sdk.auth.no_auth`, authorizes everyone. It is a placeholder, not a security control. On the registration probe and on run calls, AMC sends the key in an `api-key` header when you register with **API key**, and `Authorization: Bearer <token>` with **Managed Identity**. Check whichever applies in your own `verify_caller` and raise `fastapi.HTTPException` to reject a caller. ## Checklist - `app.py` on `amc_sdk.build_app`; the plain-HTTP server files skipped. - `step_completed` carries the input and output split, `model`, and `cached_input_tokens`, plus `model_usage` when sub-agents ran on another model. - On failure, `set_step_tokens` and `set_cost` run before `raise`. - The `health=` probe checks credentials and the skill's `SKILL.md`. - The container listens on port `8080`, and the AMC registration points at it. ## Related - [The integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) - [Deploy and register in AMC](https://www.agentmissioncontrol.ai/developers/self-hosted-deploy-and-register/) - [The agent core and prompt strategies](https://www.agentmissioncontrol.ai/developers/self-hosted-agent-core/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-amc-sdk/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Build the Docker image Agent Mission Control developer docs > Claude skill, self-hosted > The generated Dockerfile, the skill-specific prep patterns, the non-root user, and the port convention. **Before you start:** A generated agent directory and Docker. The Dockerfile builds from the **repository root**, so the build context can reach both the agent directory and the source skill. Here it is filled in for a skill that writes reports to `/outputs`: `Dockerfile` ```dockerfile # Report Generator Agent: Claude Agent SDK + report-generator skill # Build from the repo root: # docker build -f azurefoundry_report_generator_skills/Dockerfile -t report-generator-skills-agent . FROM python:3.12-slim WORKDIR /app # No Node.js: claude-agent-sdk>=0.2.138 bundles the Claude Code CLI inside the wheel. RUN apt-get update \ && apt-get install -y --no-install-recommends bash curl ca-certificates \ && rm -rf /var/lib/apt/lists/* # Skill-specific prep ({{DOCKERFILE_SKILL_PREP_BLOCK}}): a writable outputs dir RUN mkdir -p /outputs && chmod 777 /outputs RUN pip install --no-cache-dir uv # Python deps first, for layer caching COPY azurefoundry_report_generator_skills/pyproject.toml azurefoundry_report_generator_skills/uv.lock* ./ RUN uv pip install --system -e . # The source skill, into the SDK discovery path COPY skills/report-generator /app/.claude/skills/report-generator # Agent code COPY azurefoundry_report_generator_skills/ /app/ # Non-root user ({{NONROOT_EXTRA_DIRS}} = /outputs) RUN useradd --create-home --shell /bin/bash --uid 1000 claude \ && chown -R claude:claude /app /home/claude /outputs USER claude ENV HOME=/home/claude ENV AGENT_PORT=9024 ENV AGENT_HOST=0.0.0.0 ENV REPORT_OUTPUTS_DIR=/outputs ENV CLAUDE_AGENT_CWD=/app ENV CLAUDE_AGENT_MODEL=claude-sonnet-4-6 EXPOSE ${AGENT_PORT} CMD ["sh", "-c", "uv run . --host ${AGENT_HOST} --port ${AGENT_PORT}"] ``` - `pyproject.toml` is what the image installs from. It never reads `requirements.txt`, so the `claude-agent-sdk>=0.2.138` floor that matters lives in `pyproject.toml`. Keep the two files in step. - Do not lower the SDK floor. Releases below 0.2.138 need Node.js, which the image no longer installs. - The skill is copied unchanged to `/app/.claude/skills/<skill-name>/`, where `setting_sources=["project"]` finds it. ## Skill prep patterns Replace `{{DOCKERFILE_SKILL_PREP_BLOCK}}` with whichever of these the skill needs. Concatenate several if needed. ### Hardcoded session paths Some skills written in a Claude Cowork session hardcode a session-specific path such as `/sessions/<session-id>/mnt/outputs/...` in their scripts. Create the exact path; do not try to detect it dynamically. ```dockerfile RUN mkdir -p /sessions/<exact-id-from-skill>/mnt/outputs/<rest-of-path> \ && chmod -R 777 /sessions ``` ### A CLI binary the skill calls ```dockerfile ARG TOOL_VERSION=1.2.3 RUN curl -fsSL "https://github.com/<org>/<repo>/releases/download/v${TOOL_VERSION}/<asset>.tar.gz" \ -o /tmp/tool.tgz \ && tar -xzf /tmp/tool.tgz -C /tmp \ && mv /tmp/<bin-name> /usr/local/bin/<bin-name> \ && chmod +x /usr/local/bin/<bin-name> \ && rm /tmp/tool.tgz ``` If the skill's bootstrap looks for the binary at a specific path, put it exactly there instead of on `PATH`. ### A writable outputs directory ```dockerfile RUN mkdir -p /outputs && chmod 777 /outputs ENV REPORT_OUTPUTS_DIR=/outputs ``` Mount a file share at `/outputs` at deploy time so files survive container restarts. ### A token or credentials directory ```dockerfile ENV REPORT_DIR=/mnt/report ``` Set only the env var. Do not create the directory in the image: a file share is mounted there at deploy time. Document the share setup in the README. ### System packages ```dockerfile RUN apt-get update \ && apt-get install -y --no-install-recommends \ bash curl ca-certificates \ jq pandoc \ && rm -rf /var/lib/apt/lists/* ``` Extend the existing `apt-get install` step instead of adding a second one, to keep layers small. Do not add Node.js back. ### Python deps for skill scripts Add what the skill's scripts import to `{{SKILL_EXTRA_DEPS}}` in `pyproject.toml` (for example `"feedparser>=6.0.10"`) and mirror it in `requirements.txt`. No Dockerfile change is needed. ## The non-root user The template always runs as user `claude` (uid 1000). Add every extra directory the skill writes to `{{NONROOT_EXTRA_DIRS}}`, for example `/sessions /outputs` or `/sessions /mnt/<name> /outputs`. If you skip the `chown`, the agent starts but fails with `Permission denied` when the skill writes. Staying non-root also protects the permission model. The bundled CLI refuses `--dangerously-skip-permissions`, which `permission_mode="bypassPermissions"` translates to, when it runs as root. An agent that switched to `bypassPermissions` and ran as root would start and pass `/health`, then fail every run with `--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons`. ## What stays out of the image - **Secrets.** Never `ENV ANTHROPIC_API_KEY=...` and never `COPY .env .`. Credentials come at runtime from Container Apps secrets or `--env-file`. - **`tests/`.** `.dockerignore` excludes it. Keep that line. - **Local edits to the skill.** Copy the skill as it lives in the repo. Patch the filesystem or the env, not the skill. `.dockerignore` ```text __pycache__/ *.pyc *.pyo *.pyd .pytest_cache/ .venv/ venv/ .env *.log .git/ .gitignore README.md tests/ ``` ## Ports | Surface | Port | | --- | --- | | Plain HTTP | The lowest unused port at or above `9024`, recorded in a port list kept with your agents. `9000`–`9023` are reserved for the orchestrator, websocket server, and frontend dev server; `9100` and up for non-A2A internal services. | | AMC variant | `8080`, with no port list entry needed. | Never invent a port for a plain-HTTP agent: check the list first and add the new agent to it in the same change. ## Related - [Configuration and environment](https://www.agentmissioncontrol.ai/developers/self-hosted-configuration/) - [Deploy and register in AMC](https://www.agentmissioncontrol.ai/developers/self-hosted-deploy-and-register/) - [Common quirks and troubleshooting](https://www.agentmissioncontrol.ai/developers/self-hosted-troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-docker/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Configuration and environment Agent Mission Control developer docs > Claude skill, self-hosted > The two credential paths and every environment variable the generated agent reads. **Before you start:** A generated agent directory with `.env.example`. Copy `.env.example` to `.env` for local runs. `__main__.py` and every test script load it. In a deployed container, set the same variables as Container Apps secrets and env vars, or pass `--env-file` to `docker run`. ## Claude credentials: pick one path ### Path A: Anthropic API direct `.env` ```bash ANTHROPIC_API_KEY=<your-anthropic-api-key> ``` ### Path B: Claude on Microsoft Foundry `.env` ```bash CLAUDE_CODE_USE_FOUNDRY=1 ANTHROPIC_FOUNDRY_RESOURCE=<your-foundry-resource-name> ANTHROPIC_FOUNDRY_API_KEY=<your-foundry-resource-key> ANTHROPIC_API_KEY=<the-same-foundry-resource-key> ``` - `ANTHROPIC_FOUNDRY_RESOURCE` is the resource **name** only: no URL, no protocol. The SDK builds `https://<name>.services.ai.azure.com/anthropic/` itself. - Set `ANTHROPIC_API_KEY` to the same value as `ANTHROPIC_FOUNDRY_API_KEY`. The bundled Claude Code CLI reads that name even in Foundry mode, and the health probe requires it. - The auth header on the wire is Anthropic-style `x-api-key`, not Azure's `api-key`. ## Every variable | Variable | Default | What it does | | --- | --- | --- | | `ANTHROPIC_API_KEY` | none | Required on both paths. | | `CLAUDE_CODE_USE_FOUNDRY` | unset | `1` switches to Microsoft Foundry. | | `ANTHROPIC_FOUNDRY_RESOURCE` | none | Foundry resource name (Path B). | | `ANTHROPIC_FOUNDRY_API_KEY` | none | Foundry resource key (Path B). | | `AGENT_HOST` | `0.0.0.0` | Bind address (plain-HTTP surface). | | `AGENT_PORT` | `{{PORT}}` | Listen port (plain-HTTP surface). | | `AGENT_MAX_CONCURRENCY` | `3` | Concurrent `/execute` streams per process. | | `CLAUDE_AGENT_CWD` | `/app` | Working directory; the SDK looks for `.claude/skills/` here. | | `CLAUDE_AGENT_MODEL` | `claude-sonnet-4-6` | Model id. | | `CLAUDE_AGENT_MAX_TURNS` | `50` | Tool-call turns per run. Raise it if you see `approaching cap` warnings; lower it for stricter cost control. | | `CLAUDE_AGENT_MAX_BUDGET_USD` | unset (no ceiling) | Hard per-run cost ceiling, passed as `max_budget_usd`. | | `CLAUDE_AGENT_EFFORT` | unset (SDK default) | `low`, `medium`, `high`, `xhigh`, or `max`. Other values are ignored. | | `CLAUDE_AGENT_MAX_BUFFER_BYTES` | unset (SDK's 1 MiB) | Raise for file-heavy skills, for example `67108864`. See below. | | `CLAUDE_CODE_MAX_RETRIES` | `5` | Bounded CLI retries. | | `<SKILL>_OUTPUTS_DIR` | `/outputs` | Only if the skill writes files; named by you, for example `REPORT_OUTPUTS_DIR`. | | `EMIT_STEP_ACTIVITY_LOG` | your choice | AMC variant only: turns the thinking and tool detail rows on or off. | > **Important:** The SDK frames its stdout at 1 MiB per message. Parallel tool results arrive as one message, so a file-heavy skill can exceed the limit and die with `JSON message exceeded maximum buffer size` after the orchestration was already paid for. Set `CLAUDE_AGENT_MAX_BUFFER_BYTES` for those skills. ## Env vars for the skill's own scripts The CLI runs the skill's scripts with the `env=` dict from `ClaudeAgentOptions`. Put anything the scripts read there through `{{SKILL_ENV_VARS}}`, for example `"REPORT_OUTPUTS_DIR": OUTPUTS_DIR` for a skill that writes files or `"REPORT_DIR": "/mnt/report"` for one that reads tokens from a mounted share. ## Where each value is set | Set in | Values | | --- | --- | | `Dockerfile` `ENV` | `HOME`, `AGENT_PORT`, `AGENT_HOST`, `CLAUDE_AGENT_CWD`, `CLAUDE_AGENT_MODEL`, and the outputs or token directory variables. Never secrets. | | Runtime (secrets) | `ANTHROPIC_API_KEY`, `ANTHROPIC_FOUNDRY_API_KEY`. | | Runtime (plain env) | `CLAUDE_CODE_USE_FOUNDRY`, `ANTHROPIC_FOUNDRY_RESOURCE`, and any tuning knob you want to change without a rebuild. | ## Related - [Build the Docker image](https://www.agentmissioncontrol.ai/developers/self-hosted-docker/) - [Test the agent locally](https://www.agentmissioncontrol.ai/developers/self-hosted-testing/) - [The agent core and prompt strategies](https://www.agentmissioncontrol.ai/developers/self-hosted-agent-core/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-configuration/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Test the agent locally Agent Mission Control developer docs > Claude skill, self-hosted > Run the phased smoke test and the credential diagnostics, then smoke-test the container over HTTP. **Before you start:** A filled-in `.env` and Python 3.12 with the agent's dependencies installed. ## Check the generated code parses ```bash cd azurefoundry_report_generator_skills python -c "import ast; [ast.parse(open(f).read()) for f in ['__main__.py','claude_skills_agent.py','claude_skills_agent_executor.py','http_agent_server.py']]" ``` A parse error here is real: every placeholder should be filled by now. ## The diagnostic scripts `tests/` holds five standalone scripts. Each loads `../.env`, so you do not re-export credentials. Run them from the `tests/` directory. If everything is healthy you only need `test_run.py`; use the others to isolate a failure when it stops partway. | # | Script | What it isolates | | --- | --- | --- | | 1 | `test_foundry_direct.py` | Resource URL, key, model deployment, and auth header style, over plain HTTP with no SDK. Tries `api-key`, `x-api-key`, and `Authorization: Bearer` until one returns 200. | | 2 | `test_anthropic_sdk_foundry.py` | The `anthropic` package's Foundry client, bypassing `claude-agent-sdk`. If this works and the agent does not, the problem is in the agent SDK layer. | | 3 | `inspect_foundry_client.py` | What `AnthropicFoundry.__init__` expects for `resource`, and the base URL each candidate format produces. | | 4 | `debug_sdk.py [model]` | `claude-agent-sdk` end to end with every message dumped. Use it when `test_run.py` Phase 1 fails with a vague error. | | 5 | `test_run.py` | Phases 0 to 3: env, SDK auth, skill discovery, adapter wiring. | > **Note:** `test_foundry_direct.py` appends `/anthropic/v1/messages` to `ANTHROPIC_FOUNDRY_RESOURCE` as-is, so it needs the full `https://...` resource URL in that variable for the run. The agent itself expects the resource name only. Set the variable back afterwards. ## The phased smoke test ```bash cd azurefoundry_report_generator_skills/tests python test_run.py ``` | Phase | Checks | Exit code on failure | | --- | --- | --- | | 0: env sanity | `ANTHROPIC_API_KEY` is set; in Foundry mode, `ANTHROPIC_FOUNDRY_RESOURCE` too. | 1 | | 1: SDK auth | A one-turn `query()` with no tools returns text (`PING`). | 2 | | 2: skill discovery | Copies the skill into a temp `.claude/skills/<skill-name>/`, asks the SDK to list its skills, and checks the name appears. | 3 | | 3: adapter wiring | Runs your agent class in an empty temp cwd, so the skill is not invoked, and checks the first event starts with 🤖, and the last is not an error. It also reports whether token usage was captured, without failing on it. | 4 | Exit code `0` means every phase passed. Phase 3 avoids the skill on purpose, so it works on Windows even when the skill hardcodes Linux paths. What the smoke test does not cover: running the skill end to end when its scripts need Linux paths, the full HTTP `/execute` NDJSON path, and volume-mounted outputs. Use the container for those. ## Smoke-test the container (plain HTTP) ```bash # From the repo root docker build -f azurefoundry_report_generator_skills/Dockerfile -t report-generator-skills-agent-test . docker run --rm -it \ -p 9024:9024 \ --env-file azurefoundry_report_generator_skills/.env \ -v "${PWD}/outputs:/outputs" \ report-generator-skills-agent-test curl http://localhost:9024/health curl -X POST http://localhost:9024/execute \ -H 'Content-Type: application/json' \ -d '{"context_id":"t1","task_id":"t1","message":"Build a test report"}' ``` ## Smoke-test the container (AMC variant) ```bash docker run --rm -it -p 8080:8080 --env-file .env report-generator-skills-agent-test curl -i http://localhost:8080/health # 200 when ready, 503 when not curl http://localhost:8080/workflows curl -X POST http://localhost:8080/runs \ -H 'Content-Type: application/json' \ -d '{"inputs": {"topic": "weekly"}}' # returns orchestratorRunId curl http://localhost:8080/runs/<orchestratorRunId>/status curl -N http://localhost:8080/runs/<orchestratorRunId>/events curl http://localhost:8080/runs/<orchestratorRunId>/tokens ``` Check that the completed step on `/status` carries `inputTokens`, `outputTokens`, and a model, and that `/tokens` is not zero. Those are what AMC prices. ## Related - [Common quirks and troubleshooting](https://www.agentmissioncontrol.ai/developers/self-hosted-troubleshooting/) - [Configuration and environment](https://www.agentmissioncontrol.ai/developers/self-hosted-configuration/) - [Deploy and register in AMC](https://www.agentmissioncontrol.ai/developers/self-hosted-deploy-and-register/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-testing/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Deploy and register in AMC Agent Mission Control developer docs > Claude skill, self-hosted > Push the image, run it as a container with secrets and volumes, and register its endpoint in AMC. **Before you start:** An AMC-variant image that passes a local smoke test, a container registry, and a role that can register agents. The generated README targets Azure Container Apps. Any host that runs a container and exposes HTTPS works, as long as AMC can reach the endpoint. ## Build and push ```bash # From the repo root docker build \ -f azurefoundry_report_generator_skills/Dockerfile \ -t <registry>.azurecr.io/a2a-report-generator-skills-agent:$(date +%Y%m%d-%H%M%S) \ . docker push <registry>.azurecr.io/a2a-report-generator-skills-agent:<tag> ``` The generated README tags each build with a timestamp. ## Set credentials as secrets ```bash az containerapp secret set \ --name report-generator-skills-agent-uami --resource-group <resource-group> \ --secrets anthropic-api-key=<your-anthropic-api-key> az containerapp update \ --name report-generator-skills-agent-uami --resource-group <resource-group> \ --set-env-vars ANTHROPIC_API_KEY=secretref:anthropic-api-key ``` For Foundry credentials, also set `CLAUDE_CODE_USE_FOUNDRY=1`, `ANTHROPIC_FOUNDRY_RESOURCE`, and `ANTHROPIC_FOUNDRY_API_KEY`. See [Configuration and environment](https://www.agentmissioncontrol.ai/developers/self-hosted-configuration/). ## Mount volumes the skill needs Without a mount, anything the skill writes is lost when the container restarts. Register an Azure Files share with the Container Apps environment, then attach it at the path the Dockerfile expects (`/outputs`, or `/mnt/<name>` for a token directory). ```bash az containerapp env storage set \ --name <environment-name> --resource-group <resource-group> \ --storage-name report-outputs \ --azure-file-account-name <storage-account> \ --azure-file-account-key <storage-account-key> \ --azure-file-share-name report-outputs \ --access-mode ReadWrite ``` Then add a `volumeMounts` entry with `mountPath: /outputs` to the container app. Azure Files suits outputs under 100 MB; for larger files or many files, use blob storage. ## Register in AMC 1. On **Agents**, select **Register**. 2. Enter a **Name**, choose the **Kind** (**Workflow** or **Agent**), choose the **Flight Director / custom** adapter, and pick a **Department**. 3. Check **This resource uses the AMC SDK (no path segments required)**. 4. Enter the **Endpoint URL**: the container's base URL, for example `https://<your-agent>.example.com`. 5. Under **Authentication**, choose **API key** or **Managed Identity (same tenant)**. 6. Select **Register**. AMC probes the endpoint and reports the result. Full registration detail, including input contracts and declared steps, is in [Register your agent](https://www.agentmissioncontrol.ai/developers/register-your-agent/). ## What the registration probe checks AMC makes one `GET <endpoint>/health` call with a 5-second timeout, presenting the credential you chose. | Response | AMC reports | | --- | --- | | Any 2xx | **Healthy** | | 401 or 403 | **Access not granted** | | Any other status, a timeout, or a connection failure | **Unreachable** | > **Important:** `amc_sdk` returns 503 from `/health` when your `health` probe reports `ready=False`, for example when credentials are missing. The probe treats that as **Unreachable**. Fix configuration and check `curl -i <endpoint>/health` returns 200 before you register. The endpoint must be `http` or `https`, and the probe does not follow redirects, so register the final URL. Managed identity with a self-hosted endpoint needs all of: a Tenant Admin registering it, an `https` endpoint, an `api://` resource scope, and the host pre-approved in the installation's `SelfHostedManagedIdentityTargets` list. That list is empty by default, so a default install refuses it. Use an API key unless your installation has approved the host. ## Related - [Register your agent: the technical reference](https://www.agentmissioncontrol.ai/developers/register-your-agent/) - [Serve the agent to AMC with amc_sdk](https://www.agentmissioncontrol.ai/developers/self-hosted-amc-sdk/) - [Common quirks and troubleshooting](https://www.agentmissioncontrol.ai/developers/self-hosted-troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-deploy-and-register/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Common quirks and troubleshooting Agent Mission Control developer docs > Claude skill, self-hosted > Skill patterns to catch before you generate, and the errors a self-hosted skill agent most often hits. **Before you start:** A generated agent, or a skill you are about to convert. ## Check the skill before you convert it | Symptom in the skill | What to do | | --- | --- | | Scripts hardcode `/sessions/<session-id>/...` paths | Create the exact path in the Dockerfile and flag the brittleness. One `mkdir` usually covers every script. | | `SKILL.md` names a CLI tool, or scripts call `subprocess`, `os.system`, or `os.popen` | Install the binary in the Dockerfile, at the exact path the skill looks for it. | | A bootstrap or auth flow references `tokens.json` or `~/.config/<name>/` | Do not bake credentials or wire OAuth into the agent. Do the first OAuth login on a laptop with the same skill in Claude Code, upload the tokens to a file share, mount it, and point the skill's env var at the mount. | | Imports such as `feedparser`, `youtube-transcript-api`, `arxiv`, or `praw` | Add them to the dependencies and confirm the container's egress can reach the public internet. Check the skill tolerates one source failing. | | Multi-MB outputs | File share for files under 100 MB; blob storage (blobfuse or an upload sidecar) for larger or many files. Note size estimates in the README. | | The skill calls other skills | Bake every skill it needs into `/app/.claude/skills/`, say so in the prompt, and use the multi-purpose pattern. | | The skill keeps state (`state.json`, `history.db`) | Mount a file share on the state directory. Per-conversation state needs a context id from the caller; treat that as out of scope for a first version. | | The skill needs a newer Python | The image uses `python:3.12-slim`. Bump it if needed; never go below 3.10, which the SDK requires. | | Scripts call `input()` or `getpass()`, or open an editor | Not viable unattended. Refactor the skill to take everything through arguments or env first. | | The skill imports a package that is not on PyPI | Stop. That is a blocker. | ## Errors and what they mean | You see | Cause and fix | | --- | --- | | `/health` returns `not_configured` | Read the missing-vars list in the response body's `detail`. | | AMC registration says **Unreachable** but the container is running | `/health` returned 503 because the health probe is not ready, or the URL redirects. See [Deploy and register in AMC](https://www.agentmissioncontrol.ai/developers/self-hosted-deploy-and-register/). | | The agent runs but never invokes the skill | Check `/app/.claude/skills/<skill-name>/SKILL.md` exists in the container, `SKILL_NAME` matches the frontmatter `name:`, `allowed_tools` includes `"Skill"`, and `setting_sources=["project"]` is set. | | `--dangerously-skip-permissions cannot be used with root/sudo privileges` | Something switched to `bypassPermissions` while running as root. Restore `permission_mode="dontAsk"` and the non-root user. | | `Permission denied` when the skill writes | The directory is missing from `{{NONROOT_EXTRA_DIRS}}`, so user `claude` does not own it. | | `JSON message exceeded maximum buffer size` | A message passed the SDK's 1 MiB framing limit. Set `CLAUDE_AGENT_MAX_BUFFER_BYTES`. | | A `Run used N turns (max_turns=50)` warning that says `approaching cap` | Raise `CLAUDE_AGENT_MAX_TURNS`. | | `Error: assistant message error: authentication_failed` (or `billing_error`, `rate_limit`) | Credentials, billing, or quota. Run `tests/test_foundry_direct.py` to isolate it. | | `Error: agent completed but produced no text response` | Look for an earlier `AssistantMessage` error or a `ResultMessage flagged is_error=True` line in the logs. | | 401 from Foundry | Run `tests/test_foundry_direct.py`, then `test_anthropic_sdk_foundry.py`, to find which layer fails. | | Phase 1 fails with `Claude Code returned an error result` | Run `tests/debug_sdk.py` for the full message dump. Check the SDK is at least 0.2.138. | | Tokens always report 0 | Code reads flat `input_tokens` attributes that do not exist. Read `ResultMessage.model_usage`. | | AMC prices the run at the default rate | `step_completed` has no `model`. Pass it, with `cached_input_tokens`. | | A failed run shows zero cost in AMC | `set_step_tokens` and `set_cost` were not called before re-raising. | | Prompt tokens are much higher than the job needs | `tools=` is unset, so the full Claude Code toolset is defined in every prompt. Set it to the base set. | > **Note:** A slow failure that looks like a hang is often the CLI retrying. `CLAUDE_CODE_MAX_RETRIES` bounds it at 5 by default. ## Related - [Test the agent locally](https://www.agentmissioncontrol.ai/developers/self-hosted-testing/) - [Build the Docker image](https://www.agentmissioncontrol.ai/developers/self-hosted-docker/) - [Configuration and environment](https://www.agentmissioncontrol.ai/developers/self-hosted-configuration/) --- Source: https://www.agentmissioncontrol.ai/developers/self-hosted-troubleshooting/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Build on Microsoft Agent Framework Agent Mission Control developer docs > Microsoft Agent Framework (Python) > When to build a single agent or a multi-agent workflow on Microsoft Agent Framework, which versions to pin, and how either one reaches AMC. **Before you start:** Python 3.11 or later, a Microsoft Foundry project with a deployed chat model, and the Azure CLI. In this build path the framework does the reasoning. Microsoft Agent Framework for Python runs the model loop, calls your tools and MCP servers, and streams the result. You deploy the agent or workflow to Microsoft Foundry as a **hosted agent**, and AMC runs it over the Foundry Responses protocol. If you want a Claude skill to do the reasoning and only use Agent Framework as the host, this is the wrong path. See [Choose a build path](https://www.agentmissioncontrol.ai/developers/choose-a-build-path/). ## Agent or workflow | You need | Build | | --- | --- | | One model turn with tools does the whole job | A single agent: one `Agent`, served directly. | | Several agents or functions that hand work to each other | A workflow: executors connected by edges with `WorkflowBuilder`. | | Each stage shown as its own step in AMC | A workflow. Every executor reports as a step. A single agent reports as one step. | | A deterministic check between model calls | A workflow with a plain Python executor in the middle. | | Parallel branches joined at the end | A workflow with fan-out and fan-in edges. | | A pause for a person mid-run | A workflow that calls `ctx.request_info(...)`. | | A run that survives the serving process stopping | A workflow. Process-loss recovery (`resilient_background`) is workflow-only. | > **Note:** A single agent cannot get process-loss recovery. If a single-agent job needs it, build it as a one-node workflow instead. ## How it reaches AMC 1. Build an `Agent` or a `Workflow` with `FoundryChatClient`-backed agents. 2. Write a `main.py` that wraps it in `ResponsesHostServer`. It serves `/responses` and a health probe on port `8088`. A workflow is wrapped with `workflow.as_agent(name=...)` first. 3. Deploy the folder to Foundry as a hosted agent version and route traffic to it. 4. Register it in AMC. AMC calls it over the Responses protocol by its agent name. 5. While it runs, AMC reads steps, tokens, model, failures and human-input requests from the stream. The hosted Responses stream flattens a workflow into one agent stream, and its run-level `usage` block is always empty. So the agent or workflow reports what AMC needs itself, on informational `function_call` markers. [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) has the code. The general rules every agent meets are in [the integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/) and [how agents connect](https://www.agentmissioncontrol.ai/developers/how-agents-connect/). ## Versions this section targets Agent Framework's Python API has changed across releases. The skills this section is based on verify their APIs against `agent-framework` **1.11.0**. Process-loss recovery needs the newer core **1.19.0** line. Pin exact versions in a container image, and don't install with `--pre`, which pulls pre-releases over the stable line. | Package | Single agent | Workflow with recovery | Provides | | --- | --- | --- | --- | | `agent-framework-core` | `1.11.0` | `1.19.0` | `Agent`, `Executor`, `WorkflowBuilder`, `Content`, `Message`. 1.19 adds the checkpoint allow-list. | | `agent-framework-foundry` | `1.10.1` | `1.13.1` | `FoundryChatClient`, `FoundryAgent`, `to_prompt_agent` | | `agent-framework-foundry-hosting` | `1.0.0a260709` | `1.0.0b260918` | `ResponsesHostServer`, `CheckpointStoreProvider` | | `azure-ai-agentserver-responses` | Pulled in by hosting | `>=2.2.0b1,<3` | `ResponsesServerOptions` | | `azure-ai-projects` | `>=2.3.0` | `>=2.3.0` | Deploy and invoke hosted agents | | `azure-identity` | `>=1.19.0` | `>=1.19.0` | `DefaultAzureCredential` | > **Important:** Don't depend on the `agent-framework` meta-package in anything Foundry builds. It resolves to `agent-framework-core[all]`, which pulls in `agent-framework-hyperlight` and `hyperlight-sandbox-backend-wasm`. That package has no installable distribution for Foundry's Linux remote build, so the build fails with pip `ResolutionImpossible`. Depend on `agent-framework-core`, `agent-framework-foundry` and `agent-framework-foundry-hosting` directly. ### Older examples you will find Many published examples use an earlier SDK surface. Map them to the 1.11.0 surface like this: | Older examples | 1.11.0 | | --- | --- | | `from agent_framework.azure import AzureAIAgentsProvider` | Not present. Use `from agent_framework.foundry import FoundryChatClient` for a local agent, or `FoundryAgent` to connect to an existing hosted agent. | | `provider.create_agent(...)` | `Agent(client=FoundryChatClient(...), ...)`. To publish a persistent prompt agent, `AIProjectClient.agents.create_version(name, definition=to_prompt_agent(agent))`. | | `provider.get_agent(agent_id)` | `FoundryAgent(project_endpoint=..., agent_name=..., credential=...)`. It connects to an existing agent and never creates one. Omit `agent_version` for the latest. | | Get-or-create by name | `AIProjectClient.agents.get(name)`, and on `ResourceNotFoundError` call `agents.create_version(name, definition=...)`. | | `agent.run_stream(...)` | `agent.run(..., stream=True)`, an async iterator of `AgentResponseUpdate`. | | Reading usage with `getattr` | `AgentResponse.usage_details` is a dict. Read `input_token_count`, `output_token_count` and `total_token_count` with `.get`. | ## In this section - [Build a single agent](https://www.agentmissioncontrol.ai/developers/af-build-an-agent/): client, tools, MCP servers, structured output, streaming. - [Build a workflow](https://www.agentmissioncontrol.ai/developers/af-build-a-workflow/): executors, edges, fan-out and fan-in, routing, loops. - [Orchestrations, human input and checkpoints](https://www.agentmissioncontrol.ai/developers/af-orchestration-and-checkpoints/): the high-level builders, `as_agent()`, pauses and resume. - [Host and deploy on Foundry](https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/): `main.py`, the container, and the deploy script. - [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/): the marker helpers and process-loss recovery. - [Report steps, tokens and human input](https://www.agentmissioncontrol.ai/developers/af-report-to-amc/): what AMC reads and how it shows it. - [Register and test in AMC](https://www.agentmissioncontrol.ai/developers/af-register-and-test/) and [Troubleshooting](https://www.agentmissioncontrol.ai/developers/af-troubleshooting/). ## Related - [Build a single agent](https://www.agentmissioncontrol.ai/developers/af-build-an-agent/) - [Build a workflow](https://www.agentmissioncontrol.ai/developers/af-build-a-workflow/) - [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) --- Source: https://www.agentmissioncontrol.ai/developers/af-overview/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Build a single agent Agent Mission Control developer docs > Microsoft Agent Framework (Python) > Create an Agent Framework agent backed by FoundryChatClient, with function tools, MCP servers, structured output and streaming. **Before you start:** A Foundry project endpoint, a chat model deployment name, and an az login session. A single agent is one `Agent` object. Foundry hosts it directly, and the whole run is one model turn with whatever tools the agent calls. These examples target `agent-framework-core` 1.11.0. ## Install `requirements.txt` ```text agent-framework-core==1.11.0 agent-framework-foundry==1.10.1 agent-framework-foundry-hosting==1.0.0a260709 azure-ai-projects>=2.3.0 azure-identity>=1.19.0 ``` ## Configure | Variable | Set by | Value | | --- | --- | --- | | `FOUNDRY_PROJECT_ENDPOINT` | You locally; the Foundry hosting runtime when deployed | `https://<resource>.services.ai.azure.com/api/projects/<project>` | | `FOUNDRY_MODEL` | You, for local runs | `<model-deployment-name>` | | `FOUNDRY_MODEL_NAME` or `AZURE_AI_MODEL_DEPLOYMENT_NAME` | The Foundry hosting runtime | The model deployment the hosted agent uses | | `AZURE_TOKEN_CREDENTIALS` | You, in production | `prod`, to limit `DefaultAzureCredential` to production-safe credentials | Authenticate with `DefaultAzureCredential`. It uses your `az login` session locally and the managed identity in Azure, with no code change. Avoid connection strings and account keys: they bypass Entra audit and rotation. Wrap clients in `with` or `async with` blocks so transports and token caches are released. Read the model name from any of the three variables, so the same code runs locally and hosted: `agent.py` ```python import os def _model() -> str: for var in ("FOUNDRY_MODEL", "FOUNDRY_MODEL_NAME", "AZURE_AI_MODEL_DEPLOYMENT_NAME"): value = os.getenv(var) if value: return value raise KeyError( "No model deployment configured: set FOUNDRY_MODEL, " "FOUNDRY_MODEL_NAME or AZURE_AI_MODEL_DEPLOYMENT_NAME." ) ``` ## Create the agent `agent.py` ```python import os from typing import Annotated from agent_framework import Agent from agent_framework.foundry import FoundryChatClient from azure.identity import DefaultAzureCredential from pydantic import BaseModel, Field AGENT_NAME = "contoso-orders-agent" def get_order_status( order_id: Annotated[str, Field(description="The order number to look up")], ) -> str: """Return the current status of an order.""" return f"Order {order_id}: shipped" class Answer(BaseModel): status: str message: str def create_agent() -> Agent: client = FoundryChatClient( project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], model=_model(), credential=DefaultAzureCredential(), ) return Agent( client=client, name=AGENT_NAME, instructions="You answer order-status questions. Always call the tool. Never guess.", tools=[get_order_status], default_options={"response_format": Answer, "store": False}, ) ``` - **Function tools:** pass plain functions (sync or `async def`) in `tools=`. The framework converts them and runs them in-process during the turn. Describe parameters with `Annotated[type, Field(description=...)]` and the tool with its docstring. Don't pass tool names as strings. - **Structured output:** the Responses host does not forward a caller's `response_format`. Bake it into `default_options` so every run returns your model as JSON. - **`store: False`:** the hosting layer manages conversation history. This is the usual setting for an agent AMC triggers. - **Name:** keep `agent` in a single agent's name. AMC uses the name to classify what it imports; see [Register and test in AMC](https://www.agentmissioncontrol.ai/developers/af-register-and-test/). ## Run it locally `run_local.py` ```python import asyncio from agent import Answer, create_agent async def main() -> None: agent = create_agent() response = await agent.run("What is the status of order 1042?") answer = Answer.model_validate_json(response.text) print(answer.message) usage = response.usage_details or {} print(usage.get("input_token_count"), usage.get("output_token_count")) asyncio.run(main()) ``` > **Important:** `usage_details` is a `UsageDetails` TypedDict, which is a plain dict at runtime. Read it with `.get`. `getattr` on a dict always misses and silently returns nothing, which shows up in AMC as zero tokens. ### Streaming Pass `stream=True` and iterate the updates with `async for`. Each update is an `AgentResponseUpdate`. ```python async for update in agent.run("Summarize today's late orders", stream=True): if update.text: print(update.text, end="", flush=True) ``` A single update rarely carries the full token total. To total usage across a stream, fold each content's `usage_details` together with `add_usage_details`, as the token marker in [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) does. ## Give a message-less trigger something to act on AMC can start an agent with no meaningful input. Agent middleware runs on every hosted turn, so it can add a kickoff message before the model call. This one stamps the current time: `agent.py` ```python from collections.abc import Awaitable, Callable from datetime import datetime, timezone from agent_framework import AgentContext, Message, agent_middleware @agent_middleware async def inject_datetime(context: AgentContext, call_next: Callable[[], Awaitable[None]]) -> None: now = datetime.now(timezone.utc).isoformat() # Message takes contents (Content or str), not a text keyword. context.messages.append(Message(role="user", contents=[f"The current date and time is {now}."])) await call_next() # Agent(..., middleware=[inject_datetime]) ``` ## Hosted tools and MCP servers The skill documents these tool types, all imported from `agent_framework` and passed in `tools=` like a function tool: | Tool | Use it for | Rules | | --- | --- | --- | | `HostedCodeInterpreterTool` | Running Python on the service: maths, data analysis, file processing | Give it uploaded files with `inputs=[HostedFileContent(file_id=...)]`. | | `HostedFileSearchTool` | Searching documents in vector stores | Needs `inputs=[HostedVectorStoreContent(vector_store_id=...)]`. An empty `inputs` list is an error. Optional `max_results`. | | `HostedWebSearchTool` | Current web information through Bing grounding | Needs a Bing connection, set as `BING_CONNECTION_ID`. | | `HostedMCPTool` | An MCP server the service connects to | Configure `approval_mode`, `allowed_tools`, and `headers`. No context manager. | | `MCPStreamableHTTPTool` | An MCP server your code connects to | Must be opened with `async with`. Set auth headers through a custom `http_client`. | > **Important:** The skill wrote its hosted-tool and MCP examples against the earlier SDK surface and says to adjust class names for 1.11.0. Check that each class imports from your pinned version before you depend on it. ```python from agent_framework import HostedMCPTool, MCPStreamableHTTPTool # Service-managed: Foundry connects to the server. docs_mcp = HostedMCPTool( name="Contoso docs", url="https://<your-mcp-server>/mcp", approval_mode="never_require", allowed_tools=["search_docs", "read_doc"], ) # Client-managed: your process holds the connection, so open it first. async with MCPStreamableHTTPTool( name="Contoso internal API", url="https://<your-internal-mcp-server>/mcp", ) as internal_mcp: agent = Agent(client=client, name=AGENT_NAME, instructions="Use the docs and the internal API.", tools=[docs_mcp, internal_mcp]) response = await agent.run("What is our returns window?") ``` | `approval_mode` | Effect | | --- | --- | | `"never_require"` | Tool calls run without approval. | | `"always_require"` | Every call needs approval. | | `{"always_require_approval": [...], "never_require_approval": [...]}` | Per-tool lists. | Use `HostedMCPTool` when the server is public and Foundry can reach it. Use `MCPStreamableHTTPTool` when the server is private or needs authentication, or when you want explicit control of the connection. > **Note:** An unattended run can't complete an OAuth consent flow. If an MCP server only supports OAuth, call the service's REST API from a function tool instead, with a static credential read from an environment variable. ## Conversation state On the hosted path the hosting layer keeps conversation history, which is why the agent sets `store: False`. A client that calls the hosted agent directly opens a session and passes its id on each call; see [Host and deploy on Foundry](https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/). The skill's multi-turn thread examples (`get_new_thread()`, `thread=`) come from the earlier SDK surface, so confirm the thread API in your pinned version before you build on it. Next: make the agent report tokens to AMC. See [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/). ## Related - [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) - [Host and deploy on Foundry](https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/) - [Build a workflow](https://www.agentmissioncontrol.ai/developers/af-build-a-workflow/) --- Source: https://www.agentmissioncontrol.ai/developers/af-build-an-agent/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Build a workflow Agent Mission Control developer docs > Microsoft Agent Framework (Python) > Wire executors and agents into a typed graph with WorkflowBuilder: edges, streaming, output selection, fan-out and fan-in, routing and loops. **Before you start:** A Foundry project endpoint, a chat model deployment name, and an az login session. A workflow is a directed graph. Its nodes are **executors** and its connections are **edges**. Messages flow along the edges, and terminal nodes yield the workflow's output. Agents inside a workflow are local, ephemeral agents built on `FoundryChatClient`. ## Install `requirements.txt` ```text agent-framework-core==1.19.0 agent-framework-foundry==1.13.1 agent-framework-foundry-hosting==1.0.0b260918 azure-ai-agentserver-responses>=2.2.0b1,<3 azure-ai-projects>=2.3.0 azure-identity>=1.19.0 ``` Set `FOUNDRY_PROJECT_ENDPOINT` and `FOUNDRY_MODEL` for local runs. The orchestration builders live in `agent-framework-orchestrations`. For graph diagrams, the skill installs `agent-framework[viz]` and GraphViz, but keep that out of anything Foundry builds (see the meta-package warning in [the overview](https://www.agentmissioncontrol.ai/developers/af-overview/)). ## Executors and the workflow context An executor is an `Executor` subclass with `@handler` methods, or an `@executor`-decorated async function. Every handler takes the typed input first and a `WorkflowContext` second. | Context type | The node can | | --- | --- | | `WorkflowContext[T_Out]` | Send `T_Out` downstream with `await ctx.send_message(...)`. | | `WorkflowContext[T_Out, T_W_Out]` | Send downstream and also yield workflow output with `await ctx.yield_output(...)`. | | `WorkflowContext` | Neither. Same as `WorkflowContext[Never, Never]`. | `workflow.py` ```python from agent_framework import Executor, WorkflowBuilder, WorkflowContext, executor, handler from typing_extensions import Never class UpperCase(Executor): def __init__(self, id: str): super().__init__(id=id) @handler async def to_upper(self, text: str, ctx: WorkflowContext[str]) -> None: await ctx.send_message(text.upper()) @executor(id="reverse_text") async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None: await ctx.yield_output(text[::-1]) def create_workflow(): upper = UpperCase(id="upper") return WorkflowBuilder(start_executor=upper).add_edge(upper, reverse_text).build() ``` To bypass type introspection, for example for a union input, declare types on the decorator: `@handler(input=str, output=str)`. It is all or nothing: if you set any, set every relevant one, and `input` is required. ## Run it ```python events = await create_workflow().run("hello world") print(events.get_outputs()) # ['DLROW OLLEH'] print(events.get_final_state()) # WorkflowRunState.IDLE ``` > **Note:** Build a fresh workflow for every run with a helper like `create_workflow()`. Executor instances hold state, and reusing them leaks it between runs. Agents can be built once and shared. ## Agents as nodes ```python import os from typing import cast from agent_framework import Agent, AgentResponse, WorkflowBuilder from agent_framework.foundry import FoundryChatClient from azure.identity import AzureCliCredential client = FoundryChatClient( project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], model=os.environ["FOUNDRY_MODEL"], credential=AzureCliCredential(), ) writer = Agent(client=client, name="writer", instructions="You write and edit content.") reviewer = Agent(client=client, name="reviewer", instructions="You give concise, actionable feedback.") workflow = WorkflowBuilder(start_executor=writer).add_edge(writer, reviewer).build() events = await workflow.run("Create a slogan for an affordable electric bike.") for out in cast(list[AgentResponse], events.get_outputs()): print(f"{out.messages[0].author_name}: {out.text}") ``` Don't mix `FoundryChatClient` agents with persistent server-side agents in one workflow. If you need persistent agents, use the single-agent flow. ## Typed edges with your own executors For AMC you will usually wrap each agent in your own executor, so the executor can report a step, catch failures and pass a typed payload on. Use dataclasses for edge payloads. `workflow.py` ```python from dataclasses import dataclass from agent_framework import Executor, Message, Workflow, WorkflowBuilder, WorkflowContext, handler from typing_extensions import Never @dataclass class Draft: prompt: str draft: str def _prompt_from_messages(messages: list[Message]) -> str: for message in reversed(messages): if message.text: return message.text return "" class WriterExecutor(Executor): def __init__(self, agent, id: str = "writer") -> None: super().__init__(id=id) self._agent = agent async def _draft(self, prompt: str, ctx: WorkflowContext[Draft, Message]) -> None: response = await self._agent.run(prompt) await ctx.send_message(Draft(prompt=prompt, draft=response.text)) @handler async def handle_prompt(self, prompt: str, ctx: WorkflowContext[Draft, Message]) -> None: await self._draft(prompt, ctx) @handler async def handle_messages(self, messages: list[Message], ctx: WorkflowContext[Draft, Message]) -> None: await self._draft(_prompt_from_messages(messages), ctx) class PackageExecutor(Executor): def __init__(self, id: str = "package") -> None: super().__init__(id=id) @handler async def handle(self, message: Draft, ctx: WorkflowContext[Never, object]) -> None: await ctx.yield_output({"prompt": message.prompt, "draft": message.draft}) def build_workflow(writer_agent) -> Workflow: writer = WriterExecutor(writer_agent) package = PackageExecutor() return ( WorkflowBuilder( start_executor=writer, output_from=[package], intermediate_output_from=[writer], ) .add_edge(writer, package) .build() ) ``` - The start executor has a `list[Message]` handler. Foundry hosting wraps the workflow with `as_agent()`, which feeds the start executor the Responses conversation as `list[Message]` and rejects a start executor that can't take one. The `str` handler keeps local runs simple. - The context is typed `WorkflowContext[Draft, Message]` so the executor can also yield `Message` outputs. That is how it emits the AMC step marker. - Terminal nodes must call `ctx.yield_output(...)`, or the workflow produces no output. ## Choose which outputs surface - `output_from`: executors whose `yield_output` becomes workflow output (`type='output'` events, `get_outputs()`). Special value `"all"`. - `intermediate_output_from`: executors whose output becomes intermediate output (`type='intermediate'`). Special value `"all_other"`. - `output_from` is an allow-list, not a routing rule. Output from an executor in neither list is hidden. - These fail at build time: the same executor in both lists, unknown or duplicate executors, `output_from="all_other"`, and `intermediate_output_from="all"`. Omitting both gives a deprecation warning, so set `output_from` explicitly. > **Important:** For AMC, list every non-terminal executor in `intermediate_output_from`. An executor's step markers are only forwarded when it is designated. ## Stream events ```python from agent_framework import AgentResponseUpdate, Message last_author = None async for event in workflow.run(Message("user", ["Create a slogan."]), stream=True): if event.type == "output" and isinstance(event.data, AgentResponseUpdate): if event.data.author_name != last_author: last_author = event.data.author_name print(f"\n{last_author}: ", end="", flush=True) print(event.data.text, end="", flush=True) ``` To observe node inputs and outputs without changing executors, watch `type='executor_invoked'` and `type='executor_completed'` events. ## Fan-out and fan-in Send one message to several branches, then join their results. `AgentExecutor` wraps an agent as a node, `AgentExecutorRequest` feeds it, and the result is an `AgentExecutorResponse`. `workflow.py` ```python from agent_framework import ( Agent, AgentExecutor, AgentExecutorRequest, AgentExecutorResponse, Executor, Message, WorkflowBuilder, WorkflowContext, handler, ) from typing_extensions import Never class DispatchToExperts(Executor): @handler async def dispatch(self, prompt: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None: msg = Message("user", [prompt]) await ctx.send_message(AgentExecutorRequest(messages=[msg], should_respond=True)) class AggregateInsights(Executor): @handler async def aggregate(self, results: list[AgentExecutorResponse], ctx: WorkflowContext[Never, str]) -> None: await ctx.yield_output("\n\n".join(r.agent_response.text for r in results)) researcher = AgentExecutor(agent=Agent(client=client, name="researcher", instructions="..."), id="researcher") marketer = AgentExecutor(agent=Agent(client=client, name="marketer", instructions="..."), id="marketer") legal = AgentExecutor(agent=Agent(client=client, name="legal", instructions="..."), id="legal") dispatcher = DispatchToExperts(id="dispatcher") aggregator = AggregateInsights(id="aggregator") workflow = ( WorkflowBuilder(start_executor=dispatcher) .add_fan_out_edges(dispatcher, [researcher, marketer, legal]) .add_fan_in_edges([researcher, marketer, legal], aggregator) .build() ) ``` The fan-in target's handler receives a **list** of upstream results, so type its input as `list[...]` matching what the branches emit. `AgentExecutor` nodes emit no AMC step markers. To see each branch as its own step in AMC, wrap each agent in your own executor that emits a marker with its own step id; see [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/). ## Conditional routing, switch-case and loops These use edge groups instead of plain `add_edge`. The skill names the shapes and points to the official samples (under `python/samples/03-workflows` in the [Agent Framework repository](https://github.com/microsoft/agent-framework/tree/main/python/samples/03-workflows)) for the exact method names, which have changed between releases. Read the sample for your pinned version. | Shape | What it does | Sample | | --- | --- | --- | | Conditional edge | Route on a classifier or agent decision. | `control-flow/edge_condition.py` | | Switch-case edge group | Pick one branch by category. | `control-flow/switch_case_edge_group.py` | | Multi-selection edge group | Pick one or many targets at run time. | `control-flow/multi_selection_edge_group.py` | | Loop | A judge node routes back until a condition is met. | `control-flow/simple_loop.py` | | Cancellation | Cancel a running workflow through asyncio tasks. | `control-flow/workflow_cancellation.py` | ## Other composition options - **Sub-workflows:** nest a whole workflow as an executor in a parent (`composition/sub_workflow_basics.py`). - **Functional API:** write the workflow as plain async Python with `if`, loops and `asyncio.gather`; `@step` adds per-step checkpointing (`functional/` samples). - **Shared state:** store a value once in workflow state and reuse it across agents (`state-management/state_with_agents.py`). - **High-level builders:** for sequential, concurrent, group chat, handoff and Magentic shapes, see [Orchestrations, human input and checkpoints](https://www.agentmissioncontrol.ai/developers/af-orchestration-and-checkpoints/). ## Related - [Orchestrations, human input and checkpoints](https://www.agentmissioncontrol.ai/developers/af-orchestration-and-checkpoints/) - [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) - [Host and deploy on Foundry](https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/) --- Source: https://www.agentmissioncontrol.ai/developers/af-build-a-workflow/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Orchestrations, human input and checkpoints Agent Mission Control developer docs > Microsoft Agent Framework (Python) > Use the high-level orchestration builders, expose a workflow as an agent, pause for a person with request_info, and checkpoint so a run can resume. **Before you start:** A working workflow; see Build a workflow. ## Orchestration builders For common multi-agent shapes, use a builder from `agent_framework.orchestrations` instead of wiring edges. Each takes `participants=[...]` (agents and executors) and returns a `Workflow` from `.build()`, which you run like any workflow. ```python from agent_framework.orchestrations import ( ConcurrentBuilder, GroupChatBuilder, HandoffBuilder, MagenticBuilder, SequentialBuilder, ) workflow = SequentialBuilder(participants=[writer, reviewer], output_from="all").build() result = await workflow.run("Draft a launch slogan.") ``` | Builder | Shape | Default output | | --- | --- | --- | | `SequentialBuilder` | Agents in a chain with a shared conversation. | The last participant. `output_from="all"` surfaces every one. | | `ConcurrentBuilder` | Fan out to all participants, fan in with an aggregator you can override. | The aggregator. Participants are hidden unless selected. | | `GroupChatBuilder` | A manager picks the next speaker: `.with_orchestrator(agent=manager_agent)` or a function selector. | See the samples. | | `HandoffBuilder` | A triage agent routes to specialists. `.with_autonomous_mode()` lets specialists iterate until they hand off. | The participants. | | `MagenticBuilder` | A Magentic manager plans and drives the agents. Takes `participants={...}`. | See the samples. | > **Important:** Treat `MagenticBuilder.participants` keys as stable ids. When you resume from a checkpoint, rebuild with the same keys or the run fails fast. ## Expose a workflow as an agent ```python agent = workflow.as_agent(name="content_pipeline") response = await agent.run("...") # an AgentResponse ``` Wrapped, workflow output becomes the normal `AgentResponse.text` and intermediate output becomes `text_reasoning` content. Foundry hosting serves a workflow this way, and a wrapped workflow can be a participant in another workflow. ## Pause for a person An executor asks with `ctx.request_info(...)`. The workflow goes idle with a pending request (`IDLE_WITH_PENDING_REQUESTS`). When the answer arrives, the executor's `@response_handler` receives it and the graph continues. This start node asks a free-text question: `workflow.py` ```python from dataclasses import dataclass from agent_framework import Executor, Message, WorkflowContext, handler, response_handler @dataclass class TopicRequest: question: str # AMC shows this field to the person @dataclass class Topic: topic: str class AskExecutor(Executor): def __init__(self, id: str = "ask") -> None: super().__init__(id=id) async def _ask(self, ctx: WorkflowContext[Topic, Message]) -> None: await ctx.request_info( request_data=TopicRequest(question="What would you like me to write about?"), response_type=str, ) @handler async def handle_prompt(self, prompt: str, ctx: WorkflowContext[Topic, Message]) -> None: await self._ask(ctx) @handler async def handle_messages(self, messages: list[Message], ctx: WorkflowContext[Topic, Message]) -> None: await self._ask(ctx) @response_handler async def on_topic(self, original_request: TopicRequest, answer: str, ctx: WorkflowContext[Topic, Message]) -> None: await ctx.send_message(Topic(topic=(answer or "").strip())) ``` > **Important:** Don't put `from __future__ import annotations` in a module that uses `@response_handler`. Stringized annotations break its signature check, which inspects the raw `WorkflowContext[...]` annotation. Every orchestration builder also supports `.with_request_info()` to pause for review before continuing. For approval before sensitive tool calls, see the skill's `tool-approval/` samples. How AMC shows the pause and sends the answer back is in [Report steps, tokens and human input](https://www.agentmissioncontrol.ai/developers/af-report-to-amc/). ## Checkpoint and resume A checkpoint persists workflow state so a run can pause, including for human input, and resume later, even in another process. Locally, pass a checkpoint store to the builder with `checkpoint_storage=...`, or to `workflow.as_agent(checkpoint_storage=...)`. For durable storage the skill points to `CosmosCheckpointStorage` and its samples (`checkpoint/cosmos_workflow_checkpointing.py`); take the exact import and constructor from the sample for your version. Under Foundry hosting, `ResponsesHostServer` owns checkpointing: it restores a paused workflow's checkpoint when the answer arrives. Two rules follow: - **Default storage is instance-local.** Without process-loss recovery, the pause and the resume must land on the same warm instance, so run the agent on a single replica. - **Custom types must be allow-listed on core 1.19 and later.** Every custom type a checkpoint carries (the `request_data` payload and each edge dataclass) must be named as `"<module>:<qualname>"` in a `CheckpointStoreProvider`, or the restore fails. See [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/). ## Related - [Report steps, tokens and human input](https://www.agentmissioncontrol.ai/developers/af-report-to-amc/) - [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) - [Build a workflow](https://www.agentmissioncontrol.ai/developers/af-build-a-workflow/) --- Source: https://www.agentmissioncontrol.ai/developers/af-orchestration-and-checkpoints/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Host and deploy on Foundry Agent Mission Control developer docs > Microsoft Agent Framework (Python) > Serve an agent or workflow with ResponsesHostServer, package it, deploy it as a Foundry hosted agent version, and call it from a client. **Before you start:** A Foundry project in a region that supports hosted agents, and a data-plane role on it. ## Before you start - **Region:** Foundry hosted agents are region-limited, and an account's region is fixed when it is created. Check Microsoft's [hosted agents region list](https://learn.microsoft.com/azure/foundry/agents/concepts/hosted-agents#region-availability). An unsupported region fails with `bad_request: Unsupported region for Foundry Hosted Agents`. - **Model:** the project needs a deployed chat model. - **Role:** creating and invoking agent versions needs a **data-plane** role on the project, such as **Foundry User**. Control-plane Owner or Contributor alone is not enough. - **Tooling:** locally you only need `azure-ai-projects>=2.3.0` and `azure-identity` to drive the deploy. Foundry installs `requirements.txt` itself in a remote build. ## Write main.py `ResponsesHostServer` serves the Responses protocol (`/responses`) and a health probe on port `8088`, which is the runtime contract Foundry expects. A single agent is served directly: `main.py (single agent)` ```python from agent_framework_foundry_hosting import ResponsesHostServer from agent import create_agent def main() -> None: ResponsesHostServer(create_agent()).run() if __name__ == "__main__": main() ``` A workflow is wrapped with `as_agent()` first. Passing a raw `Workflow` makes the host call `Workflow.run(messages=...)`, which fails. This version also turns on process-loss recovery and allow-lists the checkpoint types; [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) explains both. `main.py (workflow)` ```python from agent_framework_foundry_hosting import CheckpointStoreProvider, ResponsesHostServer from azure.ai.agentserver.responses import ResponsesServerOptions from workflow import build_workflow, create_agents def main() -> None: workflow = build_workflow(*create_agents()) server = ResponsesHostServer( workflow.as_agent(name="contoso-content-pipeline"), options=ResponsesServerOptions(resilient_background=True), checkpoint_store_provider=CheckpointStoreProvider( allowed_checkpoint_types=["workflow:Draft"], ), ) server.run() if __name__ == "__main__": main() ``` ## Container `Dockerfile` ```dockerfile FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN useradd -m appuser USER appuser EXPOSE 8088 CMD ["python", "main.py"] ``` Keep `.env`, virtual environments and the deploy script out of the image and the deploy package. ## Deploy with the Python SDK This script zips the folder, creates a new **code** version with a remote build, waits for it to go active, routes all traffic to it, and smoke-tests it. Each run creates a new immutable version. `deploy.py` ```python import sys, tempfile, time, zipfile from pathlib import Path from azure.ai.projects import AIProjectClient from azure.ai.projects.models import ( AgentEndpointConfig, CodeConfiguration, CodeDependencyResolution, FixedRatioVersionSelectionRule, HostedAgentDefinition, ProtocolConfiguration, ProtocolVersionRecord, ResponsesProtocolConfiguration, VersionSelector, ) from azure.identity import AzureCliCredential ENDPOINT = "<your-project-endpoint>" MODEL_NAME = "<model-deployment-name>" AGENT_NAME = "contoso-content-pipeline" SOURCE_DIR = Path(__file__).parent.resolve() EXCLUDE_PARTS = {".git", ".venv", "__pycache__", ".azure", ".env"} EXCLUDE_NAMES = {".env", ".dockerignore", "deploy.py"} def make_zip() -> Path: zp = Path(tempfile.gettempdir()) / f"{AGENT_NAME}.zip" zp.unlink(missing_ok=True) with zipfile.ZipFile(zp, "w", zipfile.ZIP_DEFLATED) as zf: for p in SOURCE_DIR.rglob("*"): rel = p.relative_to(SOURCE_DIR) if not p.is_file() or rel.name in EXCLUDE_NAMES: continue if any(part in EXCLUDE_PARTS for part in rel.parts): continue zf.write(p, rel.as_posix()) return zp def wait_active(c, ver) -> None: for _ in range(90): time.sleep(10) status = c.agents.get_version(agent_name=AGENT_NAME, agent_version=ver)["status"] if status == "active": return if status in ("failed", "Failed"): raise RuntimeError("provisioning failed") raise TimeoutError("version never went active") def main() -> None: zp = make_zip() with zp.open("rb") as code, AzureCliCredential() as cred, \ AIProjectClient(endpoint=ENDPOINT, credential=cred) as c: created = c.agents.create_version_from_code( agent_name=AGENT_NAME, description="Contoso content pipeline", definition=HostedAgentDefinition( cpu="1", memory="2Gi", code_configuration=CodeConfiguration( runtime="python_3_13", entry_point=["python", "main.py"], dependency_resolution=CodeDependencyResolution.REMOTE_BUILD), environment_variables={ "FOUNDRY_PROJECT_ENDPOINT": ENDPOINT, "FOUNDRY_MODEL_NAME": MODEL_NAME}, protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")], ), code=code, ) wait_active(c, created.version) c.agents.update_details( agent_name=AGENT_NAME, agent_endpoint=AgentEndpointConfig( version_selector=VersionSelector(version_selection_rules=[ FixedRatioVersionSelectionRule(agent_version=created.version, traffic_percentage=100)]), protocol_configuration=ProtocolConfiguration(responses=ResponsesProtocolConfiguration()), ), ) with c.get_openai_client(agent_name=AGENT_NAME) as oai: r = oai.responses.create(input="Slogan for an affordable electric bike") print("smoke test:", r.status, r.error) if __name__ == "__main__": sys.exit(main()) ``` | Setting | Value | Why | | --- | --- | --- | | `runtime` | `python_3_13` | Agent Framework installs cleanly. Avoid `python_3_14`, where some wheels are missing. | | `entry_point` | `["python", "main.py"]` | `main.py` starts `ResponsesHostServer`. | | `dependency_resolution` | `REMOTE_BUILD` | Foundry installs `requirements.txt` in the cloud. | | `protocol_versions` | `responses` `2.0.0` | Required by the responses 2.x runtime that process-loss recovery uses. | | `cpu` / `memory` | `1` / `2Gi` | Allowed sizes are `0.5`/`1Gi`, `1`/`2Gi` and `2`/`4Gi`. | A healthy smoke test has `status == "completed"` and `error` of `None`. To roll back, point `FixedRatioVersionSelectionRule` at an earlier version. Never delete the version that is currently routed. Microsoft also documents an `azd ai agent` flow; the reference deployments behind this guide use the SDK path above because it is easier to run non-interactively. ## Call the deployed agent from a client This is a preview API. It is also how you read the raw stream AMC will see. `verify.py` ```python from azure.ai.projects import AIProjectClient from azure.ai.projects.models import VersionRefIndicator from azure.identity import DefaultAzureCredential ENDPOINT = "<your-project-endpoint>" AGENT_NAME = "contoso-content-pipeline" with (DefaultAzureCredential() as cred, AIProjectClient(endpoint=ENDPOINT, credential=cred, allow_preview=True) as project): ver = project.agents.get(agent_name=AGENT_NAME).versions["latest"].version session = project.agents.create_session( agent_name=AGENT_NAME, version_indicator=VersionRefIndicator(agent_version=ver)) oai = project.get_openai_client(agent_name=AGENT_NAME) stream = oai.responses.create( input="Draft a launch slogan.", stream=True, extra_body={"agent_session_id": session.agent_session_id}, metadata={"x-ms-debug-mode-enabled": "1"}, ) for event in stream: print(event.type) project.agents.delete_session(agent_name=AGENT_NAME, session_id=session.agent_session_id) ``` `x-ms-debug-mode-enabled` gives richer events. AMC sends the same metadata when it runs the agent. ## Related - [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) - [Register and test in AMC](https://www.agentmissioncontrol.ai/developers/af-register-and-test/) - [Troubleshoot Agent Framework agents](https://www.agentmissioncontrol.ai/developers/af-troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Harden an agent or workflow for AMC Agent Mission Control developer docs > Microsoft Agent Framework (Python) > Copy the marker helpers that carry steps, tokens, model and failures to AMC, wrap a single agent for both host paths, and turn on process-loss recovery for workflows. **Before you start:** An agent or workflow that runs locally; see Build a single agent or Build a workflow. Foundry hosting takes away what AMC needs to show a run properly. These helpers put it back. They are not in Microsoft's docs or samples; they come from agents and workflows running against AMC. Copy them into every new hosted agent or workflow. ## What hosting takes away | You lose | Because | AMC needs it for | | --- | --- | --- | | Per-executor `author_name` | Consecutive text merges into a single message item. | Labelling each step. | | Token counts | Run-level `usage` is always empty for a hosted agent or workflow, streaming or not, and the host drops the SDK's mid-stream `usage` content. | Cost, ROI and budgets. | | Which model ran | It is not on the wire. | Pricing each step by its own model. | | Why a step failed | An exception aborts the stream with no structured reason. | Showing the failure reason. | The one channel that survives is an informational `function_call` output item. Each executor, or the single agent, emits one with everything AMC needs in its `arguments`. `informational_only=True` marks it transcript-only, so Foundry never tries to run it as a tool. > **Note:** AMC tells a marker from a real tool call by `call_id == name`. A genuine tool call has a generated `call_id` that differs from its name and a paired `function_call_output`. ## The marker arguments | Key | Type | Meaning | | --- | --- | --- | | `delivered` | string | The text this step produced. It is the step's headline in AMC. | | `input_tokens`, `output_tokens` | integer | Counted by you from `usage_details`. `input_token_count` already includes cached tokens, and `output_token_count` includes reasoning tokens. | | `total_tokens` | integer | `input + output`. | | `model` | string | The model deployment id. AMC prices the step by this model. | | `cached_input_tokens` | integer | The cached subset of input, billed at the cheaper cached rate. `0` when absent. | | `model_usage` | object | Per-model breakdown: `{<model>: {"input": i, "output": o, "cached": c}}`. AMC prices each row at that model's rate and sums them. | | `failure_reason` | string | `"<ExceptionType>: <message>"`. Its presence marks the step **failed**. | | `activity_log` | array | Optional detail rows under the step; see [Report steps, tokens and human input](https://www.agentmissioncontrol.ai/developers/af-report-to-amc/). | | `run_name` | string | Optional. Renames the run while completing the step. | Azure OpenAI returns token counts, not cost, so don't send `cost_usd`. AMC's price is tokens times the model's configured rate, which means the counts must be right. ## Shared helpers `workflow.py` ```python import os from agent_framework import Content, Message, WorkflowContext def _model() -> str: # Local dev sets FOUNDRY_MODEL; Foundry hosting injects the other two. for var in ("FOUNDRY_MODEL", "FOUNDRY_MODEL_NAME", "AZURE_AI_MODEL_DEPLOYMENT_NAME"): value = os.getenv(var) if value: return value raise KeyError("Set FOUNDRY_MODEL, FOUNDRY_MODEL_NAME or AZURE_AI_MODEL_DEPLOYMENT_NAME.") def _token_count(response) -> tuple[int, int, int]: """(input, output, cached_input) from an AgentResponse, or zeros.""" usage = getattr(response, "usage_details", None) # attribute on the response... if not usage: return 0, 0, 0 return ( # ...but dict access on usage_details int(usage.get("input_token_count") or 0), int(usage.get("output_token_count") or 0), int(usage.get("cached_input_token_count") or usage.get("cached_token_count") or usage.get("cached_tokens") or 0), ) async def _emit_step_marker(ctx: WorkflowContext, step_id: str, delivered: str | None = None, input_tokens: int | None = None, output_tokens: int | None = None, model: str | None = None, cached_input_tokens: int | None = None, model_usage: dict | None = None, failure_reason: str | None = None, activity_log: list[dict] | None = None) -> None: arguments: dict = {} if delivered is not None: arguments["delivered"] = delivered if input_tokens is not None: arguments["input_tokens"] = input_tokens if output_tokens is not None: arguments["output_tokens"] = output_tokens if input_tokens is not None or output_tokens is not None: arguments["total_tokens"] = (input_tokens or 0) + (output_tokens or 0) if model: arguments["model"] = model if cached_input_tokens is not None: arguments["cached_input_tokens"] = cached_input_tokens if model_usage is None and model and (input_tokens is not None or output_tokens is not None): model_usage = {model: {"input": input_tokens or 0, "output": output_tokens or 0, "cached": cached_input_tokens or 0}} if model_usage: arguments["model_usage"] = model_usage if failure_reason: arguments["failure_reason"] = failure_reason if activity_log: arguments["activity_log"] = activity_log await ctx.yield_output(Message( role="assistant", contents=[Content.from_function_call( call_id=step_id, name=step_id, # call_id == name: the marker discriminator arguments=arguments, informational_only=True, )], author_name=step_id, )) ``` ## Workflow executors Emit the marker after the agent runs. On failure, emit a `failure_reason` marker and then re-raise, so the run still fails but AMC records why. `workflow.py` ```python class WriterExecutor(Executor): def __init__(self, agent, id: str = "writer") -> None: super().__init__(id=id) self._agent = agent async def _draft(self, prompt: str, ctx: WorkflowContext[Draft, Message]) -> None: try: response = await self._agent.run(prompt) except Exception as exc: await _emit_step_marker(ctx, "writer", failure_reason=f"{type(exc).__name__}: {exc}", model=_model()) raise in_tok, out_tok, cached_tok = _token_count(response) await _emit_step_marker(ctx, "writer", delivered=response.text, input_tokens=in_tok, output_tokens=out_tok, model=_model(), cached_input_tokens=cached_tok) await ctx.send_message(Draft(prompt=prompt, draft=response.text)) ``` - A step with no model call, such as a pure Python check or the human-input step, still emits a marker with `input_tokens=0, output_tokens=0` so every step is reported. - Designate the executors in the builder: `WorkflowBuilder(start_executor=..., output_from=[terminal], intermediate_output_from=[every other executor])`. Undesignated executors' markers are not forwarded. - If you also run the same graph in a local harness that reports to AMC some other way, pass that reporter into the executor and emit markers only when it is absent. Markers are the Foundry path. ### One executor, several models Normally one executor wraps one agent on one deployment, and the helper builds the single-row `model_usage` for you. Only when one executor calls agents on different deployments do you accumulate it yourself: ```python def _merge_usage(acc: dict, model: str, response) -> None: in_tok, out_tok, cached = _token_count(response) row = acc.setdefault(model, {"input": 0, "output": 0, "cached": 0}) row["input"] += in_tok row["output"] += out_tok row["cached"] += cached usage: dict = {} r1 = await self._planner.run(prompt); _merge_usage(usage, PLANNER_MODEL, r1) r2 = await self._writer.run(r1.text); _merge_usage(usage, WRITER_MODEL, r2) await _emit_step_marker(ctx, "compose", delivered=r2.text, input_tokens=sum(u["input"] for u in usage.values()), output_tokens=sum(u["output"] for u in usage.values()), model=WRITER_MODEL, # a display label; the cost comes from model_usage model_usage=usage) ``` - Key each row by the **pricing** model id, the id AMC has a rate for. Otherwise that slice is priced at AMC's default rate. - Add per-turn deltas. Don't also add a cumulative total on top. - The simpler alternative is one executor per model, so each step is single-model. ## A single agent A hosted single agent reports no tokens either. Subclass `Agent` and append one trailing marker whose `call_id` and `name` are both the agent's name. `ResponsesHostServer` may drive the agent streaming or not, so wrap both paths. The subclass calls `super().run()`, so middleware, tools and `response_format` still apply. `agent.py` ```python from agent_framework import ( Agent, AgentResponseUpdate, Content, Message, UsageDetails, add_usage_details, ) AGENT_NAME = "contoso-orders-agent" MARKER_STEP = AGENT_NAME # call_id == name == AGENT_NAME def _split_usage(usage: UsageDetails | None) -> tuple[int, int, int]: if not usage: return 0, 0, 0 return (int(usage.get("input_token_count") or 0), int(usage.get("output_token_count") or 0), int(usage.get("cached_input_token_count") or usage.get("cached_token_count") or usage.get("cached_tokens") or 0)) def _marker_content(delivered: str, input_tokens: int, output_tokens: int, model: str | None = None, cached_input_tokens: int = 0, activity_log: list[dict] | None = None) -> Content: arguments: dict[str, object] = { "delivered": delivered, "input_tokens": input_tokens, "output_tokens": output_tokens, "total_tokens": input_tokens + output_tokens, "cached_input_tokens": cached_input_tokens, } if model: arguments["model"] = model arguments["model_usage"] = {model: {"input": input_tokens, "output": output_tokens, "cached": cached_input_tokens}} if activity_log: arguments["activity_log"] = activity_log return Content.from_function_call(call_id=MARKER_STEP, name=MARKER_STEP, arguments=arguments, informational_only=True) class _TokenMarkerAgent(Agent): def run(self, messages=None, *, stream: bool = False, **kwargs): # type: ignore[override] if stream: return self._run_streaming_with_marker(messages, **kwargs) return self._run_nonstreaming_with_marker(messages, **kwargs) async def _run_nonstreaming_with_marker(self, messages, **kwargs): response = await super().run(messages, stream=False, **kwargs) in_tok, out_tok, cached_tok = _split_usage(getattr(response, "usage_details", None)) response.messages.append(Message( role="assistant", contents=[_marker_content(response.text or "", in_tok, out_tok, _model(), cached_tok)], author_name=MARKER_STEP, )) return response async def _run_streaming_with_marker(self, messages, **kwargs): usage: UsageDetails | None = None text_parts: list[str] = [] async for update in super().run(messages, stream=True, **kwargs): for content in update.contents or []: content_usage = getattr(content, "usage_details", None) if content_usage: usage = add_usage_details(usage, content_usage) if getattr(content, "type", None) == "text" and content.text: text_parts.append(content.text) yield update # re-emit every real update first in_tok, out_tok, cached_tok = _split_usage(usage) yield AgentResponseUpdate( contents=[_marker_content("".join(text_parts), in_tok, out_tok, _model(), cached_tok)], role="assistant", author_name=MARKER_STEP, ) ``` Build the agent from `_TokenMarkerAgent(client=..., name=AGENT_NAME, ...)` instead of `Agent(...)`. A single agent has no per-step failure branch: a hard failure raises and the marker never emits. ## Process-loss recovery for workflows By default a hosted workflow run dies with the process serving its stream. A deploy, a replica recycle, a scale-in or a dropped client connection leaves the run stuck at `in_progress`, and a reconnect replays an empty stream. `resilient_background=True` fixes this: the runtime persists the input, leases the work, re-invokes the handler in a later process, and switches replay to a file-backed store so a reconnect replays the real events. - **It is workflow-only.** `ResponsesHostServer` raises `RuntimeError` if you pass it for a single agent. - **It is a runtime upgrade.** The flag does not exist before `azure-ai-agentserver-responses` 2.x and raises `TypeError`. Bump `agent-framework-core==1.19.0`, `agent-framework-foundry==1.13.1`, `agent-framework-foundry-hosting==1.0.0b260918` and `azure-ai-agentserver-responses>=2.2.0b1,<3` together. - **It needs protocol 2.0.0.** Responses 2.x requires hosted-agent protocol `2.0.0`; the agent fails fast at startup if the environment doesn't support it. Deploy one workflow first to confirm before rolling out the rest. - **Don't hand-roll replay.** Never call `streams.use_file_backed_replay()` yourself. The flag configures it, and a manual call overwrites that process-wide setting and breaks replay. ### Allow-list every checkpoint type Recovery restores the workflow's checkpoint, and core 1.19 blocks deserializing any custom type that isn't named. List the `request_data` payload and **every** dataclass on an edge as `"<module>:<qualname>"`. A workflow with no human-input pause still needs its edge types listed. Framework types such as `Message` are already allowed. `main.py` ```python checkpoint_provider = CheckpointStoreProvider( allowed_checkpoint_types=[ "workflow:TopicRequest", # ctx.request_info(request_data=...) payload "workflow:Topic", # each edge message type "workflow:Written", ] ) ``` The module is the one that holds the type at runtime. When `main.py` does `from workflow import ...`, it is `workflow`, exactly as the error message prints it. A missing type fails with `Checkpoint deserialization blocked for type 'workflow:TopicRequest'.` ## Checklist - `_model()`, `_token_count()` and `_emit_step_marker()` copied in (or `_split_usage()`, `_marker_content()` and `_TokenMarkerAgent` for a single agent). - Every executor catches exceptions, emits `failure_reason`, and re-raises. - Every success path emits `delivered`, `input_tokens`, `output_tokens`, `model` and `cached_input_tokens`. - Only an executor that drives more than one model builds `model_usage` itself. - `output_from=[terminal]` and `intermediate_output_from=[every other executor]`. - Markers use `call_id == name` and `informational_only=True`. - Every model the agent can run has a token rate configured in AMC, or it is priced at AMC's default rate. - For recovery: `ResponsesServerOptions(resilient_background=True)`, the responses 2.x pins, a `CheckpointStoreProvider` listing every custom type, and one workflow deployed first. ## Related - [Report steps, tokens and human input](https://www.agentmissioncontrol.ai/developers/af-report-to-amc/) - [Host and deploy on Foundry](https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/) - [Troubleshoot Agent Framework agents](https://www.agentmissioncontrol.ai/developers/af-troubleshooting/) --- Source: https://www.agentmissioncontrol.ai/developers/af-amc-hardening/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Report steps, tokens and human input Agent Mission Control developer docs > Microsoft Agent Framework (Python) > How AMC turns your markers into steps, cost and detail rows, how to rename a run or mark it completed with issues, and how a request_info pause becomes a human intervention. **Before you start:** The marker helpers from Harden an agent or workflow for AMC. This page is the agent-side view of what AMC reads from a Foundry-hosted run. The general run, step and cost model is in [the integration contract](https://www.agentmissioncontrol.ai/developers/integration-contract/). ## Steps - Each informational `function_call` whose `call_id` equals its `name` becomes a step, keyed and labelled by that id. Use the executor id, such as `writer`. - The marker's `added` frame marks the step running; its `done` frame completes it with the parsed `arguments`. - A marker with `failure_reason` marks the step **failed**, even if the host frame did not. - Steps are keyed by a stable id, so parallel branches are separate steps and a replayed frame updates the same step rather than adding one. - A single agent has one step, named after the agent. - Reserved names are never steps: `request_info` (human input), `run.renamed` (rename), `run.outcome` ([completed with issues](#completed-with-issues)) and `run.artifact`. A step with any of these names silently disappears from the run. ## Tokens and cost AMC sums each step's tokens into the run total and prices each step by its `model`, with `cached_input_tokens` at the cached rate. When `model_usage` is present, AMC prices each model's row and sums them. A model AMC has no rate for, or a marker with no model, is priced at AMC's default rate. Model ids match without regard to case. > **Important:** Tokens are the only cost signal from a hosted run. If `usage_details` is read with `getattr`, every step reports zero tokens and the run shows no cost. ## Detail rows with activity_log `delivered` is the step's headline. `activity_log` adds what the agent did inside the step, which AMC shows as collapsible detail. It is optional: without it, AMC shows `delivered` as a single row. AMC replaces, not appends, the rows on each marker, so always send the full list. ```text { "time": "HH:MM:SS", "type": "thinking" | "tool" | "result", "tool": "<tool name or null>", "text": "<text>", "result": "<text or null>" } ``` - Map `text_reasoning` content to a `thinking` row. - Collapse a `function_call` and its later `function_result` onto one `tool` row keyed by `call_id`, with the name, arguments and result. - Add a trailing `result` row only when `delivered` is a short summary. If `delivered` is the full output, the row just duplicates it. - Cap argument and thinking text at 2000 characters and result text at 4000. - In a single agent, skip the agent's own marker (`call_id == AGENT_NAME`) when you build rows. - Never let capture break a run. Wrap it so any error returns `None` and the marker falls back to `delivered` only. - Gate it with an environment flag (`EMIT_STEP_ACTIVITY_LOG`), so turning it off is a configuration change, not a redeploy. A workflow executor already holds a complete `AgentResponse`, so read rows straight from `response.messages`. A streaming single agent receives a tool call's arguments in fragments, so aggregate first with `AgentResponse.from_updates(updates).messages`. ```python def _safe_activity(messages, final_text=None): if os.getenv("EMIT_STEP_ACTIVITY_LOG", "true").strip().lower() in {"false", "0", "no"}: return None try: return _activity_rows_from_messages(messages, final_text) or None except Exception: return None # Workflow executor, success path: await _emit_step_marker(ctx, "writer", delivered=response.text, ..., activity_log=_safe_activity(response.messages)) ``` ## Rename the run Once a run's identity is known, for example the topic it resolved, it can rename **this run only**. The registered default name never changes. Emit a reserved marker: ```python RUN_RENAMED_MARKER = "run.renamed" # reserved; spell it exactly async def _emit_rename_marker(ctx, name: str) -> None: clean = (name or "").strip() if not clean: return # blank is a no-op and never clears the name await ctx.yield_output(Message(role="assistant", author_name=RUN_RENAMED_MARKER, contents=[ Content.from_function_call(call_id=RUN_RENAMED_MARKER, name=RUN_RENAMED_MARKER, arguments={"name": clean}, informational_only=True)])) ``` - Or, when the rename coincides with a step, add `run_name` to that step marker's `arguments` instead of sending a second marker. - A single agent emits the same `run.renamed` content alongside its token marker. - Last non-empty name wins. A blank name is ignored. A rename after the run has ended is ignored. AMC keeps the name across reloads. ## Completed with issues A run that finishes normally after a failure that matters, but isn't fatal, says so with the reserved `run.outcome` marker. AMC then records the run's outcome as `partial` and shows it as **Completed with issues**, with the reason. The status stays completed. ```python RUN_OUTCOME_MARKER = "run.outcome" # reserved; spell it exactly async def _emit_outcome_marker(ctx, reason: str) -> None: clean = " ".join((reason or "").split())[:2000] if not clean: # a partial outcome must say what wasn't done return await ctx.yield_output(Message(role="assistant", author_name=RUN_OUTCOME_MARKER, contents=[ Content.from_function_call(call_id=RUN_OUTCOME_MARKER, name=RUN_OUTCOME_MARKER, arguments={"outcome": "partial", "reason": clean}, informational_only=True)])) ``` - The shape is exact: a `function_call` named `run.outcome`, `call_id` equal to `name`, `informational_only=True`, and `arguments` of `{"outcome": "partial", "reason": "..."}`. - `outcome` must be `"partial"`. AMC ignores any other value. `reason` says what wasn't done, up to 2000 characters. - In a workflow, emit it from the executor that decides the run is incomplete: it catches the exception, emits that step's `failure_reason` marker, doesn't re-raise, and calls `_emit_outcome_marker`. A single agent emits the same content alongside its token marker. - Emit it any time before the response completes. The last one wins. - Failed wins. If the run then ends `response.failed`, AMC ignores the marker. - Only the marker counts. AMC never infers **Completed with issues** from a step that carries `failure_reason`; without the marker, that run is a clean success. ## Human input When a workflow calls `ctx.request_info(...)`, the hosted agent surfaces the pending request as a **real** `function_call` named `request_info`. Its `call_id` is the request id. Then: 1. AMC adds a **Human input** step in the waiting state, shows the question, and puts the run into human intervention. 2. AMC reads the question from the `question` field of your `request_data` (it falls back to `prompt`). Give the dataclass a `question` field. 3. When someone answers, AMC sends a follow-up Responses call with the same `previous_response_id` and a `function_call_output` carrying the matching `call_id` and the answer text. 4. The host restores the paused workflow's checkpoint and your `@response_handler` receives the answer as the `response_type` you asked for (here `str`). 5. AMC records the step as completed with the answer and the run continues. A paused turn also ends with `response.completed`. AMC keeps the run waiting until the answer resumes it, so you don't need to do anything special. > **Important:** Without process-loss recovery, the resume checkpoint is local to the instance. Run a human-input workflow on a single replica, or turn on `resilient_background` with the checkpoint allow-list. ## Related - [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) - [Orchestrations, human input and checkpoints](https://www.agentmissioncontrol.ai/developers/af-orchestration-and-checkpoints/) - [Register and test in AMC](https://www.agentmissioncontrol.ai/developers/af-register-and-test/) --- Source: https://www.agentmissioncontrol.ai/developers/af-report-to-amc/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Register and test in AMC Agent Mission Control developer docs > Microsoft Agent Framework (Python) > Import a Foundry project or register one hosted agent, name it so AMC classifies it correctly, and confirm steps and tokens arrive. **Before you start:** A deployed hosted agent or workflow, and the Department Manager or Tenant Admin role in AMC. An Agent Manager can register only Kind = Agent: no workflows, and no project import. The general registration steps are in [Register your agent](https://www.agentmissioncontrol.ai/developers/register-your-agent/). This page covers what is specific to a Foundry-hosted Agent Framework agent. ## Give AMC access to the project AMC lists and runs Foundry agents either with its own managed identity or with a project API key. - **Managed Identity (same tenant):** grant AMC's managed identity a least-privilege role on the project, such as **Azure AI User**. Without it, AMC reports **Access not granted**. - **API key:** paste the project key. AMC stores it and never shows it again. ## Import a whole project 1. Go to **Agents** and select **Import from Foundry**. 2. Enter the **Foundry project endpoint**: the project URL, such as `https://<resource>.services.ai.azure.com/api/projects/<project>`, not an individual agent URL. 3. Choose the **Department**. Every imported agent is scoped to it. 4. Choose the authentication mode and select **Import project**. AMC adds one registration per agent and workflow in the project. Running the import again is safe: existing registrations stay, and only new ones are added. ## Register one agent 1. Go to **Agents** and select **Register**. 2. Set **Adapter** to **Azure AI Foundry**. 3. Set **Kind** to **Workflow** or **Agent**. It can't change after registration. 4. Set **Endpoint URL** to the agent's URL in the project, `<project-endpoint>/agents/<agent-name>/...`. AMC reads the agent name from the segment after `/agents/`. 5. Choose the department and authentication, then save. ## Name agents and workflows deliberately Foundry doesn't tell AMC whether a hosted agent is a workflow, so AMC classifies by the deployed name, ignoring case: | Name contains | AMC treats it as | Effect | | --- | --- | --- | | `subagent` | An internal helper | Hidden from AMC's catalog. It can't be run on its own. | | `agent` (and not `subagent`) | A single agent | Imported with **Kind** **Agent**. | | Neither | A workflow | Imported with **Kind** **Workflow**. | > **Important:** A workflow named like `multi-agent-research` contains `agent`, so an import classifies it as a single agent. Keep `agent` out of workflow names, or register the workflow by hand with **Kind** set to **Workflow**. **Kind** matters at run time. AMC runs a workflow in background mode (`background: true`, `store: true`), which is what lets it reconnect to a run after a dropped connection. By default, a registration is run in the older foreground shape only when its **Kind** is **Agent** and its type doesn't name a workflow. ## Test it 1. Before AMC, run the client script from [Host and deploy on Foundry](https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/) with `x-ms-debug-mode-enabled`. Confirm one `response.output_item.done` per step whose item `name` is the step id and whose `arguments` JSON has `delivered`, the token counts and `model`. 2. In AMC, select **Run now** for the registration and open the run. 3. Check that each executor shows as its own step with its result and model usage, and that the run total is not zero. 4. For a workflow, force a failure (for example, an invalid model name) and check the failed step shows the `failure_reason` text. A single agent has no per-step failure; the run itself fails. 5. For a human-input workflow, check the **Human input** step shows your question, answer it, and confirm the run continues to completion. 6. If you send `activity_log`, select **Show details** on the step and check the rows. Make sure every model the agent can run has a token rate configured in AMC, or its steps are priced at AMC's default rate. ## Related - [Troubleshoot Agent Framework agents](https://www.agentmissioncontrol.ai/developers/af-troubleshooting/) - [Report steps, tokens and human input](https://www.agentmissioncontrol.ai/developers/af-report-to-amc/) - [Host and deploy on Foundry](https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/) --- Source: https://www.agentmissioncontrol.ai/developers/af-register-and-test/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt # Troubleshoot Agent Framework agents Agent Mission Control developer docs > Microsoft Agent Framework (Python) > Fixes for the build, deploy, hosting and reporting failures seen with Agent Framework agents and workflows on Foundry and AMC. **Before you start:** A hosted agent or workflow, and access to its Foundry logs. ## Build and deploy | Symptom | Cause and fix | | --- | --- | | Remote build fails: `Cannot install agent-framework-core ... hyperlight-sandbox-backend-wasm ... ResolutionImpossible` | You depend on the `agent-framework` meta-package. Depend on `agent-framework-core`, `agent-framework-foundry` and `agent-framework-foundry-hosting` only. | | `bad_request: Unsupported region for Foundry Hosted Agents` | The account's region doesn't support hosted agents, and it can't be changed. Use a project in a supported region. | | azd: `project ... not found or is not eligible for the current configuration` | The same region problem, or a missing data-plane role such as **Foundry User**. | | `ImportError` for `AzureAIAgentsProvider` or `agent_framework.azure` | Older SDK surface. Use `FoundryChatClient`, `FoundryAgent` and `to_prompt_agent` from `agent_framework.foundry`; see [the overview](https://www.agentmissioncontrol.ai/developers/af-overview/). | | Agent fails at startup: running on protocol 1.0.0, but the agent requires protocol 2.0.0 | The responses 2.x runtime needs hosted-agent protocol `2.0.0`. Set `ProtocolVersionRecord(protocol="responses", version="2.0.0")` and use an environment that supports it. | | `TypeError` on `ResponsesServerOptions(resilient_background=True)` | `azure-ai-agentserver-responses` is older than 2.x. Bump the four pins together; see [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/). | | `RuntimeError` on `resilient_background=True` | You passed it for a single agent. It is workflow-only; build the job as a one-node workflow if it needs recovery. | ## Hosting | Symptom | Cause and fix | | --- | --- | | `Workflow.run() got an unexpected keyword argument 'messages'` | `ResponsesHostServer` was given a raw `Workflow`. Wrap it: `ResponsesHostServer(workflow.as_agent(name=...))`. | | `Workflow's start executor cannot handle list[Message]` | Add a `list[Message]` handler to the start executor. | | `@response_handler` signature validation fails | Remove `from __future__ import annotations` from that module. | | `Checkpoint deserialization blocked for type 'workflow:<Type>'` | Add that type, and every other custom edge or request type, to `CheckpointStoreProvider(allowed_checkpoint_types=[...])`. | | A human-input run never resumes | Without recovery the checkpoint is instance-local. Run one replica, or turn on `resilient_background` with the allow-list. | | Run stuck at `in_progress` after a deploy or a dropped connection; reconnect returns an empty stream | The run died with its process. Turn on `resilient_background` for workflows. | | Empty `output_text` from a smoke test | Expected when the terminal executor yields a dict rather than assistant text. The data is still in the response. | | A message-less trigger does nothing, or ignores your schema | Add middleware that appends a kickoff message, and set `response_format` in `default_options`; the host doesn't forward a caller's. | ## Reporting in AMC | Symptom | Cause and fix | | --- | --- | | Every step shows zero tokens, or the run has no cost | `usage_details` was read with `getattr`. It is a dict; use `.get("input_token_count")`. Hosted run-level `usage` is always empty, so markers are the only source. | | No steps appear, only the final result | Executors don't emit markers, or aren't in `intermediate_output_from`, so their markers aren't forwarded. | | One step is missing although its executor emits a marker | The marker's `call_id` and `name` differ, so AMC treats it as a real tool call and ignores it. Set both to the step id. | | Steps run but never show as failed | The executor re-raises without emitting `failure_reason` first. | | Cost looks wrong for a step | No token rate is configured for that model in AMC, or a `model_usage` key isn't the pricing model id. | | The human-input step has no question | The `request_data` dataclass has no `question` field. | | A workflow was imported as an agent, or a helper is missing from the catalog | Name-based classification: `agent` in the name means a single agent, `subagent` means hidden. Rename, or register by hand with the right **Kind**. | | Detail rows missing | `activity_log` is off (`EMIT_STEP_ACTIVITY_LOG=false`), the capture hit an error and fell back, or the model emitted no reasoning. | For Foundry-side failures, Microsoft's hosted-agent troubleshooting guide is at [aka.ms/hostedagents/tsg/code](https://aka.ms/hostedagents/tsg/code). ## Related - [Harden an agent or workflow for AMC](https://www.agentmissioncontrol.ai/developers/af-amc-hardening/) - [Host and deploy on Foundry](https://www.agentmissioncontrol.ai/developers/af-host-and-deploy/) - [Register and test in AMC](https://www.agentmissioncontrol.ai/developers/af-register-and-test/) --- Source: https://www.agentmissioncontrol.ai/developers/af-troubleshooting/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt