Skip to content

fix(crew): SDK auto-fills tool_call response for transfer/end-call (no user code) - #104

Merged
abhishekmishragithub merged 3 commits into
mainfrom
fix-crew-tool-response
Aug 20, 2026
Merged

fix(crew): SDK auto-fills tool_call response for transfer/end-call (no user code)#104
abhishekmishragithub merged 3 commits into
mainfrom
fix-crew-tool-response

Conversation

@abhishekmishragithub

Copy link
Copy Markdown
Collaborator

Why

Follow-up to #102. Crew transfer_call shows on the Events tab now, but its response was empty ({"arguments": {}}) while single-prompt shows {status, transfer_number}. Reason: the crew transfer_call is a fire-and-forget @function_tool that emits a transfer event and returns nothing, and the tool_call response is literally the tool's return value (same as the orchestrator — pipecat records function_response: result). Single-prompt looks rich only because its built-in transfer tool returns a dict.

We don't want to make users return a dict from their tool. So the SDK does it.

Fix (SDK-side, zero user code)

OutputCrewNode.send_event already latches handoff events (_HANDOFF_EVENTS). It now also records a handoff summary, and ToolRegistry._execute_single attaches that summary as the tool_call_end response when the tool itself returned None:

{ "arguments": {}, "response": { "status": "success", "action": "transfer_call",
  "transfer_number": "+91...", "transfer_type": "warm_transfer", "on_hold_music": "relaxing_sound" } }
  • Only the observability event is enriched; the tool's own return value and the LLM-facing ToolResult are unchanged (no behavior change).
  • A plain None-returning tool with no handoff stays as before (no response).
  • Works for both transfer_call and end-call, matching single-prompt.

Tests

transfer + end-call tools get the handoff response; plain None tool has none. ruff clean; crew suite (32) green.

Ships in the next release (5.11.2); crews get it on redeploy with no code change.

…o user code)

Crew transfer/end-call tools are fire-and-forget: they emit an
SDKAgentTransferConversationEvent / SDKAgentEndCallEvent and return nothing, so
the tool_call event showed only {arguments} with no response — unlike
single-prompt agents, whose built-in transfer tool returns {status,
transfer_number}.

Rather than make users return a dict from their @function_tool, the SDK now does
it: OutputCrewNode.send_event (which already latches handoff events) records a
handoff summary, and ToolRegistry._execute_single attaches it as the
tool_call_end response when the tool itself returned None. Same shape
single-prompt surfaces (status/action/transfer_number/transfer_type/on_hold_music),
zero user code. Only the observability event is enriched; the tool's own return
value and the LLM-facing result are unchanged.

Tests: transfer + end-call tools get the handoff response; a plain None tool
still has no response.
…esponse

Transfer has two kinds: cold (direct connect) and warm (brief the specialist,
then bridge). The event summary now reflects which happened: warm transfers add
the private/public handoff option (type + prompt); cold transfers carry none.
transfer_type (cold_transfer|warm_transfer) + on_hold_music already distinguished
them; this adds the whisper detail for warm.
…oop)

CrewNode init grabs the event loop, which raises on Python 3.9 when there is no
current loop. Construct the node inside asyncio.run so the handoff-response tests
pass on 3.9.
@abhishekmishragithub
abhishekmishragithub merged commit 325df8e into main Aug 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant