Voice AI agent construction

How do production voice agents (e.g. Bland.ai) orchestrate prompts and conversation flow without relying on huge hand-written system prompts?

I’m building a real-time voice agent, and I’m trying to understand how production platforms like Bland.ai, Retell, Vapi, etc. handle prompt orchestration internally.

In my current implementation, I have a base system prompt that is cached, and I perform state-wise prompt injection depending on where the user is in the conversation (e.g., booking, checking availability, Q&A, BANT qualification, etc.).

The problem I’m facing is that I still end up writing a lot of explicit instructions for edge cases. For example:

  • If the user asks a question while I’m collecting their email, answer the question and then continue collecting the email.
  • If the user changes the topic, handle it and return to the previous task.
  • If the user provides partial information, ask only for the missing fields.
  • If the user interrupts, resume the previous workflow naturally.

As more edge cases appear, the system prompt keeps growing. Even after carefully covering many scenarios, real users still behave in unexpected ways. Sometimes the model forgets a step, skips required information, or drifts away from the intended workflow.

On the other hand, platforms like Bland.ai expose a simple dashboard where you specify things like:

  • Agent name
  • Voice/Gender
  • Purpose
  • Guardrails
  • Business instructions

…and the agent seems to work surprisingly well without the developer manually encoding hundreds of conversational edge cases.

This makes me wonder:

Questions

  1. Are these platforms actually generating a much larger prompt behind the scenes?
  2. Are they using hierarchical prompts or prompt compilation?
  3. Is there an orchestration engine that dynamically injects instructions based on the conversation state?
  4. Are finite-state machines (FSMs), behavior trees, workflow graphs, or planners commonly used?
  5. How do they prevent the LLM from drifting while still allowing natural conversation?
  6. How do they recover after interruptions or topic changes without stuffing every possible scenario into the system prompt?
  7. Do they rely heavily on tool calling and external orchestration instead of prompt engineering?
  8. How are prompts compiled or transformed from the dashboard configuration into something that reliably handles real-world conversations?

One thing I’ve noticed is that if I simplify my prompt to something like:

“Answer the user’s questions and continue with the booking.”

the model often becomes too conversational and eventually stops driving the booking process. So there seems to be a balance between giving the model autonomy and enforcing deterministic business logic.

My Current Architecture

  • Backend: FastAPI
  • STT: Sarvam Speech-to-Text
  • LLM: Streaming
  • TTS: Sarvam Text-to-Speech
  • Turn Detection: Pipecat SmartTurn V3
  • Voice Activity Detection: Silero VAD

Streaming Pipeline

User Speech
     │
     ▼
Sarvam STT
     │
     ▼
LLM
     │
     ▼
Sarvam TTS
     │
     ▼
User

What I’m Trying to Achieve

My goal is to build an orchestration layer that:

  • Maintains deterministic workflows while still allowing natural conversations.
  • Can gracefully handle interruptions, off-topic questions, and unexpected user behavior.
  • Doesn’t require an ever-growing monolithic system prompt.
  • Achieves sub-second latency suitable for production voice agents.

I’m less interested in vendor-specific implementations and more interested in the architectural patterns used in production systems.

For people who have built production-grade conversational or voice AI systems:

  • How do you structure the orchestration layer?
  • How much of the logic lives in prompts versus external code?
  • What patterns have worked well for keeping the conversation on track while remaining flexible?
  • If you were building a voice agent from scratch today, how would you design this layer?

Hey @John6666 can you throw some light in this, I have read from countless forums and docs and still they do not seem to give a concrete explanation, the issue is with the orchestration layer and not the components

Anyone with a valid Idea or suggestion can feel free to reply and share their thoughts on the matter! feel free to reach out if you are someone who is building Voice AI agents and need some help or would like to brainstorm!

Hi. From what I can tell after looking into this, quite a few different problems seem to be getting tangled together, so I think stepping back and reconsidering the overall architecture—rather than continuing to patch individual edge cases—is probably the right move:


Short answer

I do not think the practical choice is “one huge prompt” versus “a rigid FSM.”

The publicly documented systems I could find point more toward a hybrid:

  1. An LLM interprets natural language and proposes typed actions.
  2. An external controller owns the authoritative dialogue/workflow state.
  3. Deterministic code validates transitions and business preconditions.
  4. Tools perform lookups or side effects under explicit lifecycle rules.
  5. The LLM verbalizes the resulting state naturally.

A compact way to state it is:

Let the model interpret and propose; let the runtime validate and commit.

There may still be generated prompts, node-specific prompts, summaries, retrieved instructions, or dynamically selected tools behind vendor dashboards. However, the public documentation for systems such as Pipecat Flows, Bland Conversational Pathways, Retell Conversation Flow, and Rasa CALM does not look like “a single secret super-prompt” is the whole mechanism.

The safest default architecture for your case would probably look roughly like this:

Audio / STT events
        |
        v
Turn finalization
        |
        v
LLM or SLM
  proposes typed command(s)
        |
        v
Deterministic controller
  - validates commands
  - owns workflow state
  - manages detours and return targets
  - decides which tool operations are legal
        |
        v
Tool / operation layer
  - lookup
  - confirmation
  - retry
  - stale-result rejection
  - idempotent commit
        |
        v
Response generation
        |
        v
TTS / spoken-output tracking

This does not require throwing away your existing Sarvam STT → streaming LLM → Sarvam TTS pipeline. It means moving responsibility for workflow correctness out of the prompt and into an explicit runtime contract.

The first architectural split I would make

I would keep the main business flow separate from reusable conversation-repair flows.

For example:

Main booking flow Reusable repair/navigation behavior
Collect date Answer a temporary question
Collect time Correct a previously supplied value
Collect party size Clarify an ambiguous request
Check availability Repeat something
Present options Cancel or restart
Confirm selection Temporarily switch tasks
Commit booking Recover from a tool/system error

That prevents every booking node from acquiring its own versions of:

  • “If the user asks a question…”
  • “If the user changes their mind…”
  • “If the user corrects a previous answer…”
  • “If the user interrupts…”
  • “If the API fails…”

Rasa’s conversation-pattern model is one public example of this separation. It treats correction, clarification, interruption, cancellation, restart, handoff, repetition, silence, and system errors as reusable patterns rather than duplicating them inside every business flow.

Bland exposes a similar idea through Global Nodes: a restaurant-hours question can temporarily interrupt reservation collection, be answered, and then automatically return to the previous reservation node.

The important part is not either product. It is the reusable pattern:

main flow
    |
    +-- temporary repair / Q&A / clarification
            |
            +-- explicit outcome:
                  - resume the same step
                  - resume an earlier step
                  - start a different flow
                  - cancel the original flow

A practical default route

A low-risk migration path would be:

  1. Keep the current audio pipeline.
  2. Pick one representative flow, such as booking.
  3. Represent its state as typed data outside the LLM context.
  4. Ask the LLM for typed commands rather than asking it to “manage the whole conversation.”
  5. Validate and apply those commands in normal code.
  6. Expose only the tools legal in the current state.
  7. Generate the user-facing reply from the resulting state.
  8. Add stronger operation semantics only where there is an actual side effect.

For example, instead of expecting the model to remember all booking rules from prose, it might propose:

{
  "commands": [
    {
      "type": "set_slot",
      "slot": "party_size",
      "value": 5
    },
    {
      "type": "start_temporary_flow",
      "flow": "restaurant_question",
      "arguments": {
        "question": "What time do you close?"
      }
    }
  ]
}

The controller would then decide:

  • whether party_size is currently editable;
  • whether changing it invalidates an earlier availability result;
  • whether the restaurant question can be answered locally or requires a tool;
  • where to return after the question;
  • whether another LLM response should be generated immediately.

This preserves the model’s ability to understand “There will be five of us, but before that, when do you close?” without giving it ownership of the booking transaction.

One important Pipecat branch

Your post says you use Pipecat SmartTurn V3, but it is not clear whether the rest of the pipeline is also built with Pipecat. The recommendation changes slightly, so I would keep both branches open:

  • If the complete cascaded pipeline uses Pipecat: Pipecat Flows is a concrete implementation candidate.
  • If Pipecat is only being used for SmartTurn: the same controller can live in your FastAPI application; Pipecat Flows can still be useful as a design reference.
  • If the architecture is mixed: keep the business controller framework-neutral and adapt audio/pipeline events at the boundary.

SmartTurn belongs to the turn-detection side of the system. Its model card does not make it the owner of business state, slot dependencies, transaction commits, or the semantic return target after a detour.

Direct answers to the questions in the post

Are vendors generating a much larger prompt?

Possibly in some places, but that cannot be established from their public interfaces, and a larger prompt alone would not explain the documented workflow behavior.

A dashboard can reasonably compile configuration into some combination of:

  • role or policy instructions;
  • node-local task messages;
  • retrieved business information;
  • available tool schemas;
  • state projected into the current context;
  • transition conditions;
  • repair/global flows;
  • runtime hooks or webhooks;
  • response templates.

So “prompt compilation” may be one piece without being the orchestration layer itself.

Are hierarchical prompts or dynamic injection used?

There are clear public examples of node-specific instructions and dynamically changing tools/context.

Pipecat Flows defines conversations as graphs where each node focuses the model on one task and only the tools needed for that task. It transitions by changing the LLM context and tools during the session.

That is dynamic injection, but it is being driven by an explicit flow runtime rather than by the LLM independently deciding what the authoritative state is.

Are FSMs, graphs, planners, or behavior trees used?

Workflow graphs and deterministic state machines are publicly documented. Planners and multi-agent routing also exist, but I would not make them the default for a booking workflow.

A useful complexity ladder is:

Situation Smallest likely useful mechanism
Mostly linear flow Typed state plus deterministic validator
Temporary detours Return stack
Nested flows Dialogue stack or hierarchical state machine
Many conditional substates Workflow graph or statechart
Complex corrections and references Revision-aware or dataflow-style state
Long-running side effects Durable operation state
Truly simultaneous independent goals Scheduler or parallel states
Distinct specialists with independent responsibilities Multi-agent routing

A flat FSM becomes awkward when every combination of slot values, corrections, detours, and tool states becomes a separate state. That does not mean abandoning state machines; it often means using hierarchy, a stack, orthogonal state variables, or a graph instead of enumerating the Cartesian product.

How is drift prevented while retaining natural conversation?

Usually by making the model flexible about interpretation and wording, but constrained about effects.

The model can remain free to:

  • infer that “five of us” means party_size = 5;
  • recognize a temporary question;
  • extract several slots from one utterance;
  • paraphrase confirmations naturally;
  • decide that clarification is needed.

The controller remains responsible for:

  • required fields;
  • legal transitions;
  • which tools are currently available;
  • whether confirmation is required;
  • whether a result is stale;
  • whether a write may be committed;
  • where a temporary flow returns.

Rasa’s LLM command generators are a useful public example. A user turn can produce a sequence such as SetSlot(...) plus StartFlow(...), while the deterministic Flow Policy owns flow execution and state transitions.

How are interruptions and topic changes recovered?

I would not model all of them as one generic “resume” operation.

They are different events:

Event Likely handling
User talks over the bot Stop or attenuate output; decide what generation/tool work is cancelled
Temporary business question Push a temporary flow and return afterward
Correction Revise a value and invalidate dependent results
New task Start or switch flow
Cancellation Explicitly cancel the current flow
Clarification Pause progression until ambiguity is resolved
Partial information Apply supplied slots and compute what remains missing
User continues an incomplete utterance Merge/replace transcript according to turn semantics

The return behavior should be an explicit controller decision, not only a sentence in the prompt.

Is tool calling central?

For external facts and actions, yes, but tool calling by itself is not an orchestration architecture.

A tool schema tells the model how to request an operation. It does not necessarily guarantee:

  • that the operation is legal in the current business state;
  • that the arguments are still current;
  • that the user confirmed it;
  • that an old asynchronous result should still be accepted;
  • that the operation will not be performed twice;
  • that a cancelled generation cannot still trigger the effect.

The controller and backend still need to enforce those properties.

What probably sits behind the vendor dashboards?

From the public documentation, a reasonable description is:

Configuration is projected into local prompts, graph nodes, tool definitions, runtime conditions, and state transitions.

That is different from claiming knowledge of any vendor’s private internal implementation.

Why these edge cases become coupled

There appear to be at least four separate timelines in a real-time voice agent.

Timeline Typical state Commit boundary
Audio/perception VAD, interim STT, final STT, turn completion, interruption What user input is accepted as a turn
LLM/context messages, active instructions, tool calls, tool results What becomes part of model context
Dialogue/workflow active flow, step, slots, return target, allowed actions What the controller accepts as current dialogue state
Business transaction lookup, tentative selection, confirmation, booking write What changes an external system

A fifth useful timeline is output delivery:

Output stage Meaning
Generated The model produced the text
Queued The text/audio is waiting to be sent
Partially spoken The user heard only part of it
Fully spoken The user heard the whole response
Context-committed The system recorded it as assistant history

These stages can disagree during barge-in, cancellation, network delay, or TTS queueing.

That is why “the user interrupted, so resume naturally” is underspecified. It could mean:

  • resume the audio sentence;
  • restart generation;
  • return to the previous workflow step;
  • continue an asynchronous lookup;
  • preserve a partially delivered confirmation;
  • cancel a pending write.

Those should not all share one implicit state flag.

Pipecat’s context-management documentation explicitly distinguishes LLM-generated frames from TTS text frames, with the latter intended to represent what was actually spoken. That is an important architectural distinction even if the exact delivery/interrupt behavior still needs testing in a specific pipeline.

A minimal controller contract

The controller does not have to be a large framework. It can begin as a small typed service in the FastAPI application.

A minimal state might contain:

session_id
active_flow
current_step
return_stack

slots
slot_revisions
state_revision

pending_confirmation
allowed_commands

pending_tool_operations
committed_operations
cancelled_operations

active_generation_id
next_response_owner
last_spoken_output

Possible internal commands might include:

SetSlot
ClearSlot
StartFlow
StartTemporaryFlow
ResumeFlow
CancelFlow
AnswerKnowledgeQuestion
RequestClarification
RequestConfirmation
InvokeLookup
CommitOperation
Handoff

The important distinction is:

LLM output:
    "Here are the commands I believe match the user's utterance."

Controller:
    "Here are the commands that are legal and will actually be applied."

A simplified application rule could look like:

def apply_command(command, state):
    if command.based_on_revision != state.revision:
        return reject("stale command")

    if command.type not in state.allowed_commands:
        return reject("command not allowed in current state")

    if not business_preconditions_hold(command, state):
        return reject("business precondition failed")

    new_state = transition(state, command)
    new_state = invalidate_dependent_data(state, new_state, command)
    new_state.revision += 1

    return accept(new_state)

The LLM does not need to see every internal field. The controller can project a smaller view into the prompt:

Current task: restaurant booking
Current step: collect contact details
Known values:
- date: 2026-07-20
- time: 19:00
- party size: 5
Missing required values:
- email
Allowed next actions:
- set_email
- answer_restaurant_question
- cancel_booking
- request_human

That is more compact and more reliable than asking the model to infer the same state from a long transcript plus many exception rules.

