Skip to content

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

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 (it is replaced on this branch by the group chat rewrite). Archived files at that commit: travel_assistant/coordinator.py and step_files/07/coordinator.py. The graph:

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 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:
    { "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 · handoff_workflow_as_agent.py

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions