# 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
