Bottom Linear Gradient  Lines image

Learn

Resources

Article

9 min read

read

Merging the Power of the Human Developer with the Power of AI

Why we built the SignalWire Agents SDK, and how it puts you in command of AI instead of in negotiation with it.

Anthony Minessale

Anthony Minessale

CEO

dev power and AI

In this article

Share

Angular Gradient Image

Build it free.

Create a space and ship your first call flow in minutes.

Subscribe

Voice AI crossed a threshold. A computer can now hold a natural phone conversation: it listens, understands, speaks, and interrupts gracefully. Businesses want it everywhere: scheduling, support, ordering, collections, triage.

But somewhere in the excitement, the industry sold developers a bad story about their own role. The story goes like this: the model is the brain now. Your job is to write it a long, careful letter called a prompt, hand it some tools, and hope it behaves. If it misbehaves, write a longer letter.

We built the SignalWire Agents SDK because that story is wrong. The model is not the brain of a production system. You are. The model is the most talented conversationalist ever hired, and like many talented performers, it improvises. Someone has to run the show: decide what happens, when it happens, and what is never allowed to happen. That someone is a developer.

This article explains why we built the SDK and what it does. More importantly, it shows how the SDK puts you in command of AI instead of in negotiation with it. It assumes you have never touched SignalWire before. Every term gets defined on first use.

The bargain you were offered

The dominant way to build an AI phone agent today works like this. You write one large prompt describing everything: the personality, the rules, the workflow, the exceptions. You attach a flat list of tools, all available all the time. Then you put it on the phone and hope.

We call this approach prompt and pray, and the name is not a joke. The prompt is a request. The model reads your two thousand words on every conversational turn and decides, probabilistically, which instructions apply right now. Usually it decides well. Sometimes it does not.

Our founding story on this point involves a pizza ordering bot. One day it started telling callers that menu items were sold out. Nothing was sold out. Nobody told it anything was sold out. The model inferred it, confidently, from nothing. That is not a bug in one model. It is the nature of probabilistic systems: they approximate. A system that is right 99 percent of the time is wrong at scale, because production means thousands of calls a day.

And the failure modes compound. With every tool always available, the refund tool exists during the greeting. The only thing preventing a confused model from using it is prose. Tool accuracy itself degrades when too many tools are active at once: past roughly seven or eight, models start missing the right tool or calling a near neighbor. State lives nowhere except the transcript, so the agent re-asks questions and skips phases. And none of it can be unit tested, because you cannot write a test for a paragraph.

The deepest problem is authority. In a prompt-and-pray system, whatever the model says becomes what your business did. If it invents a discount, a caller was offered a discount.

Your skills were never the problem

Here is the part of the story the industry got backwards. The arrival of AI did not make your engineering skills obsolete. It made them scarce exactly where they are needed most.

State machines. Input validation. Access control. Separation of concerns. Testing. Auditability. These are the disciplines that made software reliable for decades, and they are precisely what AI applications are missing. The model brings language: the astonishing ability to understand loosely phrased humans and respond naturally. It does not bring correctness. Correctness has always been your department.

Prompting is negotiation. Programming is the answer. The developers who ship voice AI that survives production are not the best prompt poets. They are the ones who put real software around the model: software that decides what the model can see, what it can do, and what happens next. The model makes the conversation natural. Your code makes it correct. Those are independent properties, and the whole trick is keeping them independent.

The SDK exists to make that arrangement the default, with the platform enforcing what you decide.

What we built, and why

First, one paragraph of context. SignalWire is a communications infrastructure company built by the team behind FreeSWITCH. That open-source engine has powered a large share of the world’s voice systems for two decades. The platform runs voice, video, messaging, and AI conversation for more than 2,700 companies, moving billions of minutes and messages a year. AI conversation is one capability of that platform, and it runs inside it, in the media path, not bolted on through a chain of third-party services.

A phone call on SignalWire is controlled by a document called SignalWire Markup Language (SWML): a JSON description of what should happen. Answer the call. Play a sound. Start an AI conversation with this prompt, these tools, this voice. When the AI needs to call a tool, the platform uses the SignalWire AI Gateway (SWAIG). SWAIG is the same concept as function calling in any LLM API. The tool’s name, description, and parameters are shown to the model. When the model calls the tool, the platform delivers the request to your code as a webhook.

You could write all of that by hand. People did. It meant hand-crafting nested JSON against a large schema, building a separate webhook server, and inventing an authentication scheme. It meant keeping tool schemas in sync with handlers by hand, then repeating the whole exercise for every agent.

The Agents SDK collapses all of it into one Python class. Your agent is a small web service that does two jobs at once: it generates the SWML document when the platform asks for it, and it hosts the tool webhooks that document points to. The webhook URLs point back at the agent itself, credentials included, discovered automatically even behind proxies and load balancers. Per-call security tokens are minted and validated for you. One process, correct by construction, deployable anywhere Python runs.

pip install signalwire-sdk
pip install signalwire-sdk
pip install signalwire-sdk
pip install signalwire-sdk

A complete agent in a page of Python

from signalwire import AgentBase
from signalwire.core.function_result import FunctionResult

class SupportAgent(AgentBase):
    def __init__(self):
        super().__init__(name="support", route="/support")
        self.prompt_add_section("Role",
            body="You are the support agent for Brightline Bikes.")
        self.prompt_add_section("Rules", bullets=[
            "Look up the order before quoting any status.",
            "Never invent shipping dates.",
        ])
        self.add_skill("datetime")

    @AgentBase.tool(name="lookup_order",
        description=("Look up an order by its number. Use this BEFORE "
                     "answering any question about an order's status."),
        parameters={"type": "object",
                    "properties": {"order_id": {"type": "string",
                        "description": "8-digit order number, no dashes."}},
                    "required": ["order_id"]})
    def lookup_order(self, args, raw_data):
        order = orders.get(args["order_id"])
        if not order:
            return FunctionResult("No order found. Ask them to re-check the number.")
        return FunctionResult(f"Order {order.id} is {order.status}, arriving {order.eta}.")

SupportAgent().run()
from signalwire import AgentBase
from signalwire.core.function_result import FunctionResult

class SupportAgent(AgentBase):
    def __init__(self):
        super().__init__(name="support", route="/support")
        self.prompt_add_section("Role",
            body="You are the support agent for Brightline Bikes.")
        self.prompt_add_section("Rules", bullets=[
            "Look up the order before quoting any status.",
            "Never invent shipping dates.",
        ])
        self.add_skill("datetime")

    @AgentBase.tool(name="lookup_order",
        description=("Look up an order by its number. Use this BEFORE "
                     "answering any question about an order's status."),
        parameters={"type": "object",
                    "properties": {"order_id": {"type": "string",
                        "description": "8-digit order number, no dashes."}},
                    "required": ["order_id"]})
    def lookup_order(self, args, raw_data):
        order = orders.get(args["order_id"])
        if not order:
            return FunctionResult("No order found. Ask them to re-check the number.")
        return FunctionResult(f"Order {order.id} is {order.status}, arriving {order.eta}.")

SupportAgent().run()
from signalwire import AgentBase
from signalwire.core.function_result import FunctionResult

class SupportAgent(AgentBase):
    def __init__(self):
        super().__init__(name="support", route="/support")
        self.prompt_add_section("Role",
            body="You are the support agent for Brightline Bikes.")
        self.prompt_add_section("Rules", bullets=[
            "Look up the order before quoting any status.",
            "Never invent shipping dates.",
        ])
        self.add_skill("datetime")

    @AgentBase.tool(name="lookup_order",
        description=("Look up an order by its number. Use this BEFORE "
                     "answering any question about an order's status."),
        parameters={"type": "object",
                    "properties": {"order_id": {"type": "string",
                        "description": "8-digit order number, no dashes."}},
                    "required": ["order_id"]})
    def lookup_order(self, args, raw_data):
        order = orders.get(args["order_id"])
        if not order:
            return FunctionResult("No order found. Ask them to re-check the number.")
        return FunctionResult(f"Order {order.id} is {order.status}, arriving {order.eta}.")

SupportAgent().run()
from signalwire import AgentBase
from signalwire.core.function_result import FunctionResult

