Azure-Samples / Azure-Samples/foundry-hosted-agents-workshop

[Bug] Step 7 handoff + hosted agents unreliable — archived; moving Step 7 to group chat

Open
#19 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Python
Stars
7
Forks
7
Avg merge
2h 59m
Merged PRs (30d)
3

Description

## What happened?

Step 7 currently builds the multi-agent TravelBuddy as a **runtime handoff** (`HandoffBuilder` exposed via `workflow.as_agent()`, hosted by `ResponsesHostServer`). In practice, **handoff + hosted agents do not work reliably** and we are pivoting Step 7 to a **group chat** orchestration instead. This issue **archives the handoff attempt + investigation** so we can revisit it once the upstream handoff/hosting story matures.

The handoff code being archived is preserved at commit [`a01ae6d`](https://github.com/Azure-Samples/foundry-hosted-agents-workshop/commit/a01ae6d789660227fba45025a0e8ac55a0a776cb) (it is replaced on this branch by the group chat rewrite). Archived files at that commit: [`travel_assistant/coordinator.py`](https://github.com/Azure-Samples/foundry-hosted-agents-workshop/blob/a01ae6d789660227fba45025a0e8ac55a0a776cb/.workshop/solutions/07-multi-agent/travel_assistant/coordinator.py) and [`step_files/07/coordinator.py`](https://github.com/Azure-Samples/foundry-hosted-agents-workshop/blob/a01ae6d789660227fba45025a0e8ac55a0a776cb/.workshop/step_files/07/coordinator.py). The graph:

```python
workflow = (
HandoffBuilder(name="travelbuddy-runtime-handoff",
participants=[coordinator, flights, hotels, activities])
.with_start_agent(coordinator)
.add_handoff(coordinator, [flights, hotels, activities])
.add_handoff(flights, [coordinator])
.add_handoff(hotels, [coordinator])
.add_handoff(activities, [coordinator])
.build()
)
return workflow.as_agent()
```

> **Note — snippet simplified.** The block above shows only the graph edges. The exact archived implementation at [`a01ae6d`](https://github.com/Azure-Samples/foundry-hosted-agents-workshop/blob/a01ae6d789660227fba45025a0e8ac55a0a776cb/.workshop/solutions/07-multi-agent/travel_assistant/coordinator.py) also sets `require_per_service_call_history_persistence=True` on every participant and passes a `termination_condition` to `HandoffBuilder(...)` (the fragile symptom-2 workaround). To re-introduce the handoff, start from the permalinked file, not this snippet.

### Symptoms observed

1. **A follow-up (second) question crashes.** The first question is answered, but any subsequent question in the same conversation fails with a `response.failed` event:
```json
{ "error": { "code": "server_error",
"message": "Unexpected content type while awaiting request info responses." } }
```
2. **With a `termination_condition` fix applied, the run stops after the first specialist.** It routes to the Flights specialist, shows the flight result, and pauses (conversationally) instead of producing the full flights+hotel+activities plan in one shot. Follow-ups then continue to the next specialist, but the flagship "all three specialists in one invocation" behavior the Step 7 doc promises no longer holds reliably.
3. **The specialist is invoked but never makes a function/tool call.** In at least one run the Flights specialist was selected but never actually called its tools (flight search / `get_local_time` / `convert_currency`) — so it answered from memory instead of grounding the answer, defeating the point of the specialist.

## Steps to reproduce

1. Advance to Step 7 and build the `HandoffBuilder` coordinator (`travel_assistant/coordinator.py`), host with `ResponsesHostServer`.
2. `azd ai agent run`, then `azd ai agent invoke --local "Help me plan a 5-day Tokyo trip: flights from Lisbon, a hotel near Shibuya under €200/night, and a day-trip suggestion."`
3. Ask a **second** question in the same conversation → symptom 1 (crash).
4. Add `termination_condition=lambda conv: conv and conv[-1].role == "assistant" and conv[-1].author_name == "Coordinator"` + instruct the Coordinator to route silently → symptom 2 (stops after one specialist; follow-ups continue).
5. Inspect the Flights specialist's turn → symptom 3 (no tool call in some runs).

## Expected behavior

A hosted multi-agent TravelBuddy should answer an initial multi-part request by consulting the relevant specialists (each actually invoking its grounded tools), synthesize one answer, and accept follow-up questions in the same conversation **without crashing**.

## Additional context

**Root cause of symptom 1 (crash).** `HandoffBuilder` runs human-in-the-loop by default: after every non-handoff Coordinator turn it calls `ctx.request_info(...)` and parks the workflow in `WorkflowRunState.IDLE_WITH_PENDING_REQUESTS`, expecting the next input as a **function-result** message correlated to that pending request (this is what the upstream `handoff_workflow_as_agent.py` sample does). But the hosted `ResponsesHostServer` delivers the next user turn as plain **text**. On the follow-up, `agent_framework_foundry_hosting/_responses.py::_handle_inner_workflow` restores the prior checkpoint (which replays the pending `request_info` and repopulates `self._agent.pending_requests`), then runs the workflow with the new text; `WorkflowAgent._extract_function_responses()` only accepts `function_approval_response` / `function_result` content and raises `AgentInvalidResponseException: Unexpected content type while awaiting request info responses.`

**Why the `termination_condition` fix is fragile (symptom 2).** A `termination_condition` makes each turn end `IDLE` (so the hosting layer replays history as a fresh run next turn — fixing the crash). But `agent_framework_orchestrations/_handoff.py::_run_agent_and_emit` (line ~363) checks the *same* condition **before each executor runs**, so any condition that matches a Coordinator/assistant text message will (a) terminate mid-plan when the Coordinator produces intermediate text and (b) skip the next specialist if the Coordinator narrated its handoff. The only way to distinguish "final answer" from "intermediate/narration" text purely from the conversation is to force **silent routing** (Coordinator emits only the handoff tool call, no text) — which is model-dependent and breaks on weaker models or across multi-hop plans. There is no built-in "disable request_info" switch on `HandoffBuilder`.

**Why group chat is the chosen replacement.** `agent_framework_orchestrations/_group_chat.py` has **zero `request_info` calls** in its default flow (`_request_info_enabled=False`). The `GroupChatBuilder` runs an orchestrator (an LLM `orchestrator_agent` returning structured `AgentOrchestrationOutput` = `terminate`/`next_speaker`/`final_message`, or a deterministic `selection_func`) for up to `max_rounds`, then the workflow **completes (IDLE)**. No parking → no follow-up crash. The orchestrator's `final_message` is the synthesized answer, and the Activities participant can still own the PDF/guardrails deliverable by being selected last.

**Versions:** `agent-framework-core==1.10.0`, `agent-framework-orchestrations==1.0.0`, `agent-framework-foundry-hosting>=1.0.0a260630`.

**Open questions for future investigation of the handoff path:**
- Does a newer `agent-framework-foundry-hosting` translate a hosted text turn into a `request_info` response so parked handoff workflows can resume? (Would fix symptom 1 without a `termination_condition`.)
- Symptom 3 (specialist invoked but no tool call) — is this a handoff replay/`store=False` interaction, a model issue, or a toolbox wiring issue under handoff? Reproduce and isolate.
- Is there a supported way to host a human-in-the-loop handoff as a fire-and-forget text agent, or is handoff-as-hosted-agent intended to be single-turn per conversation?

Docs: [handoff orchestration](https://learn.microsoft.com/agent-framework/user-guide/agent-orchestration/handoff) · [`handoff_workflow_as_agent.py`](https://github.com/microsoft/agent-framework/blob/main/python/samples/03-workflows/agents/handoff_workflow_as_agent.py)

Contributor guide

Open the contributing guide

Research direction

Start with the archived implementations at commit a01ae6d: .workshop/solutions/07-multi-agent/travel_assistant/coordinator.py and .workshop/step_files/07/coordinator.py, then compare them with the current group chat rewrite. Reproduce the follow-up crash and specialist tool-call behavior using the listed azd commands; future work is complete only when hosted conversations handle follow-ups reliably and all relevant specialists use their tools.

Written by the indexing model from the issue text.

Assessment

Tech stack
azure, python
Domain
ai-infra-agents
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.