State revision and dependency invalidation

Suppose availability was checked for:

date = July 20
time = 19:00
party_size = 5

Then the user says:

Actually, make that eight people.

The correct update is not only:

party_size = 8

It may also require:

availability_result = invalid
selected_option = invalid
pending_confirmation = invalid

The controller should know which derived data depends on which inputs.

A useful operation record might be:

{
  "operation_id": "availability-123",
  "type": "check_availability",
  "based_on_state_revision": 17,
  "arguments": {
    "date": "2026-07-20",
    "time": "19:00",
    "party_size": 5
  },
  "status": "running"
}

If the result returns when the current state revision is 18 and the party size is now 8, the controller can mark it stale rather than allowing it to overwrite current state.

Read operations versus writes

Not every tool needs heavy transaction machinery.

A useful distinction is:

Read-only lookup
- request ID
- based-on revision
- stale-result check
- timeout/cancellation policy

Important write
- operation ID
- current-state validation
- explicit confirmation where appropriate
- backend-side authorization/validation
- idempotency key
- committed/failed/unknown status
- audit record

Booking creation should not rely only on the LLM deciding that a tool call “looks right.”

The general idempotency principle is described well in Temporal’s Activity documentation: retryable operations should be designed so repeated execution does not accidentally repeat the side effect. You do not need Temporal to apply that principle.

Mapping the examples from the post to reusable patterns

“If the user asks a question while I am collecting their email…”

This can be represented as a temporary detour:

collect_email
    |
    +-- push restaurant_question
            |
            +-- answer question
            |
            +-- pop and resume collect_email

The return target should be explicit. It can be a stack entry rather than a prompt instruction such as “remember to continue.”

“If the user changes the topic…”

“Topic change” can mean several different things:

Temporary question
    -> push temporary flow, answer, return

New independent request
    -> start another flow

Replacement request
    -> cancel or suspend current flow, start new flow

Correction to current request
    -> revise slot, invalidate dependent results

Ambiguous switch
    -> ask a short clarification

Treating all of those as one intent called topic_change would likely move the ambiguity rather than solve it.

“If the user provides partial information…”

The model can emit several commands from one turn:

User:
"Tomorrow at seven, for five people."

Commands:
SetSlot(date, tomorrow)
SetSlot(time, 19:00)
SetSlot(party_size, 5)

The controller then computes missing required fields.

It does not need another prompt rule for every combination of supplied and missing fields.

“If the user interrupts…”

It helps to split acoustic interruption from semantic interruption.

Acoustic interruption:
- stop or duck TTS
- decide whether generation remains valid
- track what was actually delivered
- accept the new user turn

Semantic interruption:
- temporary detour
- correction
- cancellation
- task switch
- clarification

A caller speaking over “Your reservation is confirmed…” is not equivalent to a caller asking about parking while the system is collecting an email.

Corrections inside detours

A stack is particularly useful when detours can be nested:

booking flow
    -> restaurant question
        -> clarification
            -> return to restaurant question
        -> return to booking flow

Rasa’s Flow Policy publicly documents a last-in-first-out dialogue stack, where newly started flows can complete and then return control to the previous flow.

For more complex corrections and references, the SMCalFlow paper is useful background. It represents dialogue state as a dataflow graph and includes explicit operators for reference and revision. That is probably more machinery than a basic booking bot initially needs, but it provides vocabulary for cases where users revise or refer back to earlier values.

How much machinery is actually necessary?

I would start with the smallest mechanism that expresses the required invariants.

Typed state plus validator

Use this when:

  • the flow is mostly linear;
  • detours are shallow;
  • there are few concurrent operations;
  • correction rules are simple.

This is probably the default starting point.

Return stack

Add this when:

  • temporary Q&A is common;
  • clarification can interrupt collection;
  • nested flows must return to their caller.

Hierarchical statechart or workflow graph

Add this when:

  • a flat state list is becoming a Cartesian product;
  • several substates share the same transitions;
  • history or nested states are important;
  • there are distinct reusable subflows.

Revision-aware/dataflow state

Consider this when:

  • users frequently refer to earlier values;
  • corrections need to propagate through derived values;
  • multiple candidate plans coexist;
  • reconstructing the intended state from a flat slot dictionary becomes difficult.

Durable workflow or operation ledger

Consider this when:

  • operations last longer than a conversational turn;
  • retries and process restarts matter;
  • writes affect money, reservations, healthcare, or other important systems;
  • “unknown whether committed” is a meaningful state.

Planner or multi-agent architecture

I would reserve this for cases where there are genuinely independent goals or specialists.

For a normal booking flow, adding a router LLM, planner LLM, worker LLM, reviewer LLM, and response LLM could increase latency and introduce more ownership ambiguity without solving the underlying state problem.

A local deterministic controller plus one model pass capable of emitting multiple commands is a simpler default.

Pipecat-specific route and current caveats

As of July 13, 2026, the latest Pipecat release is v1.5.0, and Flows is included in Pipecat.

If your full pipeline is Pipecat-based

Pipecat Flows is close to the orchestration layer you are describing:

  • conversations are graphs of nodes;
  • each node focuses the LLM on one task;
  • each node exposes only relevant tools;
  • context and tools can change during the session;
  • flow logic is separated from pipeline mechanics.

The Flows examples are worth inspecting for reservation, intake, handoff, and related patterns.

Cross-node values can be stored in flow_manager.state.

However, I would treat that dictionary as application state storage, not automatically as:

  • a revision system;
  • an idempotency system;
  • a transaction log;
  • a dependency graph;
  • an audit trail.

Those semantics still need to be designed where required.

Context strategy still matters

Flows currently offers the following context strategies:

  • APPEND, which is the default;
  • RESET;
  • RESET_WITH_SUMMARY, which is deprecated in favor of Pipecat’s native LLMSummarizeContextFrame.

A graph does not automatically prevent prompt growth if every node appends more instructions to the same context.

The context policy should therefore be explicit:

  • preserve history when it remains relevant;
  • reset when old node instructions could conflict;
  • summarize when long-term facts matter but exact wording does not;
  • keep authoritative business state outside the summary.

Function lifecycle still matters

Pipecat’s function-calling documentation exposes several independent controls:

  • whether a tool is cancelled on interruption;
  • synchronous versus asynchronous function calls;
  • model-directed cancellation of asynchronous calls;
  • whether parallel calls are grouped;
  • whether an LLM run occurs after a result;
  • intermediate versus final results.

These settings illustrate why “the graph moved to another node” and “the old operation no longer has effects” are not necessarily the same event.

Generated versus spoken output

Pipecat’s context-management documentation describes assistant context being updated from TTSTextFrame, representing the text passed through TTS, rather than treating every generated token as necessarily spoken.

That is the right distinction to preserve, especially during interruption.

Recent open reports are useful boundary tests

These reports do not establish that Pipecat is causing the behavior in your system. They are useful examples of lifecycle boundaries a production design should make explicit.

At the time of writing:

  • Issue #4997 reports a tool call from an interrupted/discarded generation still executing.
  • Issue #4936 reports a timed-out function handler continuing to run, with potential duplicate-side-effect risk.
  • Issue #4979 reports duplicate responses after a multi-worker handoff; PR #4995 is an open proposed fix based on an explicit “no response” outcome.
  • Issue #4996 reports already-played TTS text being lost from assistant context during interruption; PR #5008 is an open proposed fix.

The general invariant I would take from these examples is:

At any time, explicitly identify the owner of:
- the active generation;
- the next audible response;
- each tool operation;
- each workflow transition;
- each irreversible side effect.

If you only use SmartTurn

There is no need to migrate the whole system to Pipecat merely to get this architecture.

The FastAPI service can own:

ConversationState
CommandValidator
FlowStack
OperationRegistry
ResponseCoordinator

SmartTurn, Silero, Sarvam STT, and Sarvam TTS can remain adapters around that controller.

What the public vendor designs suggest—and what they do not prove

Bland

Bland Conversational Pathways publicly exposes:

  • nodes;
  • conditions;
  • pathway labels;
  • global nodes;
  • webhook/API interactions;
  • automatic return from a global node to the previous node.

Its reservation example specifically describes answering a restaurant-hours question during reservation collection and then returning to the original reservation step.

That supports the general graph-plus-local-prompt pattern. It does not reveal the complete private runtime, model prompts, training data, evaluation stack, or fallback logic.

Retell

Retell Function Nodes separate several choices:

  • whether to wait for the function result;
  • whether to speak while it executes;
  • whether interruptions are blocked;
  • when the transition occurs;
  • whether transition conditions depend on the result.

That is another example of lifecycle decisions being explicit runtime configuration rather than one general prompt instruction.

Rasa CALM

Rasa publicly separates:

The exact Rasa framework may or may not suit your latency, licensing, or deployment needs, but the component boundaries are useful to compare against a custom implementation.

Research background

Task-Oriented Dialogue as Dataflow Synthesis is useful if correction, reference, and revision become more complex than a normal slot-filling flow.

I would use it as a design reference rather than jumping directly to implementing the full formalism.

Optional controls for deciding whether the architecture is improving

These do not need to become a large test project immediately. They can be treated as executable invariants.

Invariant What it protects
No booking write before confirmation Premature side effects
Same operation ID cannot commit twice Retry/duplicate protection
A slot correction invalidates dependent results Stale availability or pricing
Temporary Q&A returns to the same missing field Detour recovery
Cancelled flow cannot later commit a tool result Zombie operations
Only one component owns the next response Duplicate speech
Context reflects delivered output, or records uncertainty Repetition after barge-in
A stale async result cannot overwrite current state Race conditions
Required fields cannot be skipped Workflow drift

There are three useful validation layers.

Controller-level

Feed text commands or user turns into the controller without STT/TTS.

This isolates:

  • slot updates;
  • transition rules;
  • return-stack behavior;
  • stale-result rejection;
  • operation idempotency.

Pipeline-level text mode

Run the real LLM and tool pipeline while bypassing audio.

This isolates:

  • prompt/context behavior;
  • command generation;
  • function selection;
  • multi-turn state;
  • response ownership.

Audio-level

Add:

  • STT;
  • VAD;
  • SmartTurn;
  • TTS;
  • barge-in;
  • partial delivery;
  • telephony/network behavior.

Pipecat’s built-in Evals framework supports text and audio modes, multi-turn assertions, function-call checks, interruptions, and timing budgets. It can be used directly if the complete agent runs in Pipecat, or simply used as a reference for the kinds of events worth asserting in a custom harness.

The point is not to enumerate every possible utterance. It is to preserve architectural properties across varied utterances.

Bottom line

I would not begin by trying to discover the exact hidden prompt used by Bland, Retell, or Vapi, because their public documentation already suggests that prompt text is only one layer.

I would also not begin with a large planner or multi-agent system.

The default I would choose is:

Natural-language interpretation
        +
typed commands
        +
deterministic workflow state
        +
explicit repair/return behavior
        +
validated and idempotent side effects
        +
separate audio/output lifecycle

If your full pipeline already uses Pipecat, implementing one representative booking flow with Pipecat Flows is a reasonable route.

If Pipecat is only providing SmartTurn, the same pattern can be implemented in FastAPI without changing the rest of your stack.

Either way, the main shift is from asking the prompt to remember how the entire business process works to letting the model interpret the user while a runtime explicitly owns what has been accepted, what remains pending, what may execute, and where the conversation returns next.