Skip to content

fix(pipeline): inject built-in transfer_call / end_call tools into LLMLoop#115

Merged
nicolotognoni merged 1 commit into
PatterAI:mainfrom
knowsuchagency:fix/pipeline-builtin-tools
May 27, 2026
Merged

fix(pipeline): inject built-in transfer_call / end_call tools into LLMLoop#115
nicolotognoni merged 1 commit into
PatterAI:mainfrom
knowsuchagency:fix/pipeline-builtin-tools

Conversation

@knowsuchagency
Copy link
Copy Markdown
Contributor

Summary

  • Pipeline mode (PipelineStreamHandlerLLMLoop) was passing only the user-provided tools through, so the LLM never saw transfer_call or end_call — even though the docs and the add-tools-and-handoffs Agent Skill (PR docs: add Agent Skills bundle for AI agent harnesses #108) describe them as available across all three modes.
  • Mirror the realtime path's agent_tools + [TRANSFER_CALL_TOOL, END_CALL_TOOL] injection (stream_handler.py:997) via a new helper _augment_with_builtin_handoff_tools that builds handler closures wired to the telephony-level _transfer_fn / _hangup_fn already attached to the stream handler.
  • 6 new unit tests in tests/test_pipeline_builtin_tools.py cover empty-user-tools, non-empty-user-tools, missing-fn, handler-dispatch (transfer + hangup), and graceful-empty-args paths.

Closes #110.

Why

Reproduced against gpt-4o-mini in pipeline mode (OpenAILLM + DeepgramSTT + ElevenLabsRestTTS). The LLM correctly heard the affirmative ("Yeah. Go ahead and transfer me.") and replied "Alright, transferring you now." — but emitted no tool call. Twilio call events showed zero POST /Calls/{sid}.json. The tool simply wasn't registered.

stream_handler.py:2422-2435 (before this PR):

self._llm_loop = LLMLoop(
    ...,
    tools=self.agent.tools,   # <-- only user tools
    ...,
)

vs realtime stream_handler.py:997:

openai_tools = agent_tools + [TRANSFER_CALL_TOOL, END_CALL_TOOL]

Implementation notes

The handler shape inside LLMLoop is (arguments_dict, call_context_dict) (see ToolExecutor._invoke_handler at tools/tool_executor.py:46-63). The helper builds two async closures with that exact signature and routes them to self._transfer_fn(number) / self._hangup_fn() which already exist on PipelineStreamHandler (assigned at stream_handler.py:1899-1900 from the telephony adapter at telephony/twilio.py:483-484).

Built-ins are skipped when the corresponding telephony fn is missing — keeps the unit-test harness path clean (no Twilio adapter → no transfer/hangup tools, matching the prior behaviour for non-telephony tests).

TypeScript parity

The TypeScript SDK's libraries/typescript/src/server.ts already injects the same two built-ins for both paths (agentTools + [TRANSFER_CALL_TOOL, END_CALL_TOOL]). Spot-checked against the source — TS is already correct; this PR brings Python to parity, not the other way around.

Test plan

  • pytest libraries/python/tests/test_pipeline_builtin_tools.py -v — 6/6 pass
  • Verified end-to-end against OpenAILLM(model="gpt-4o-mini") + DeepgramSTT + ElevenLabsRestTTS on Twilio: caller says "transfer me," LLM emits transfer_call({"number": "+1..."}), helper dispatches to _twilio_transfer, Twilio call-update REST fires, GV number rings, audio bridges.
  • CI green

…MLoop

Pipeline mode (STT → LLM → TTS) was constructing ``LLMLoop`` with only
the user-provided tools (``tools=self.agent.tools``), so the LLM never
saw ``transfer_call`` or ``end_call`` even though they're documented
as built-in across all three modes (Realtime, ConvAI, Pipeline).

The realtime path at ``stream_handler.py:997`` already injects them:

    openai_tools = agent_tools + [TRANSFER_CALL_TOOL, END_CALL_TOOL]

Mirror that here with a new helper ``_augment_with_builtin_handoff_tools``
that builds handler closures wired to the telephony-level
``_transfer_fn`` / ``_hangup_fn`` callbacks already attached to the
``PipelineStreamHandler``. The handler signature ``(arguments,
call_context)`` matches ``ToolExecutor._invoke_handler``'s calling
convention.

Tools are appended (not prepended) so user-provided tool order is
preserved. Built-ins are skipped when the corresponding telephony fn
is unavailable (e.g. test harness without a Twilio adapter).

Includes 6 unit tests covering empty-user-tools / non-empty-user-tools /
missing-fn / handler-dispatch / graceful-empty-args paths.

Closes PatterAI#110.
@nicolotognoni nicolotognoni self-assigned this May 27, 2026
@nicolotognoni nicolotognoni merged commit 8b258fa into PatterAI:main May 27, 2026
9 checks passed
nicolotognoni added a commit that referenced this pull request May 27, 2026
…115) (#118)

- Pipeline mode in `libraries/typescript/src/stream-handler.ts` did not
  inject the built-in `transfer_call` / `end_call` tools into the
  `LLMLoop` (both `new LLMLoop(...)` call sites at lines 1891 and 1906
  passed `agent.tools` through unchanged). The Realtime path injects
  them at `server.ts:374`; pipeline was missing the parity. Added
  `augmentWithBuiltinHandoffTools` helper mirroring the Python helper
  shipped in #115; it builds handler closures that validate E.164 /
  default `reason` and dispatch to the existing telephony bridge
  methods (`bridge.transferCall` / `bridge.endCall`). Built-ins are
  skipped when the corresponding callback is missing, keeping
  non-telephony test harnesses clean. Exported
  `TRANSFER_CALL_TOOL` / `END_CALL_TOOL` from `server.ts` so the
  helper can re-use the canonical schema.
- Docs: `docs/typescript-sdk/events.mdx` advertised the same
  non-existent `phone.events.on(PatterEventType.X, handler)` API as
  the Python events page (closed by #114). Replaced the broken
  `EventBus` section with documentation of the APIs that actually
  exist on the TypeScript `Patter` class: speech-edge attribute
  setters (`onUserSpeechStarted` / `onAgentSpeechEnded` /
  `onLlmToken` / `onAudioOut` etc.) and tool events via
  `onTranscript` with `role === "tool"`.
- CHANGELOG: backfilled `## Unreleased` `### Fixed` entries for
  upstream PRs #113, #114, #115 (Python fixes by @knowsuchagency that
  landed without the corresponding TypeScript / changelog updates),
  plus entries for the two TypeScript parity fixes shipped here.

Test plan:
- Python: covered by tests added in the upstream PRs (#113, #115).
- TypeScript: 8 new unit tests in
  `libraries/typescript/tests/pipeline-builtin-tools.test.ts` cover
  empty-user-tools, non-empty user tools order, missing-callback
  skip, transfer dispatch + E.164 rejection, end_call default and
  user-supplied `reason`. Full suite: 1521 passed / 8 skipped, lint
  clean, build green.
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.

[BUG] Pipeline mode does not inject built-in transfer_call / end_call tools into the LLMLoop

2 participants