class SupportAgent(AgentBase):
    def __init__(self):
        super().__init__(name="support", route="/support")
        self.prompt_add_section("Role",
            body="You are the support agent for Brightline Bikes.")
        self.prompt_add_section("Rules", bullets=[
            "Look up the order before quoting any status.",
            "Never invent shipping dates.",
        ])
        self.add_skill("datetime")

    @AgentBase.tool(name="lookup_order",
        description=("Look up an order by its number. Use this BEFORE "
                     "answering any question about an order's status."),
        parameters={"type": "object",
                    "properties": {"order_id": {"type": "string",
                        "description": "8-digit order number, no dashes."}},
                    "required": ["order_id"]})
    def lookup_order(self, args, raw_data):
        order = orders.get(args["order_id"])
        if not order:
            return FunctionResult("No order found. Ask them to re-check the number.")
        return FunctionResult(f"Order {order.id} is {order.status}, arriving {order.eta}.")

SupportAgent().run()

That is a complete production service: HTTP server, generated SWML, authenticated tool routing, and response formatting. The same run() call detects its environment. This one file deploys unchanged as a standalone server, an AWS Lambda function, a Google Cloud Function, an Azure Function, or a CGI script.

And you can interrogate it without making a single phone call:

swaig-test support.py --list-tools
swaig-test support.py --exec lookup_order --order_id 12345678
swaig-test support.py --dump-swml
swaig-test support.py --list-tools
swaig-test support.py --exec lookup_order --order_id 12345678
swaig-test support.py --dump-swml
swaig-test support.py --list-tools
swaig-test support.py --exec lookup_order --order_id 12345678
swaig-test support.py --dump-swml
swaig-test support.py --list-tools
swaig-test support.py --exec lookup_order --order_id 12345678
swaig-test support.py --dump-swml

swaig-test is the SDK’s command-line harness. It runs your actual handlers with realistic platform data and prints the exact document a call would receive. Your agent gets a development loop, like the software it is.

Your code governs the conversation

Now the heart of it. The SDK is pleasant plumbing, but plumbing is not why it exists. It exists to enforce a discipline we call Programmatically Governed Inference, or PGI. In plain terms: the model talks, your code decides. The model is a controlled participant inside a deterministic system that you wrote, and it never receives authority in the first place.

PGI works through four locks. Only the first depends on the model’s cooperation. The other three are mechanical.

Lock 1: the prompt. Instructions describing role and behavior. This is the only lock most frameworks have, and it is the weakest, because it depends on probabilistic compliance. In the SDK it is guidance and personality, never enforcement.

Lock 2: tools that do not exist cannot be called. Your agent can define workflows as a sequence of steps, and each step declares exactly which tools the model can see. During the greeting step, the refund tool is not forbidden. It is absent. It does not appear in the schema the model receives, so there is nothing to call, nothing to jailbreak, nothing to reason about. This is the difference between telling someone not to open a door and removing the door from the building.

Lock 3: the conversation is a state machine. Each step also declares which steps may come next. The model’s only way to move the conversation is a navigation tool whose options are rebuilt every turn from your list. Give it an empty list and the model cannot navigate at all. Meanwhile your code is never bound by those lists: a tool handler can move the conversation anywhere, any time. The whitelist constrains the model. It never constrains you.

Lock 4: tool calls are requests, not commands. When the model calls a tool, your handler runs. It reads the real state, applies the real business rules, and returns two things: a prompt for the model, the answer it sought or new instructions, injected into its context, and actions for the platform to execute. The model does not update state and does not decide what happens next.

Here is lock 4 in a blackjack game, the example we use because the stakes are vivid:

def handle_hit(self, args, raw_data):
    game = raw_data["global_data"]["game_state"]
    card = game["deck"].pop()
    game["player_hand"].append(card)
    score = calculate_hand(game["player_hand"])

    result = FunctionResult(f"You drew {format_card(card)}. Your total is {score}.")
    result.update_global_data({"game_state": game})
    if score > 21:
        result.swml_change_step("you_lost")
    return result
def handle_hit(self, args, raw_data):
    game = raw_data["global_data"]["game_state"]
    card = game["deck"].pop()
    game["player_hand"].append(card)
    score = calculate_hand(game["player_hand"])

    result = FunctionResult(f"You drew {format_card(card)}. Your total is {score}.")
    result.update_global_data({"game_state": game})
    if score > 21:
        result.swml_change_step("you_lost")
    return result
