Browse the docs

Human-in-the-loop for a Foundry hybrid agent

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.View as Markdown

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
{
  "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.

Last updated 2026-10-21