---
type: trekresearch-brief
created: 2026-06-24
question: "Does Microsoft Agent Framework 1.9.0 (core + orchestrations 1.0.0) provide a native human-in-the-loop primitive to pause a workflow for external input and resume it, and how does it interact with checkpointing and session state?"
confidence: 0.85
dimensions: 6
mcp_servers_used: [microsoft-learn, tavily]
local_agents_used: [installed-source-introspection (orchestrator, main context)]
external_agents_used: [docs-researcher, community-researcher, security-researcher, contrarian-researcher, gemini-bridge (unavailable)]
topic: 1
brief: .claude/projects/2026-06-24-fase2-mvp-vertical-slice/brief.md
---
# Native human-in-the-loop in MAF 1.9.0 workflows
> Generated by trekresearch (Voyage 5.6.0) on 2026-06-24. Topic 1 of 3 for the
> Fase 2 MVP-vertical-slice brief. Scope: external swarm + installed-source
> introspection (API truth from the installed 1.9.0 package wins over Learn docs,
> per CLAUDE.md). Gemini second opinion was unavailable (MCP SDK broke on
> Google's May-2026 Interactions API change) — triangulation rests on docs +
> community + contrarian + installed source.
## Research Question
Does `agent-framework` 1.9.0 (`agent-framework-core` + `agent-framework-orchestrations` 1.0.0)
provide a native human-in-the-loop (HITL) mechanism to pause a workflow for
external/human input and resume it, and how does it interact with checkpointing
and session/conversation state? Decision it feeds: the Fase 2 "expert verdict
captured via HITL" success criterion — design the two-layer HITL + verdict-capture
+ feed-forward seam, and the per-project workflow graph.
## Executive Summary
**Yes — native HITL exists and is GA in 1.9.0**, in three distinct surfaces:
(1) a workflow-level request/response primitive (`ctx.request_info()` + `@response_handler`,
resume via `run(responses=...)`), (2) durable checkpoint-and-resume that persists
pending requests, and (3) an orchestration-level human-approval gate that — verified
in installed source — **works on our Group Chat maker-checker default** via
`GroupChatBuilder.with_request_info(agents=[...])`. **Confidence on the mechanism is
HIGH (installed-source-verified); confidence on durable checkpoint-resume RELIABILITY
is LOW/contradictory** — official docs promise lossless durable resume, but the bug
tracker shows the *pause* half is solid while the *persist-and-resume* half has
multiple open/silent-failure bugs surviving into the 1.9.0 line (#5818, #6127, #6372,
#5621) plus a pickle type-allowlist that rejects custom Pydantic types on restore (#5810).
**Key caveat:** our learned-verdict path is an *indefinite, out-of-band, never-resume-the-
original-run* decision whose durable artifact is the hand-rolled VerdictStore — so the
recommendation is to use the GA *in-run* approval gate for synchronous Layer-1 review,
keep the durable learned verdict OUT-OF-BAND in the VerdictStore, and **defer
checkpointing off the MVP critical path**.
## Dimensions
### 1. Native HITL primitive — Confidence: high
**Installed-source findings (ground truth, 1.9.0):**
- `WorkflowContext.request_info(request_data: object, response_type: type, *, request_id: str | None = None)` — any executor calls this to suspend the workflow and request external input (`agent_framework/_workflows/_workflow_context.py:393`).
- Every `Executor` inherits `RequestInfoMixin` (`_workflows/_executor.py:30`); a method decorated `@response_handler` receives the typed response on resume; handlers are matched by the request/response **type annotations** (`_workflows/_request_info_mixin.py`).
- `RequestInfoExecutor` is **NOT present** in installed 1.9.0 (only `RequestInfoMixin` + `response_handler` + `ctx.request_info`). `send_responses`/`send_responses_streaming` are **NOT public** on `Workflow` — internal `_send_responses_internal` only.
**External findings:**
- Docs + AutoGen→MAF migration guide confirm "workflows can pause execution and wait for external input before continuing" — a capability AutoGen's `Team` lacked.
- The versioned upgrade guides explain why the installed surface looks as it does: `RequestInfoExecutor` removed (python-1.0.0b251104) in favour of `ctx.request_info()` + `@response_handler`; `send_responses*` removed (python-1.0.0b260210, PR #3720) in favour of `run(responses=...)`. Both predate 1.9.0 (2026-06-18). ,
**Contradictions:**
- The auto-generated **API-reference** page (`agent-framework-python-latest`) still lists `send_responses*` and prose-mentions `RequestInfoExecutor`. This **conflicts** with both the dated changes-guide and the installed source. Resolution: **installed source wins** (CLAUDE.md invariant) — those names are doc-lag / the C# (`RequestPort`) idiom. Do not write them into Python 1.9.0 code.
### 2. Pause/resume mechanics — Confidence: high
**Installed-source findings:**
- `Workflow.run(message=None, *, responses=None, checkpoint_id=None, checkpoint_storage=None)` — three intents, exactly one of `message` / `responses` / `checkpoint_id` per call (`_workflows/_workflow.py:675+`). `responses` is `Mapping[str, Any]` keyed by `request_id`; mutually exclusive with `message`; **can be combined with `checkpoint_id`** ("restore then send responses in a single call" per the docstring).
- Run-state enum (`_workflows/_events.py:58-65`): `IDLE`, `IDLE_WITH_PENDING_REQUESTS` (paused awaiting input — non-terminal), `IN_PROGRESS_PENDING_REQUESTS`. `result.get_final_state()` returns it.
- `RunnerContext.send_request_info_response(request_id, response)` validates the response **type** against the original request and raises `ValueError` on unknown `request_id` or type mismatch (`_workflows/_runner_context.py:457-471`).
- Events collapsed to a generic `WorkflowEvent[DataT]` carrying `.request_id` / `.data` / `.type` (no `RequestInfoEvent` subclass).
**External findings:**
- Canonical loop (official sample `guessing_game_with_human_input.py`): `run(..., stream=True)` → collect `request_info` events → `run(stream=True, responses=pending)` → repeat until no pending requests. **In-process resume needs NO checkpoint** — "state is preserved across multiple calls to run."
- Request IDs are caller-supplied or auto-UUID; responses strongly typed via `response_type`.
### 3. Checkpointing interaction — Confidence: high (mechanism) / contradictory (reliability)
**Installed-source + docs findings (mechanism — high):**
- `WorkflowCheckpoint.pending_request_info_events: dict[str, ...]` — pending HITL requests ARE serialized into the checkpoint (`_workflows/_checkpoint.py:81`). On restore the runner rehydrates them (`_runner_context.py:424-425`) and **re-emits** them as `request_info` events; you then answer with a *separate* `run(responses=...)` call (you cannot inject responses during the restore call itself).
- Storage backends (all implement the `CheckpointStorage` protocol, swap without code change): `InMemoryCheckpointStorage` (ephemeral), `FileCheckpointStorage` (local disk, explicit `storage_path`, **pickle** + restricted unpickler), `CosmosCheckpointStorage` (Azure, preview — egress). Checkpoints fire at superstep boundaries, so a HITL pause lands on one cleanly.
**Contradictions (reliability — LOW):**
- **Docs** present durable "checkpoint → exit process → restore → respond" as a supported happy path.
- **Community / bug tracker** shows persist-and-resume is the fragile half:
- #5818 (OPEN, Magentic + `AgentSession` resume): same `request_id` sent back, workflow does not resume.
- #6127 (unanswered, Sequential): resume re-prompts the same approval **and** re-executes the function; duplicate `CallId` in streaming chunks.
- #6372 (fixed PR #6491, but affected 1.1.0–**1.9.0**): fan-in barrier silently loses buffered messages across checkpoint/resume — **no error raised**.
- #5621 (OPEN, Handoff restore): `Expected exactly one update for key 'SharedState'`, unmatched ToolApproval.
- #5810 (fixed PR #6049): restore **type-allowlist** blocks non-safe types (`Checkpoint deserialization blocked for type … MessageRole`) — custom/Pydantic payloads must be registered via `allowed_checkpoint_types`.
- #3255 (fixed PR #3689): sub-workflow restore re-sent already-answered requests → `Response provided for unknown request ID`.
- **Version-skew (documented):** PR #3744 — "**Existing checkpoints cannot be resumed between versions.**" A MAF bump invalidates stored checkpoints.
### 4. Session / conversation-state interaction — Confidence: high
**External findings:**
- The built-in `AgentExecutor` (wraps an agent inside a workflow) serializes on checkpoint: internal message cache, **full conversation history**, agent session state, and pending requests/responses — and restores them. So a HITL-paused agent-bearing workflow does NOT lose chat history.
- **Material gap for the Azure/Foundry profile:** "Checkpointing with agents that use **server-side sessions** (e.g. `FoundryAgent`) has limitations. Server-side session state is **not captured in checkpoints**." A durable HITL pause with a Foundry-hosted agent will not have its conversation reliably restored from the checkpoint alone. (Same doc.)
- For pure custom-executor workflows (no `AgentExecutor`): only shared `state`, in-transit messages, and pending requests are captured; executor-local fields persist only if you override `on_checkpoint_save()` / `on_checkpoint_restore()`.
- Pre-1.9.0 rename relevant to code around a pause: `SharedState`→`State`, `ctx.shared_state`→`ctx.state`, state getters/setters now **synchronous** (PR #3667).
**Cross-link:** this is the Fase 1 B7 bleed vector — cross-run conversation state lives in `AgentSession.state` + `InMemoryHistoryProvider`. The HITL/checkpoint state is workflow-level and distinct, but `fresh_workflow()` isolation still governs whether a restored conversation contaminates the next project run. (See `docs/research/2026-06-24-maf-capability-map.md`, Fase 1 B7.)
### 5. Group-chat / orchestration-level approval gate — Confidence: high
**Installed-source findings (decisive for our debate default):**
- `GroupChatBuilder.with_request_info(*, agents: Sequence[str | SupportsAgentRun] | None = None)` **EXISTS** (`agent_framework_orchestrations/_group_chat.py:882`). It pauses after the named agent(s) respond and emits a `request_info` event (`type='request_info'`) "that allows the caller to review the conversation and optionally [approve/edit] … the standard response_handler/request_info pattern." Same method exists on `SequentialBuilder` (`_sequential.py:154`).
- Participants matching the filter are wrapped as `AgentApprovalExecutor(WorkflowExecutor)` (`_orchestration_request_info.py:168`), constructed `allow_direct_output=True` so the user-approved final response surfaces as workflow output.
- The human reply object is `AgentRequestInfoResponse` (public export) with `.approve()` (accept as-is), `.from_strings([text])`, `.from_messages([...])` (`_orchestration_request_info.py:44-85`). Supplied via `run(responses={request_id: AgentRequestInfoResponse...})`.
- `resolve_request_info_filter(agents)` selects which agents pause for approval — e.g. pause only before the **checker** in maker-checker.
- A separate, lighter gate also exists: tool-approval via `@tool(approval_mode="always_require")` → `function_approval_request` content → approve/deny (in-run, no graph).
**External findings:**
- Docs/maintainer confirm there is **no single universal "approve/edit/reject" object**; the documented routes are (a) `with_request_info` + `AgentRequestInfoResponse`, (b) tool-approval, (c) Magentic plan-review (`enable_plan_review`) — Magentic is experimental and OFF our path. For arbitrary maker-checker over agent output, the intended route is `with_request_info` (now installed-source-confirmed for GroupChat) or a custom executor calling `ctx.request_info()`.
- **Caveat:** community shows the orchestration approval *resume* path is where bugs cluster (#5818, #6127, #6006) — the gate fires reliably; persisting/resuming the approval across serialization is fragile.
### 6. Design implication for our two-layer HITL + verdict capture — Confidence: high (recommendation)
This is decision-relevant — see **Recommendation** below. Short form: the brief's
fallback assumption ("if MAF lacks native HITL, capture out-of-band") is **partly
inverted**: MAF *has* native HITL, and the right split is to use the GA *in-run*
pieces for synchronous review but keep the *durable learned verdict* out-of-band.
## External Knowledge
### Best Practice
- Canonical 1.9.0 HITL = `ctx.request_info()` + `@response_handler` + `run(responses=)`; detect pause via `IDLE_WITH_PENDING_REQUESTS` / `get_request_info_events()`. Official samples: `guessing_game_with_human_input.py`, `sequential_request_info.py`, `checkpoint/checkpoint_with_human_in_the_loop.py`, `magentic_human_plan_review.py` (all under `microsoft/agent-framework` `python/samples/03-workflows/`).
### Security (relevant to D3 no-silent-egress + local-only)
- `FileCheckpointStorage` writes **unencrypted pickle** blobs containing conversation history + pending HITL payloads + shared state. Encryption-at-rest / permissions / ACLs are **the developer's responsibility** (no built-in encryption). Lock the `storage_path` down (dedicated dir, `0700`/`0600`, encrypted volume; macOS FileVault helps).
- Restore is hardened: restricted unpickler **ON by default** since 1.0.1 (we're on 1.9.0); non-safe types throw `WorkflowCheckpointException` unless registered in `allowed_checkpoint_types`. Treat it as a safety net, not the control — "never load checkpoints from untrusted sources."
- **No CVEs** against `agent-framework*` (GitHub Security Advisories empty; OSV empty). Two transitive Starlette advisories on deps.dev — confirm via `uv run pip-audit`. Semantic Kernel CVEs (CVE-2026-26030/-25592) are a *different package* and not in the checkpoint path; only relevant if `semantic-kernel` is pulled in (it is not, per our pinned tree).
- **Telemetry OFF by default** — no exporter ships; `ENABLE_INSTRUMENTATION`/`ENABLE_SENSITIVE_DATA`/`ENABLE_CONSOLE_EXPORTERS` all default false. No-silent-egress holds out of the box if we (a) don't set those, (b) set no `OTEL_EXPORTER_OTLP_*`, (c) use File/InMemory (never Cosmos). One subtlety: MAF auto-propagates OTel trace context into MCP `tools/call` `_meta` when a span is active — inert with instrumentation off; keep MCP servers local (stdio) regardless.
### Known Issues
- See Dimension 3 contradictions. Plus: `with_request_info()` naming is opaque and there is no event-type filter for which events trigger a HITL pause (#3534, open). Local-model + HITL is **unverified by anyone** — zero community signal; several worst resume bugs are Azure-server-side-persistence-specific and may simply not apply locally, but then we own conversation-history persistence ourselves. Spike locally; do not rely on precedent.
## Gemini Second Opinion
Unavailable. The `gemini-mcp` server's client SDK predates Google's May-2026
Interactions API breaking change and returned `400 BadRequestError` before any
research ran. No independent Gemini triangulation was obtained for this topic;
treat the second opinion as absent (not negative). To restore: upgrade the
gemini-mcp server's client SDK to ≥ 2.0.0.
## Synthesis
The triangulation surfaces an insight no single source states: **MAF 1.9.0 has
three different HITL surfaces, and the strongest one for *our* need is the
lightest one — while the heaviest one (durable checkpoint-resume) is both the
shakiest in practice and a poor fit for the problem.**
1. **In-run synchronous review** (`GroupChatBuilder.with_request_info` →
`AgentApprovalExecutor` → `run(responses={id: AgentRequestInfoResponse...})`)
is GA, installed-source-confirmed for our Group Chat maker-checker default, and
needs **no checkpointing** (state persists across `run()` calls in-process).
This is the solid, happy-path piece.
2. **Durable cross-process pause** (checkpoint + restore + responses) is where the
docs promise and the bug tracker diverge hardest: open/silent-failure resume
bugs into the 1.9.0 line, a pickle type-allowlist that fights our Pydantic IR,
and "checkpoints cannot be resumed between versions." The contrarian pass is
right that coupling our **highest-value data path** (the verdict the system
*learns* from) to MAF's **most-churned, least-durable, Azure-favoring** surface
is a self-inflicted risk — and a lock-in against the D7 Claude-SDK sibling,
which has no executor/checkpoint model and can only share a *framework-agnostic*
verdict seam.
3. The actual shape of "fagekspert enters a verdict the next run learns from" is
an **indefinite, out-of-band, never-resume-the-original-run** decision. Its
durable artifact is the hand-rolled **VerdictStore**, not an in-flight workflow
checkpoint. A workflow checkpoint is engineered for "pause seconds-to-minutes,
resume the same process" — the wrong tool for "pause indefinitely, decide
elsewhere."
So the brief's binary ("native HITL → use it; else out-of-band") resolves to a
**split**: adopt the GA in-run approval gate for the *synchronous Layer-1 review*
where it fits; keep the *durable, learning-loop verdict* out-of-band in the
VerdictStore; and **defer checkpointing off the MVP critical path** (matches the
brief's `[OPEN]` default — and Topic 1 confirms the default rather than overturning
it, because durable checkpoint-resume is the fragile part).
## Open Questions
- **Layer-1 review: native gate vs. simplest possible?** `with_request_info` is
GA and fits, but a custom executor calling `ctx.request_info()` gives full
control over the request payload (the `ValidatedProposal` + provenance). Decide
in /trekplan: native `with_request_info(agents=[checker])` vs custom request-info
executor. Either way: in-process, no checkpoint.
- **Is Layer-1 even in the MVP, or is it async-only?** The brief marks two-layer
HITL semantics (sync review vs async+notification stub) as `[OPEN]`. If Fase 2
ships only the async/out-of-band verdict + notification *stub* (B11), the native
in-run gate may be deferred too — fewer moving parts. Resolve in /trekplan.
- **Local-profile HITL behaviour** — unverified by anyone. Needs a self-spike:
run the approval gate / `request_info` loop against the local OpenAI-compatible
endpoint (Topic 3) and confirm it fires and resumes. Tie to Topic 3's outcome.
- **If durable pause is ever needed:** the acceptance gate must be a
resume-integrity test (pause → checkpoint → restore → assert pending requests
match + conversation intact), given #6372/#5621-class silent failures. Out of
MVP scope but record the condition.
## Recommendation
**For the Fase 2 MVP, capture the expert verdict OUT-OF-BAND and keep the durable
artifact in the hand-rolled VerdictStore; do NOT couple the learned-verdict path
to native checkpoint-resume.** Concretely:
1. **Layer 2 (durable, learning loop) = out-of-band VerdictStore.** The emitted
`ValidatedProposal` + provenance is presented to the fagekspert out-of-band; the
verdict is written to the VerdictStore; the next run retrieves it via the ExpeL
`ContextProvider` seam (`extend_instructions(source_id, instructions)`). This is
framework-agnostic, D7-portable, and the success-criterion ("second run
retrieves the prior verdict") is satisfied without any MAF checkpoint.
2. **Layer 1 (optional in-run synchronous review) = GA native gate IF included.**
If Fase 2 ships a synchronous review, use `GroupChatBuilder.with_request_info(
agents=[checker])` (or a custom `ctx.request_info()` executor for a richer typed
payload) — in-process, resume via `run(responses=...)`, **no checkpointing**.
Register any custom Pydantic response type's expectations now so it survives if
checkpointing is ever added.
3. **Defer checkpointing off the MVP critical path** (brief `[OPEN]` default
upheld). `InMemoryCheckpointStorage` is fine for tests; do not put
`FileCheckpointStorage` durable HITL on the critical path. If added later:
File/InMemory only (never Cosmos — egress), locked-down `storage_path`,
`allowed_checkpoint_types` for our IR, a resume-integrity acceptance test, and
awareness of cross-version checkpoint invalidation.
4. **Provenance, not checkpoint, carries the audit trail** — the single emitted
proposal's provenance stamp (citations + model/role + validator decision + token
usage) is the durable record, consistent with the no-silent-egress + provenance
NFRs.
Risks to carry into the plan: (a) MAF HITL/checkpoint API churn → any MAF-native
HITL code carries upgrade cost; pin the surface and watch the changes guide.
(b) Local-profile HITL is unverified → spike with Topic 3's local client.
(c) If a synchronous in-run gate is used, the orchestration *resume* path is where
community bugs cluster — keep it in-process (no serialization boundary) to dodge
that whole class.
## Sources
| # | Source | Type | Quality | Used in |
|---|--------|------|---------|---------|
| 1 | `.venv/.../agent_framework/_workflows/_workflow_context.py:393` (`request_info`) | codebase | high | Dim 1, 2 |
| 2 | `.venv/.../agent_framework/_workflows/_request_info_mixin.py` (`response_handler`, `RequestInfoMixin`) | codebase | high | Dim 1 |
| 3 | `.venv/.../agent_framework/_workflows/_workflow.py:675+` (`run(responses=, checkpoint_id=)`) | codebase | high | Dim 2, 3 |
| 4 | `.venv/.../agent_framework/_workflows/_events.py:58-65` (`WorkflowRunState`) | codebase | high | Dim 2 |
| 5 | `.venv/.../agent_framework/_workflows/_runner_context.py:424,457-471` (rehydrate + `send_request_info_response`) | codebase | high | Dim 2, 3 |
| 6 | `.venv/.../agent_framework/_workflows/_checkpoint.py:81` (`pending_request_info_events`) | codebase | high | Dim 3 |
| 7 | `.venv/.../agent_framework_orchestrations/_group_chat.py:882` (`GroupChatBuilder.with_request_info`) | codebase | high | Dim 5 |
| 8 | `.venv/.../agent_framework_orchestrations/_orchestration_request_info.py:44-85,168` (`AgentRequestInfoResponse`, `AgentApprovalExecutor`) | codebase | high | Dim 5 |
| 9 | | official | high | Dim 1,2,3,5 |
| 10 | | official | high | Dim 3, Security |
| 11 | | official | high | Dim 4 |
| 12 | | official | high | Dim 1,2,3 |
| 13 | | official | high | Dim 1 |
| 14 | | official | high | Dim 1 |
| 15 | (Magentic AgentSession resume, OPEN) | community | high | Dim 3,5 |
| 16 | (Sequential re-prompt+re-exec) | community | medium | Dim 3,5 |
| 17 | (fan-in barrier silent loss, 1.1.0–1.9.0) | community | high | Dim 3 |
| 18 | (Handoff restore fails, OPEN) | community | high | Dim 3 |
| 19 | (checkpoint type-allowlist) | community | high | Dim 3, Security |
| 20 | (sub-workflow dup request, fixed) | community | high | Dim 3 |
| 21 | (with_request_info naming/filter, OPEN) | community | medium | Known Issues |
| 22 | (no universal approval object; custom executor) | community | high | Dim 5 |
| 23 | | community | medium | Synthesis |
| 24 | (checkpoint limitations) | community | medium | Synthesis |
| 25 | (stability/production timeline, unanswered) | community | medium | Synthesis |
| 26 | (no advisories) | official | high | Security |
| 27 | (no results) | official | high | Security |
| 28 | (telemetry off by default) | official | high | Security |
| 29 | (local provider exists) | official | medium | Known Issues |
| 30 | (1.9.0 = 2026-06-18) | official | high | Exec summary |