def handle_hit(self, args, raw_data):
    game = raw_data["global_data"]["game_state"]
    card = game["deck"].pop()
    game["player_hand"].append(card)
    score = calculate_hand(game["player_hand"])

    result = FunctionResult(f"You drew {format_card(card)}. Your total is {score}.")
    result.update_global_data({"game_state": game})
    if score > 21:
        result.swml_change_step("you_lost")
    return result
def handle_hit(self, args, raw_data):
    game = raw_data["global_data"]["game_state"]
    card = game["deck"].pop()
    game["player_hand"].append(card)
    score = calculate_hand(game["player_hand"])

    result = FunctionResult(f"You drew {format_card(card)}. Your total is {score}.")
    result.update_global_data({"game_state": game})
    if score > 21:
        result.swml_change_step("you_lost")
    return result

The model asked to hit. Your code drew the card, computed the score, and, on a bust, moved the conversation to a step named you_lost. That step has zero tools and zero allowed transitions. The caller can beg or negotiate. Nothing works, because the mechanism for continuing does not exist. The model itself has no idea a rule fired. It reads the result and finds its world has changed.

There is one more piece: the hidden data layer. That global_data object in the code above is a data store that rides along with the call. Every tool handler receives it. The model never sees it unless you deliberately surface a specific field. The full account record, the risk flags, the deck of cards, the card number a payment flow collects: all of it can flow through your handlers while staying entirely outside the model’s world. The model cannot leak a value it never received, and cannot negotiate over a threshold it has never seen.

Put the locks together and you get the property that separates governance from guardrails: the model does not know it is being governed. There is nothing to reason around. And you get a claim you can make to a compliance auditor with a straight face: a wrong word can never become a wrong action. Our standing test is blunt. Replace the model with a rigid touch-tone menu and a governed agent still produces correct outcomes through the same handlers. The experience gets worse. The correctness does not move.

The toolbox

Everything below is a mechanism you command. This is the survey; each item goes deeper in the SDK documentation.

Prompts as structure, not walls of text. Prompts are built from named sections with bodies and bullets, a format we call the Prompt Object Model (POM). Sections are addressable: code can add, extend, or check them, which is what lets capabilities inject their own guidance without trampling yours. Your prompt becomes reviewable structure instead of a fragile blob.

Tools, three ways. Define tools with a decorator and a JSON schema, or skip the schema entirely: type-hinted Python parameters and a docstring become the schema automatically. For pure REST lookups, DataMap tools run on SignalWire’s servers with no webhook at all: you declare the URL template and the response format, and the platform makes the call. Your infrastructure is not even involved.

Skills: capabilities as one-liners. A skill is a packaged capability: tools, prompt guidance, configuration, and dependency checks in one module. agent.add_skill("web_search", {...}) and your agent searches the web. Seventeen ship in the box, including web search, Wikipedia, weather, maps, document search, call transfer, and background audio. Writing your own is a small class, and it becomes a one-liner for every agent your team builds after that.

Conversations as workflows. The step system described above also handles structured intake. Declare a list of typed questions and the platform asks them one at a time. It confirms the answers you flag, locks the model out of wandering off mid-form, and delivers a clean typed record to your code. Forms without the model ever seeing a form.

Actions: what your handlers can command. A tool result can carry around forty distinct actions. Transfer the call, hang up, send an SMS, start or stop recording. Collect a payment through a PCI-scoped flow where card digits never touch the model or the recording. Play background audio, update the hidden data layer, jump steps, or adjust the AI’s own settings mid-call. Push live events to a web page synchronized with the call. Reach into a second live call to orchestrate a screened transfer. Each one is a line of Python in a handler you wrote.

Prebuilt agents. Five common archetypes ship as classes you instantiate with data: an information gatherer, a survey runner, a receptionist that routes to departments, an FAQ answerer, and a concierge. A working receptionist is a constructor call with a department list.

One deployment, many tenants. Register a callback and every incoming request gets a fresh, disposable copy of your agent to customize: different prompt, voice, skills, and data per customer, keyed off headers or query parameters. One process serves hundreds of differently configured tenants with no cross-contamination.

Sounding human. Declare languages and pick voices per language, including per-persona voices that change when the conversation changes departments. Add vocabulary hints so the recognizer catches your product names, and pronunciation rules so the voice says them correctly. Add filler phrases so callers never hear dead air while a tool runs or the workflow advances.

Knowledge on board. Build a searchable index from your documentation with one command, ship it as a single file next to your agent, and add the search skill. Your agent answers from your documents, even on serverless platforms, without an external vector database.

Test like software. Because the agent is a class, it slots into CI. swaig-test executes any tool with any arguments and dumps the generated document. That lets you pin your tool inventory, step whitelists, and per-tenant differences in automated tests. Agents get code review, diffs, and version control, because they are code.

Getting the most out of it

Ten practices separate a demo from a production agent. All of them are your leverage, not busywork.

1. Write tool descriptions for the model, not for teammates. The description is read by the model on every turn and drives its decision to call. Say when to use the tool and what it is not for. Vague descriptions are the top cause of tools that never fire.

2. Keep each step’s tool list small. Seven or eight active tools is the practical ceiling before selection accuracy slips. Scope tools to steps and split busy steps.

3. Declare the tool list on every step. A step that declares nothing inherits the previous step’s tools, which is rarely what you want. Treat omission as a decision, not a default.

4. Carry facts in the data layer, not in hope. When a handler learns something the next phase needs, store it in global_data and have the next step’s text reference it. Then write that step to confirm the fact rather than ask again.

5. Let the prompt persuade and the step enforce. Anything that must be true belongs in a handler or a step boundary. Prose is for tone.

6. Give every wait a voice. Configure filler phrases for tool calls and workflow transitions. Silence is where callers hang up.

7. Read the generated document. swaig-test --dump-swml shows exactly what the platform and model will see. Most mysteries dissolve there.

8. Reach for skills and prebuilt agents first. The fastest custom agent is the one where you only wrote the parts unique to your business.

9. Match the model to the work. Scoped steps carry short prompts and few tools, so simpler steps run well on smaller, cheaper models. Governance and economy come from the same mechanism.

10. Put the agent in CI. Test handlers like functions, pin the document in golden tests, and review changes like any other pull request. Your agent is software. Insist on it.

The payoff

Build this way and something compounding happens. When a better model ships, you swap it with a configuration change, and nothing breaks, because correctness never lived in the model. The prompt-and-pray shops rewrite their prompts and re-run their prayers. Your agent gets more natural for free while staying exactly as correct as your code, which is to say: as correct as you made it.

There is a second payoff, and it lands in the risk review. Ungoverned agents pass review by removal: fewer tools, closed questions, scripted intents, until nothing useful is left. A governed agent walks in with its capability intact, because the guarantees are architectural: the tools are scoped, the actions are validated, the sensitive data never entered the model’s context. Everyone else makes AI safe by making it do less. Governance makes it safe enough to be given more.

That is the real merger of human and AI power. Not a human writing longer letters to a black box, but a division of labor between two parties doing what each does best. The model brings language nobody could program by hand. You bring judgment, structure, and enforcement nobody should ever ask a probability distribution to provide. The SDK is where the two meet: your rules, made mechanical, wrapped around the most capable conversational technology ever built, running on infrastructure that has carried real-world calls for twenty years.

The model was never the hero of this story. It is the best supporting actor in history. The hero is the person who makes it safe to put on the phone.

Start here:

pip install signalwire-sdk
swaig-test your_agent.py --dump-swml
pip install signalwire-sdk
swaig-test your_agent.py --dump-swml
pip install signalwire-sdk
swaig-test your_agent.py --dump-swml
pip install signalwire-sdk
swaig-test your_agent.py --dump-swml

The SDK ships in ten languages with the same concepts and names in each, including Python, TypeScript, Go, Ruby, Java, Perl, and C++. Get documentation, examples, and the full guides here.

Then bring your questions to our dev community on Discord.

Related Resources

Bottom Linear Gradient  Lines image

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.

The Communications Stack for What's Next

APIs built for speed. Infrastructure built for scale. AI built in from day one.